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)
/opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/phiml/math/_tensors.py:1377: RuntimeWarning: invalid value encountered in power
  result = op(n1, n2)
/opt/hostedtoolcache/Python/3.12.9/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]:

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)
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.9/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.9/x64/lib/python3.12/site-packages/phiml/math/_ops.py:1075, in grid_sample(grid, coordinates, extrap, **kwargs)
   1073 assert channel(coordinates).rank == 1, f"coordinates must have at most one channel dimension but got {channel(coordinates)}"
   1074 coordinates = rename_dims(coordinates, channel, 'vector')
-> 1075 result = broadcast_op(functools.partial(_grid_sample, extrap=extrap, pad_kwargs=kwargs), [grid, coordinates])
   1076 return result

File /opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/phiml/math/_ops.py:1149, in broadcast_op(operation, tensors, iter_dims, no_return)
   1147 iter_dims = broadcast_dims(*tensors) if iter_dims is None else iter_dims
   1148 if len(iter_dims) == 0:
-> 1149     return operation(*tensors)
   1150 else:
   1151     if isinstance(iter_dims, SHAPE_TYPES):

File /opt/hostedtoolcache/Python/3.12.9/x64/lib/python3.12/site-packages/phiml/math/_ops.py:1088, in _grid_sample(grid, coordinates, extrap, pad_kwargs)
   1080 """
   1081 Args:
   1082     grid:
   (...)   1085     pad_kwargs:
   1086 """
   1087 dim_names = channel(coordinates).item_names[0] or grid.shape.spatial.names
-> 1088 dims = grid.shape.only(dim_names, reorder=True)
   1089 assert len(dims) == len(dim_names), f"all grid dims {dim_names} must be present on grid but got shape {grid.shape}"
   1090 if grid.shape.batch == coordinates.shape.batch or grid.shape.batch.volume == 1 or coordinates.shape.batch.volume == 1:
   1091     # call backend.grid_sample()

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