<jupyter_start><jupyter_text>Understanding Structured Point Clouds (SPCs) Structured Point Clouds (SPC) is a differentiable, GPU-compatible, spatial-data structure which efficiently organizes 3D geometrically sparse information in a very compressed manner.![alt text](../samples/spcTeapot.png "Structured Point Cloud Hierarchy") When should you use it? * The SPC data structure is very general, which makes it a suitable building block for a variety of applications.* Examples include:     * [Representation & rendering of implicit 3D surfaces](https://nv-tlabs.github.io/nglod/)    * Convolutions on voxels, meshes and point clouds    * and more..SPCs are easily convertible from point clouds and meshes, and can be optimized to represent encoded neural implicit fields. In this tutorial you will learn to: 1. Construct a SPC from triangular meshes and point clouds.2. Visualize the SPC using ray-tracing functionality.3. Become familiar with the internals of kaolin's SPC data structurePractitioners are encouraged to view the [documentation](https://kaolin.readthedocs.io/en/latest/modules/kaolin.ops.spc.html?highlight=spckaolin-ops-spc) for additional details about the internal workings of this data structure. This tutorial is best run locally to observe the full output.  Setup This tutorial assumes a minimal version of [kaolin v0.10.0](https://kaolin.readthedocs.io/en/latest/notes/installation.html). In addition, the following libraries are required for this tutorial:<jupyter_code>!pip install -q matplotlib
!pip install -q termcolor
!pip install -q ipywidgets
from PIL import Image
import torch
import torch.nn.functional as F
import numpy as np
from matplotlib import pyplot as plt
import ipywidgets as widgets
from termcolor import colored

import kaolin as kal
from spc_formatting import describe_octree, color_by_level<jupyter_output><empty_output><jupyter_text>To study the mechanics of the SPC structure, we'll need some auxilary functions (you may skip for now):<jupyter_code>def describe_tensor(torch_tensor, tensor_label, with_shape, with_content):
    if with_shape:
        print(f'"{tensor_label}" is a {torch_tensor.dtype} tensor of size {tuple(torch_tensor.shape)}')
    if with_content:
        print(f'Raw Content: \n{torch_tensor.cpu().numpy()}')
        

def convert_texture_to_torch_sample_format(texture):
    """ Convert to (1, C, Tex-H, Tex-W) format """
    return texture.unsqueeze(0).type(sampled_uvs.dtype).permute(0, 3, 1, 2)<jupyter_output><empty_output><jupyter_text>Preliminaries: Load Mesh and sample as Point Cloud Throughout this tutorial we'll be using a triangular mesh as an example. First, we import the mesh using kaolin:<jupyter_code># Path to some .obj file with textures
mesh_path = "../samples/colored_sphere.obj"
mesh = kal.io.obj.import_mesh(mesh_path, with_materials=True)

print(f'Loaded mesh with {len(mesh.vertices)} vertices, {len(mesh.faces)} faces and {len(mesh.materials)} materials.')<jupyter_output>Loaded mesh with 642 vertices, 1280 faces and 1 materials.<jupyter_text>Next, we'll oversample the mesh faces to make sure our SPC structure is densely populated and avoids "holes"at the highest resolution level.Note that our mesh face-vertices are mapped to some texture coordinates.Luckily, kaolin has a `sample_points` function that will take care of interpolating these coordinates for us. The sampled vertices will be returned along with the interpolated uv coordinates as well:<jupyter_code># Sample points over the mesh surface
num_samples = 1000000

# Load the uv coordinates per face-vertex like "features" per face-vertex,
# which sample_points will interpolate for new sample points.
# mesh.uvs is a tensor of uv coordinates of shape (#num_uvs, 2), which we consider as "features" here
# mesh.face_uvs_idx is a tensor of shape (#faces, 3), indexing which feature to use per-face-per-vertex
# Therefore, face_features will be of shape (#faces, 3, 2)
face_features = mesh.uvs[mesh.face_uvs_idx]

# Kaolin assumes an exact batch format, we make sure to convert from:
# (V, 3) to (1, V, 3)
# (F, 3, 2) to (1, F, 3, 2)
# where 1 is the batch size
batched_vertices = mesh.vertices.unsqueeze(0)
batched_face_features = face_features.unsqueeze(0)

# sample_points is faster on cuda device
batched_vertices = batched_vertices.cuda()
faces = mesh.faces.cuda()
batched_face_features = batched_face_features.cuda()

sampled_verts, _, sampled_uvs = kal.ops.mesh.trianglemesh.sample_points(batched_vertices,
                                                                        faces,
                                                                        num_samples=num_samples,
                                                                        face_features=batched_face_features)

print(f'Sampled {sampled_verts.shape[1]} points over the mesh surface:')
print(f'sampled_verts is a tensor with batch size {sampled_verts.shape[0]},',
      f'with {sampled_verts.shape[1]} points of {sampled_verts.shape[2]}D coordinates.')

print(f'sampled_uvs is a tensor with batch size {sampled_uvs.shape[0]},',
      f'representing the corresponding {sampled_uvs.shape[1]} {sampled_uvs.shape[2]}D UV coordinates.')<jupyter_output>Sampled 1000000 points over the mesh surface:
sampled_verts is a tensor with batch size 1, with 1000000 points of 3D coordinates.
sampled_uvs is a tensor with batch size 1, representing the corresponding 1000000 2D UV coordinates.<jupyter_text>To finish our setup, we'll want to use the UV coordinates to perform texture sampling and obtain the RGB color of each point we have:<jupyter_code># Convert texture to sample-compatible format
diffuse_color = mesh.materials[0]['map_Kd']    # Assumes a shape with a single material
texture_maps = convert_texture_to_torch_sample_format(diffuse_color) # (1, C, Th, Tw)
texture_maps = texture_maps.cuda()

# Sample colors according to uv-coordinates
sampled_uvs = kal.render.mesh.utils.texture_mapping(texture_coordinates=sampled_uvs,
                                                    texture_maps=texture_maps,
                                                    mode='nearest')
# Unbatch
vertices = sampled_verts.squeeze(0)
vertex_colors = sampled_uvs.squeeze(0)

# Normalize to [0,1]
vertex_colors /= 255

print(f'vertices is a tensor of {vertices.shape}')
print(f'vertex_colors is a tensor of {vertices.shape}')<jupyter_output>vertices is a tensor of torch.Size([1000000, 3])
vertex_colors is a tensor of torch.Size([1000000, 3])<jupyter_text>1. Create & Visualize SPC  Create the SPC We start by converting our Point Cloud of continuous 3D coordinates to a Structured Point Cloud. `unbatched_pointcloud_to_spc` will return a `Spc` object, a data class holding all Structured Point Cloud related information. At the core of this object, points are kept in quantized coordinates using a compressed octree. The returned object contains multiple low-level data structures which we'll explore in details in the next section.For now keep in mind that its important fields: `octree`, `features`, `point_hierarchy`, `pyramid` and `prefix`, represent our data structure.When constructing a `Spc` object, the resolution of quantized coordinates can be controlled by the octree `level` arg, such that: $resolution=2^{level}$<jupyter_code># Our SPC will contain a hierarchy of multiple levels
level = 3
spc = kal.ops.conversions.pointcloud.unbatched_pointcloud_to_spc(vertices, level, features=vertex_colors)<jupyter_output><empty_output><jupyter_text>Set-up the camera The SPC data structure can be efficiently visualized using ray-tracing ops. Note that SPC also supports differentiable rendering. In this tutorial we'll limit our demonstration to rendering this data structure efficiently. Differentiable ray-tracing is beyond the scope of this guide, and will be covered in future tutorials. To begin our ray tracing implementation, we'll first need to set up our camera view and [generate some rays](https://www.scratchapixel.com/lessons/3d-basic-rendering/ray-tracing-generating-camera-rays). We'll assume a pinhole camera model, and use the `look_at` function, which sets up a camera view originating at position `camera_from`, looking towards `camera_to`. `width`, `height`, `mode` and `fov` will determine the dimensions of our view.<jupyter_code>def _normalized_grid(width, height, device='cuda'):
    """Returns grid[x,y] -> coordinates for a normalized window.
    
    Args:
        width, height (int): grid resolution
    """

    # These are normalized coordinates
    # i.e. equivalent to 2.0 * (fragCoord / iResolution.xy) - 1.0
    window_x = torch.linspace(-1, 1, steps=width, device=device) * (width / height)
    window_y = torch.linspace(1,- 1, steps=height, device=device)

    coord = torch.stack(torch.meshgrid(window_x, window_y)).permute(1,2,0)
    return coord


def look_at(camera_from, camera_to, width, height, mode='persp', fov=90.0, device='cuda'):
    """Vectorized look-at function, returns an array of ray origins and directions
    URL: https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/lookat-function
    """

    camera_origin = torch.FloatTensor(camera_from).to(device)
    camera_view = F.normalize(torch.FloatTensor(camera_to).to(device) - camera_origin, dim=0)
    camera_right = F.normalize(torch.cross(camera_view, torch.FloatTensor([0,1,0]).to(device)), dim=0)
    camera_up = F.normalize(torch.cross(camera_right, camera_view), dim=0)

    coord = _normalized_grid(width, height, device=device)
    ray_origin = camera_right * coord[...,0,np.newaxis] * np.tan(np.radians(fov/2)) + \
                 camera_up * coord[...,1,np.newaxis] * np.tan(np.radians(fov/2)) + \
                 camera_origin + camera_view
    ray_origin = ray_origin.reshape(-1, 3)
    ray_offset = camera_view.unsqueeze(0).repeat(ray_origin.shape[0], 1)
    
    if mode == 'ortho': # Orthographic camera
        ray_dir = F.normalize(ray_offset, dim=-1)
    elif mode == 'persp': # Perspective camera
        ray_dir = F.normalize(ray_origin - camera_origin, dim=-1)
        ray_origin = camera_origin.repeat(ray_dir.shape[0], 1)
    else:
        raise ValueError('Invalid camera mode!')


    return ray_origin, ray_dir<jupyter_output><empty_output><jupyter_text>Now generate some rays using the functions we've just created:<jupyter_code># ray_o and ray_d ~ torch.Tensor (width x height, 3)
# represent rays origin and direction vectors
ray_o, ray_d = look_at(camera_from=[-2.5,2.5,-2.5],
                       camera_to=[0,0,0],
                       width=1024,
                       height=1024,
                       mode='persp',
                       fov=30,
                       device='cuda')

print(f'Total of {ray_o.shape[0]} rays generated.')<jupyter_output>Total of 1048576 rays generated.<jupyter_text>Render We're now ready to perform the actual ray tracing. kaolin will "shoot" the rays for us, and perform an efficient intersection test between each ray and cell within the SPC structure. In kaolin terminology, nuggets are "ray-cell intersections" (or rather "ray-point" intersections).nuggets  are of represented by a structure of two tensors: `nugs_ridx` and `nugs_pidx`, which form together pairs of `(index_to_ray, index_to_points)`. Both tensors are 1-dimensional tensors of shape (`num_intersection`,).<jupyter_code>octree, features = spc.octrees, spc.features
point_hierarchy, pyramid, prefix = spc.point_hierarchies, spc.pyramids[0], spc.exsum
nugs_ridx, nugs_pidx, depth = kal.render.spc.unbatched_raytrace(octree, point_hierarchy, pyramid, prefix, ray_o, ray_d, level)
print(f'Total of {nugs_ridx.shape[0]} nuggets were traced.\n')<jupyter_output>Total of 1195190 nuggets were traced.<jupyter_text>Since we're assuming here our surface is opaque, for each ray, we only care about the nuggetclosest to the camera. Note that per "ray-pack", the returned nuggets are already sorted by depth. The method below returns a boolean mask which specifies which nuggets represent a "first-hit".<jupyter_code>masked_nugs = kal.render.spc.mark_pack_boundaries(nugs_ridx)
nugs_ridx = nugs_ridx[masked_nugs]
nugs_pidx = nugs_pidx[masked_nugs]<jupyter_output><empty_output><jupyter_text>Finally, for each ray that hit the surface, a corresponding "first-hit" nugget exists.<jupyter_code># 1. We initialize an empty canvas.
image = torch.ones_like(ray_o)

# 2. We'll query all first-hit nuggets to obtain their corresponding point-id (which cell they hit in the SPC).
ridx = nugs_ridx.long()
pidx = nugs_pidx.long() - pyramid[1,level]

# 3. We'll query the features auxilary structure to obtain the color.
# 4. We set each ray value as the corresponding nugget color.
image[ridx] = features[pidx]

image = image.reshape(1024, 1024, 3)<jupyter_output><empty_output><jupyter_text>Putting it all together, we write our complete `render()` function and display the trace using matplotlib:<jupyter_code>import matplotlib.pyplot as plt

def render(level):
    """ Create & render an image """
    spc = kal.ops.conversions.pointcloud.unbatched_pointcloud_to_spc(vertices, level, vertex_colors)
    octree, features, point_hierarchy, pyramid, prefix = spc.octrees, spc.features, spc.point_hierarchies, spc.pyramids[0], spc.exsum
    
    nugs_ridx, nugs_pidx, depth = kal.render.spc.unbatched_raytrace(octree, point_hierarchy, pyramid, prefix, ray_o, ray_d, level)
    masked_nugs = kal.render.spc.mark_pack_boundaries(nugs_ridx)
    nugs_ridx = nugs_ridx[masked_nugs]
    nugs_pidx = nugs_pidx[masked_nugs]
    
    ridx = nugs_ridx.long()
    pidx = nugs_pidx.long() - pyramid[1,level]
    image = torch.ones_like(ray_o)
    image[ridx] = features[pidx]
    image = image.reshape(1024, 1024, 3)
    return image


fig = plt.figure(figsize=(20,10))

# Render left image of level 3 SPC
image1 = render(level=3)
image1 = image1.cpu().numpy().transpose(1,0,2)
ax = fig.add_subplot(1, 2, 1)
ax.set_title("level 3", fontsize=26)
ax.axis('off')
plt.imshow(image1)

# Render right image of level 5 SPC
image2 = render(level=8)
image2 = image2.cpu().numpy().transpose(1,0,2)
ax = fig.add_subplot(1, 2, 2)
ax.set_title("level 5", fontsize=26)
ax.axis('off')
plt.imshow(image2)

plt.show()<jupyter_output><empty_output><jupyter_text>Finally, putting it all together, we may also construct the following interactive demo:<jupyter_code>def update_demo(widget_spc_level):
    image = render(widget_spc_level)
    plt.figure(figsize=(10,10))
    plt.axis('off')
    plt.imshow(image.cpu().numpy().transpose(1,0,2))
    plt.show()

def show_interactive_demo(max_level=10):
    start_value = min(7, max_level)
    widget_spc_level = widgets.IntSlider(value=start_value, min=1, max=max_level, step=1, orientation='vertical',
                                     description='<h5>SPC Level:</h5>', disabled=False,
                                     layout=widgets.Layout(height='100%',))
    
    out = widgets.interactive_output(update_demo, {'widget_spc_level': widget_spc_level})
    display(widgets.HBox([widgets.VBox([widget_spc_level]), out]))

show_interactive_demo()<jupyter_output><empty_output><jupyter_text>2. SPC internals In this section we'll explore the various components that make up the [SPC](https://kaolin.readthedocs.io/en/latest/modules/kaolin.ops.spc.html?highlight=spcstructured-point-clouds) we've just created. We'll learn how data is stored, and how to view stored data.  Boilerplate code Let's rebuild our SPC object with fewer levels, that will make the internals easier to study. You may customize the number of levels and compare how the output changes.<jupyter_code>level = 3
spc = kal.ops.conversions.pointcloud.unbatched_pointcloud_to_spc(vertices, level, features=vertex_colors)<jupyter_output><empty_output><jupyter_text>Ok, let's see what we've got.  octree The first field we'll look into, `octrees`, keeps the entire geometric structure in a compressed manner. This is a huge advantage, as this structure is now small enough to fit our sparse data, which makes it very efficient.<jupyter_code>octree = spc.octrees
describe_tensor(torch_tensor=octree, tensor_label='octree', with_shape=True, with_content=True)

print(f'\n"octrees" represents a hierarchy of {len(octree)} octree nodes.')
print(f"Let's have a look at the binary representation and what it means:\n")

describe_octree(octree, level)

text_out = widgets.Output(layout={'border': '0.2px dashed black'})
with text_out:
    print('How to read the content of octrees?')
    print('- Each entry represents a single octree of 8 cells --> 8 bits.')
    print('- The bit position determines the cell index, in Morton Order.')
    print('- The bit value determines if the cell is occupied or not.')
    print(f'- If a cell is occupied, an additional octree may be generated in the next level, up till level {level}.')
    print('For example, an entry of 00000001 is a single level octree, where only the bottom-left most cell is occupied.')
    
display(widgets.HBox([text_out]))<jupyter_output>"octree" is a torch.uint8 tensor of size (17,)
Raw Content: 
[255 128  64  32  16   8   4   2   1 127 191 223 239 247 251 253 254]

"octrees" represents a hierarchy of 17 octree nodes.
Let's have a look at the binary representation and what it means:

Level [31m#1, [0mPath[31m*[0m,        11111111
Level [34m#2, [0mPath[31m*[0m-[34m1[0m,      10000000
Level [34m#2, [0mPath[31m*[0m-[34m2[0m,      01000000
Level [34m#2, [0mPath[31m*[0m-[34m3[0m,      00100000
Level [34m#2, [0mPath[31m*[0m-[34m4[0m,      00010000
Level [34m#2, [0mPath[31m*[0m-[34m5[0m,      00001000
Level [34m#2, [0mPath[31m*[0m-[34m6[0m,      00000100
Level [34m#2, [0mPath[31m*[0m-[34m7[0m,      00000010
Level [34m#2, [0mPath[31m*[0m-[34m8[0m,      00000001
Level [32m#3, [0mPath[31m*[0m-[34m1[0m-[32m8[0m,    01111111
Level [32m#3, [0mPath[31m*[0m-[34m2[0m-[32m7[0m,    10111111
Level [32m#3, [0mPath[31m*[0m-[34m3[0m-[32m6[0m,    11011111
Level [32[...]<jupyter_text>Order of octants within partitioned cells![alt text](../samples/octants.png "Octants") Notice the field is named in plural.That's because kaolin can batch multiple instances of octrees together within the same object.<jupyter_code>print(spc.batch_size)<jupyter_output>1<jupyter_text>Pay attention that `octrees` uses [packed representation](https://kaolin.readthedocs.io/en/latest/modules/kaolin.ops.batch.html?highlight=packedpacked), meaning, there is no explicit batch dimension. Instead, we track the length of each octree instance in a separate field:<jupyter_code>octrees_lengths = spc.lengths
describe_tensor(torch_tensor=octrees_lengths, tensor_label='lengths', with_shape=True, with_content=True)

text_out = widgets.Output(layout={'border': '0.2px dashed black'})
with text_out:
    print('How to read the content of lengths?')
    print(f'- This Spc stores a batch of {len(spc.lengths)} octrees.')
    print(f'- The first octree is represented by {spc.lengths[0]} non-leaf cells.')
    print(f'- Therefore the information of the first octree is kept in bytes 0-{spc.lengths[0]-1} of the octrees field.')
    
display(widgets.HBox([text_out]))<jupyter_output>"lengths" is a torch.int32 tensor of size (1,)
Raw Content: 
[17]<jupyter_text>Advanced users who prefer a non object-oriented lower-level api can also use the following functionality which `kal.ops.conversions.pointcloud.unbatched_pointcloud_to_spc` employs under the hood:<jupyter_code>from kaolin.ops.spc.points import quantize_points, points_to_morton, morton_to_points, unbatched_points_to_octree

# Construct a batch of 2 octrees. For brevity, we'll use the same ocuupancy data for both.
# 1. Convert continous to quantized coordinates
# 2. Build the octrees

points1 = quantize_points(vertices.contiguous(), level=2)
octree1 = unbatched_points_to_octree(points1, level=2)

points2 = quantize_points(vertices.contiguous(), level=3)
octree2 = unbatched_points_to_octree(points2, level=3)

# Batch 2 octrees together. For packed representations, this is just concatenation.
octrees = torch.cat((octree1, octree2), dim=0)
lengths = torch.tensor([len(octree1), len(octree2)], dtype=torch.int32)

describe_tensor(torch_tensor=octrees, tensor_label='octrees', with_shape=True, with_content=True)
print('')
describe_tensor(torch_tensor=lengths, tensor_label='lengths', with_shape=True, with_content=True)<jupyter_output>"octrees" is a torch.uint8 tensor of size (26,)
Raw Content: 
[255 128  64  32  16   8   4   2   1 255 128  64  32  16   8   4   2   1
 127 191 223 239 247 251 253 254]

"lengths" is a torch.int32 tensor of size (2,)
Raw Content: 
[ 9 17]<jupyter_text>These structures form the bare minimum required to shift back to high-level api and construct a Spc object:<jupyter_code>kal.rep.spc.Spc(octrees, lengths)<jupyter_output><empty_output><jupyter_text>features So far we've lookied into how Structured Point Clouds keep track of occupancy. Next we'll study how they keep track of features. The `features` field contains features information per cell.<jupyter_code>features = spc.features
def paint_features(features):
    plt.figure(figsize=(10,10))
    plt.axis('off')
    plt.imshow(features.cpu().numpy()[None])
    plt.show()
    
print('In this tutorial, cell features are RGB colors:')
describe_tensor(torch_tensor=features, tensor_label='features', with_shape=True, with_content=False)
paint_features(features)

text_out = widgets.Output(layout={'border': '0.2px dashed black'})
with text_out:
    print('How to read the content of features?')
    print(f'- We keep features only for leaf cells, a total of {features.shape[0]}.')
    print(f'- The number of leaf cells can be obtained by summarizing the "1" bits at level {spc.max_level},\n' \
    '  the last level of the octree.')
    print(f'- The dimensionality of each attribute is {features.shape[1]} (e.g: RGB channels)')
    print('\nReminder - the highest level of occupancy octree is:')
    describe_octree(spc.octrees, level, limit_levels=[spc.max_level])
    

display(widgets.HBox([text_out]))<jupyter_output>In this tutorial, cell features are RGB colors:
"features" is a torch.float32 tensor of size (56, 3)<jupyter_text>pyramid & exsum Since the occupancy information is [compressed](https://kaolin.readthedocs.io/en/latest/modules/kaolin.ops.spc.html?highlight=spcoctree) and [packed](https://kaolin.readthedocs.io/en/latest/modules/kaolin.ops.batch.html?highlight=packedpacked), accessing level-specific information consistently involvescumulative summarization of the number of "1" bits. It makes sense to calculate this information once and then cache it. The `pyramid` field does exactly that: it keeps summarizes the number of occupied cells per level, and their cumsum, for fast level-indexing.<jupyter_code># A pyramid is kept per octree in the batch.
# We'll study the pyramid of the first and only entry in the batch.
pyramid = spc.pyramids[0]
describe_tensor(torch_tensor=pyramid, tensor_label='pyramid', with_shape=True, with_content=True)

out_left = widgets.Output(layout={'border': '0.2px dashed black'})
out_right = widgets.Output(layout={'border': '0.2px dashed black'})

print('\nHow to read the content of pyramids?')

with out_left:
    print('"pyramid" summarizes the number of occupied \ncells per level, and their cumulative sum:\n')
    for i in range(pyramid.shape[-1]):
        if i ==0:
            print(f'Root node (implicitly defined):')
        elif i+1 < pyramid.shape[-1]:
            print(f'Level #{i}:')
        else:
            print(f'Final entry for total cumsum:')
        print(f'\thas {pyramid[0,i]} occupied cells')
        print(f'\tstart idx (cumsum excluding current level): {pyramid[1,i]}')
        
with out_right:
    print(f'"octrees" represents a hierarchy of {len(spc.octrees)} octree nodes.')
    print(f"Each bit represents a cell occupancy:\n")
    describe_octree(octree, level)
    
    
display(widgets.HBox([out_left, out_right]))<jupyter_output>"pyramid" is a torch.int32 tensor of size (2, 5)
Raw Content: 
[[ 1  8  8 56  0]
 [ 0  1  9 17 73]]

How to read the content of pyramids?<jupyter_text>Similarly, kaolin keeps a complementary field, `exsum`, which tracks the cumulative summarization of bits per-octree to fast access parent-child information between levels:<jupyter_code>exsum = spc.exsum
describe_tensor(torch_tensor=exsum, tensor_label='exsum', with_shape=True, with_content=True)

out_left = widgets.Output(layout={'border': '0.2px dashed black'})
out_right = widgets.Output(layout={'border': '0.2px dashed black'})

print('\nHow to read the content of exsum?')

with out_left:
    print('"exsum" summarizes the cumulative number of occupied \ncells per octree, e.g: exclusive sum of "1" bits:\n')
    for i in range(exsum.shape[-1]):
        print(f'Cells in Octree #{i} start from cell idx: {exsum[i]}')
        
with out_right:
    print(f'"octrees" represents a hierarchy of {len(octree)} octree nodes.')
    print(f"Each bit represents a cell occupancy:\n")
    describe_octree(octree, level)
    
    
display(widgets.HBox([out_left, out_right]))<jupyter_output>"exsum" is a torch.int32 tensor of size (18,)
Raw Content: 
[ 0  8  9 10 11 12 13 14 15 16 23 30 37 44 51 58 65 72]

How to read the content of exsum?<jupyter_text>When using Spc objects, pyramids are implicitly created the first time they are needed so you don't have to worry about them. For advanced users, the low-level api allows their explicit creation through `scan_octrees`:<jupyter_code>lengths = torch.tensor([len(octrees)], dtype=torch.int32)
max_level, pyramid, exsum = kal.ops.spc.spc.scan_octrees(octree, lengths)

print('max_level:')
print(max_level)

print('\npyramid:')
print(pyramid)

print('\nexsum:')
print(exsum)<jupyter_output>max_level:
3

pyramid:
tensor([[[ 1,  8,  8, 56,  0],
         [ 0,  1,  9, 17, 73]]], dtype=torch.int32)

exsum:
tensor([ 0,  8,  9, 10, 11, 12, 13, 14, 15, 16, 23, 30, 37, 44, 51, 58, 65, 72,
        72, 72, 72, 72, 72, 72, 72, 72, 77], device='cuda:0',
       dtype=torch.int32)<jupyter_text>point_hierarchies `point_hierarchies` is an auxilary field, which holds the *sparse* coordinates of each point / occupied cell within the octree, for easier access.Sparse coordinates are packed for all cells on all levels combined.<jupyter_code>describe_tensor(torch_tensor=spc.point_hierarchies, tensor_label='point_hierarchies', with_shape=True, with_content=False)<jupyter_output>"point_hierarchies" is a torch.int16 tensor of size (73, 3)<jupyter_text>We can use the information stored in the pyramids field to color the coordinates by level:<jupyter_code>out_left = widgets.Output(layout={'border': '0.2px dashed black'})
out_right = widgets.Output(layout={'border': '0.2px dashed black', 'width': '60%'})

max_points_to_display = 17  # To avoid clutter

with out_left:
    level_idx =0
    point_idx = 0
    remaining_cells_per_level = spc.pyramids[0,0].cpu().numpy().tolist()

    for coord in spc.point_hierarchies:
        if not remaining_cells_per_level[level_idx]:
            level_idx += 1
            point_idx = 0
        else:
            remaining_cells_per_level[level_idx] -= 1

        if point_idx == max_points_to_display:
            print(colored(f'skipping more..', level_color))
        elif point_idx < max_points_to_display:
            level_color = color_by_level(level_idx - 1)
            print(colored(f'Level #{level_idx}, Point #{point_idx}, ' \
                          f'Coords: {coord.cpu().numpy().tolist()}', level_color))
        point_idx += 1

with out_right:
    print('How to read the content of point_hierarchies?')
    print(f'- Each cell / point is represented by {spc.point_hierarchies.shape[-1]} indices (xyz).')
    print('- Sparse coordinates are absolute: \n  they are defined relative to the octree origin.')    
    print('- Compare the point coordinates with the demo below.\n\n  Remember: unoccupied cells are not displayed!')
    show_interactive_demo(max_level=spc.max_level)

display(widgets.HBox([out_left, out_right]))<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.10617891396584304, 'fraction_numerical': 0.02959318287160837, 'mean_word_length': 4.51583087512291, 'pattern_counts': {'":': 0, '<': 91, '<?xml version=': 0, '>': 91, 'https://': 9, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 5, 'word_list_counts': {'cursed_substrings.json': 23, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '25', 'dir': 'jupyter-structured-clean-dedup', 'id': '15404', 'license': 'ECL-2.0', 'n_tokens_mistral': 9709, 'n_tokens_neox': 8960, 'n_words': 2755, 'path': 'examples/tutorial/understanding_spcs_tutorial.ipynb', 'repo_name': 'zookae/kaolin', 'size': '28047'}