Core#

tensor.py#

  1# ----------------------------------------------------------------------------
  2# -                        CloudViewer: www.cloudViewer.org                  -
  3# ----------------------------------------------------------------------------
  4# Copyright (c) 2018-2024 www.cloudViewer.org
  5# SPDX-License-Identifier: MIT
  6# ----------------------------------------------------------------------------
  7
  8import cloudViewer.core as cv3c
  9import numpy as np
 10import os
 11import pickle
 12import tempfile
 13
 14# # Tensor
 15#
 16# Tensor is a "view" of a data Blob with shape, stride, and a data pointer. I
 17# t is a multidimensional and homogeneous matrix containing elements of single data type.
 18# It is used in CloudViewer to perform numerical operations. It supports GPU operations as well.
 19#
 20# ## Tensor creation
 21#
 22# Tensor can be created from list, numpy array, another tensor.
 23# A tensor of specific data type and device can be constructed by passing a ```cv3c.Dtype``` and/or ```cv3c.Device```
 24# to a constructor. If not passed, the default data type is inferred from the data, and the default device is CPU.
 25# Note that while creating tensor from a list or numpy array, the underlying memory is not shared and a copy is created.
 26
 27# Tensor from list.
 28a = cv3c.Tensor([0, 1, 2])
 29print("Created from list:\n{}".format(a))
 30
 31# Tensor from Numpy.
 32a = cv3c.Tensor(np.array([0, 1, 2]))
 33print("\nCreated from numpy array:\n{}".format(a))
 34
 35# Dtype and inferred from list.
 36a_float = cv3c.Tensor([0.0, 1.0, 2.0])
 37print("\nDefault dtype and device:\n{}".format(a_float))
 38
 39# Specify dtype.
 40a = cv3c.Tensor(np.array([0, 1, 2]), dtype=cv3c.Dtype.Float64)
 41print("\nSpecified data type:\n{}".format(a))
 42
 43# Specify device.
 44a = cv3c.Tensor(np.array([0, 1, 2]), device=cv3c.Device("CUDA:0"))
 45print("\nSpecified device:\n{}".format(a))
 46
 47#    Tensor can also be created from another tensor by invoking the copy constructor.
 48#    This is a shallow copy, the data_ptr will be copied but the memory it points to will not be copied.
 49
 50# Shallow copy constructor.
 51vals = np.array([1, 2, 3])
 52src = cv3c.Tensor(vals)
 53dst = src
 54src[0] += 10
 55print("\n")
 56# Changes in one will get reflected in other.
 57print("Source tensor:\n{}".format(src))
 58print("\nTarget tensor:\n{}".format(dst))
 59
 60# ## Properties of a tensor
 61
 62vals = np.array((range(24))).reshape(2, 3, 4)
 63a = cv3c.Tensor(vals, dtype=cv3c.Dtype.Float64, device=cv3c.Device("CUDA:0"))
 64print("\n")
 65print(f"a.shape: {a.shape}")
 66print(f"a.strides: {a.strides}")
 67print(f"a.dtype: {a.dtype}")
 68print(f"a.device: {a.device}")
 69print(f"a.ndim: {a.ndim}")
 70
 71# ## Copy & device transfer
 72# We can transfer tensors across host and multiple devices.
 73
 74print("\n")
 75# Host -> Device.
 76a_cpu = cv3c.Tensor([0, 1, 2])
 77a_gpu = a_cpu.cuda(0)
 78print(a_gpu)
 79
 80# Device -> Host.
 81a_gpu = cv3c.Tensor([0, 1, 2], device=cv3c.Device("CUDA:0"))
 82a_cpu = a_gpu.cpu()
 83print(a_cpu)
 84
 85# Device -> another Device.
 86# a_gpu_0 = cv3c.Tensor([0, 1, 2], device=cv3c.Device("CUDA:0"))
 87# a_gpu_1 = a_gpu_0.cuda(1)
 88# print(a_gpu_1)
 89
 90# ## Data Types
 91#
 92# CloudViewer defines seven tensor data types.
 93#
 94# | Data type                | dtype               | byte_size  |
 95# |--------------------------|---------------------|------------|
 96# | Uninitialized Tensor     | cv3c.Dtype.Undefined | -          |
 97# | 32-bit floating point    | cv3c.Dtype.Float32   | 4          |
 98# | 64-bit floating point    | cv3c.Dtype.Float64   | 8          |
 99# | 32-bit integer (signed)  | cv3c.Dtype.Int32     | 4          |
100# | 64-bit integer (signed)  | cv3c.Dtype.Int64     | 8          |
101# | 8-bit integer (unsigned) | cv3c.Dtype.UInt8     | 1          |
102# | Boolean                  | cv3c.Dtype.Bool      | 1          |
103#
104# ### Type casting
105# We can cast tensor's data type. Forced casting might result in data loss.
106
107print("\n")
108
109# E.g. float -> int
110a = cv3c.Tensor([0.1, 1.5, 2.7])
111b = a.to(cv3c.Dtype.Int32)
112print(a)
113print(b)
114
115# E.g. int -> float
116a = cv3c.Tensor([1, 2, 3])
117b = a.to(cv3c.Dtype.Float32)
118print(a)
119print(b)
120
121# ## Numpy I/O with direct memory map
122#
123# Tensors created by passing numpy array to the constructor(```cv3c.Tensor(np.array(...)```)
124# do not share memory with the numpy aray. To have shared memory,
125# you can use ```cv3c.Tensor.from_numpy(...)``` and ```cv3c.Tensor.numpy(...)```.
126# Changes in either of them will get reflected in other.
127
128# Using constructor.
129print("\n")
130np_a = np.ones((5,), dtype=np.int32)
131o3_a = cv3c.Tensor(np_a)
132print(f"np_a: {np_a}")
133print(f"o3_a: {o3_a}")
134print("")
135
136# Changes to numpy array will not reflect as memory is not shared.
137np_a[0] += 100
138o3_a[1] += 200
139print(f"np_a: {np_a}")
140print(f"o3_a: {o3_a}")
141
142# From numpy.
143print("\n")
144np_a = np.ones((5,), dtype=np.int32)
145o3_a = cv3c.Tensor.from_numpy(np_a)
146
147# Changes to numpy array reflects on cloudViewer Tensor and vice versa.
148np_a[0] += 100
149o3_a[1] += 200
150print(f"np_a: {np_a}")
151print(f"o3_a: {o3_a}")
152
153# To numpy.
154print("\n")
155o3_a = cv3c.Tensor([1, 1, 1, 1, 1], dtype=cv3c.Dtype.Int32)
156np_a = o3_a.numpy()
157
158# Changes to numpy array reflects on cloudViewer Tensor and vice versa.
159np_a[0] += 100
160o3_a[1] += 200
161print(f"np_a: {np_a}")
162print(f"o3_a: {o3_a}")
163
164# For CUDA Tensor, call cpu() before calling numpy().
165o3_a = cv3c.Tensor([1, 1, 1, 1, 1], device=cv3c.Device("CUDA:0"))
166print(f"\no3_a.cpu().numpy(): {o3_a.cpu().numpy()}")
167
168# ## PyTorch I/O with DLPack memory map
169# We can convert tensors from/to DLManagedTensor.
170
171import torch
172import torch.utils.dlpack
173
174print("\n")
175# From PyTorch
176th_a = torch.ones((5,)).cuda(0)
177o3_a = cv3c.Tensor.from_dlpack(torch.utils.dlpack.to_dlpack(th_a))
178print(f"th_a: {th_a}")
179print(f"o3_a: {o3_a}")
180print("")
181
182# Changes to PyTorch array reflects on cloudViewer Tensor and vice versa
183th_a[0] = 100
184o3_a[1] = 200
185print(f"th_a: {th_a}")
186print(f"o3_a: {o3_a}")
187
188print("\n")
189# To PyTorch
190o3_a = cv3c.Tensor([1, 1, 1, 1, 1], device=cv3c.Device("CUDA:0"))
191th_a = torch.utils.dlpack.from_dlpack(o3_a.to_dlpack())
192o3_a = cv3c.Tensor.from_dlpack(torch.utils.dlpack.to_dlpack(th_a))
193print(f"th_a: {th_a}")
194print(f"o3_a: {o3_a}")
195print("")
196
197# Changes to PyTorch array reflects on cloudViewer Tensor and vice versa
198th_a[0] = 100
199o3_a[1] = 200
200print(f"th_a: {th_a}")
201print(f"o3_a: {o3_a}")
202
203# ## Binary element-wise operation:
204#
205# Supported element-wise binary operations are:
206# 1. `Add(+)`
207# 2. `Sub(-)`
208# 3. `Mul(*)`
209# 4. `Div(/)`
210# 5. `Add_(+=)`
211# 6. `Sub_(-=)`
212# 7. `Mul_(*=)`
213# 8. `Div_(/=)`
214#
215# Note that the operands have to be of same Device, dtype and Broadcast compatible.
216
217print("\n")
218a = cv3c.Tensor([1, 1, 1], dtype=cv3c.Dtype.Float32)
219b = cv3c.Tensor([2, 2, 2], dtype=cv3c.Dtype.Float32)
220print("a + b = {}".format(a + b))
221print("a - b = {}".format(a - b))
222print("a * b = {}".format(a * b))
223print("a / b = {}".format(a / b))
224
225# Broadcasting follows the same numpy broadcasting rule as given
226# [here](https://numpy.org/doc/stable/user/basics.broadcasting.html).<br>
227# Automatic type casting is done in a way to avoid data loss.
228
229# Automatic broadcasting.
230print("\n")
231a = cv3c.Tensor.ones((2, 3), dtype=cv3c.Dtype.Float32)
232b = cv3c.Tensor.ones((3,), dtype=cv3c.Dtype.Float32)
233print("a + b = \n{}\n".format(a + b))
234
235# Automatic type casting.
236a = a[0]
237print("a + 1 = {}".format(a + 1))  # Float + Int -> Float.
238print("a + True = {}".format(a + True))  # Float + Bool -> Float.
239
240# Inplace.
241a -= True
242print("a = {}".format(a))
243
244# ## Unary element-wise operation:
245# Supported unary element-wise operations are:
246# 1. `sqrt`, `sqrt_`(inplace))
247# 2. `sin`, `sin_`
248# 3. `cos`, `cos_`
249# 4. `neg`, `neg_`
250# 5. `exp`, `exp_`
251# 6. `abs`, `abs_`
252#
253
254print("\n")
255a = cv3c.Tensor([4, 9, 16], dtype=cv3c.Dtype.Float32)
256print("a = {}\n".format(a))
257print("a.sqrt = {}\n".format(a.sqrt()))
258print("a.sin = {}\n".format(a.sin()))
259print("a.cos = {}\n".format(a.cos()))
260
261# Inplace operation
262a.sqrt_()
263print(a)
264
265# ## Reduction:
266#
267# CloudViewer supports following reduction operations.
268# 1. `sum` - returns a tensor with sum of values over a given axis.
269# 2. `mean` - returns a tensor with mean of values over a given axis.
270# 3. `prod` - returns a tensor with product of values over a given axis.
271# 4. `min` - returns a tensor of minimum values along a given axis.
272# 5. `max` - returns a tensor of maximum values along a given axis.
273# 6. `argmin` - returns a tensor of minimum value indices over a given axis.
274# 7. `argmax` - returns a tensor of maximum value indices over a given axis.
275
276print("\n")
277vals = np.array(range(24)).reshape((2, 3, 4))
278a = cv3c.Tensor(vals)
279print("a.sum = {}\n".format(a.sum()))
280print("a.min = {}\n".format(a.min()))
281print("a.ArgMax = {}\n".format(a.argmax()))
282
283# With specified dimension.
284print("\n")
285vals = np.array(range(24)).reshape((2, 3, 4))
286a = cv3c.Tensor(vals)
287
288print("Along dim=0\n{}".format(a.sum(dim=(0))))
289print("Along dim=(0, 2)\n{}\n".format(a.sum(dim=(0, 2))))
290
291# Retention of reduced dimension.
292print("Shape without retention : {}".format(a.sum(dim=(0, 2)).shape))
293print("Shape with retention : {}".format(a.sum(dim=(0, 2), keepdim=True).shape))
294
295# ## Slicing, indexing, getitem, and setitem
296#
297# Basic slicing is done by passing an integer, slice object(```start:stop:step```),
298# index array or boolean array. Slicing and indexing produce a view of the tensor.
299# Hence any change in it will also get reflected in the original tensor.
300
301print("\n")
302vals = np.array(range(24)).reshape((2, 3, 4))
303a = cv3c.Tensor(vals)
304print("a = \n{}\n".format(a))
305
306# Indexing __getitem__.
307print("a[1, 2] = {}\n".format(a[1, 2]))
308
309# Slicing __getitem__.
310print("a[1:] = \n{}\n".format(a[1:]))
311
312# slice object.
313print("a[:, 0:3:2, :] = \n{}\n".format(a[:, 0:3:2, :]))
314
315# Combined __getitem__
316print("a[:-1, 0:3:2, 2] = \n{}\n".format(a[:-1, 0:3:2, 2]))
317
318print("\n")
319vals = np.array(range(24)).reshape((2, 3, 4))
320a = cv3c.Tensor(vals)
321
322# Changes get reflected.
323b = a[:-1, 0:3:2, 2]
324b[0] += 100
325print("b = {}\n".format(b))
326print("a = \n{}".format(a))
327
328print("\n")
329vals = np.array(range(24)).reshape((2, 3, 4))
330a = cv3c.Tensor(vals)
331
332# Example __setitem__
333a[:, :, 2] += 100
334print(a)
335
336# ## Advanced indexing
337#
338# Advanced indexing is triggered while passing an index array or a boolean array or their
339# combination with integer/slice object. Note that advanced indexing always returns a copy of the data
340# (contrast with basic slicing that returns a view).
341# ### Integer array indexing
342# Integer array indexing allows selection of arbitrary items in the tensor based on their
343# dimensional index. Indexes passed should be broadcast compatible.
344
345print("\n")
346vals = np.array(range(24)).reshape((2, 3, 4))
347a = cv3c.Tensor(vals)
348
349# Along each dimension, a specific element is selected.
350print("a[[0, 1], [1, 2], [1, 0]] = {}\n".format(a[[0, 1], [1, 2], [1, 0]]))
351
352# Changes not reflected as it is a copy.
353b = a[[0, 0], [0, 1], [1, 1]]
354b[0] += 100
355print("b = {}\n".format(b))
356print("a[[0, 0], [0, 1], [1, 1]] = {}".format(a[[0, 0], [0, 1], [1, 1]]))
357
358# ### Combining advanced and basic indexing
359# When there is at least one slice(```:```), ellipse(```...```), or newaxis in the index,
360# then the behaviour can be more complicated. It is like concatenating the indexing result for
361# each advanced index element. Under the advanced indexing mode, some preprocessing is done before
362# sending to the advanced indexing engine.
363# 1. Specific index positions are converted to a Indextensor with the specified index.
364# 2. If slice is non-full slice, then we slice the tensor first, then use full slice for advanced indexing engine.
365#
366# ```dst = src[1, 0:2, [1, 2]]``` is done in two steps:<br>
367# ```temp = src[:, 0:2, :]```<br>
368# ```dst = temp[[1], :, [1, 2]]```
369#
370# There are two parts to the indexing operation, the subspace defined by the basic indexing,
371# and the subspace from the advanced indexing part.
372#
373# 1. The advanced indexes are separated by a slice, Ellipse, or newaxis. For example ```x[arr1, :, arr2]```.
374# 2. The advanced indexes are all next to each other. For example ```x[..., arr1, arr2, :]```,
375# but not ```x[arr1, :, 1]``` since ```1``` is an advanced index here.
376#
377# In the first case, the dimensions resulting from the advanced indexing operation come first in the result array,
378# and the subspace dimensions after that. In the second case, the dimensions from the advanced indexing operations
379# are inserted into the result array at the same spot as they were in the initial array.
380
381print("\n")
382vals = np.array(range(24)).reshape((2, 3, 4))
383a = cv3c.Tensor(vals)
384
385print("a[1, 0:2, [1, 2]] = \n{}\n".format(a[1, 0:2, [1, 2]]))
386
387# Subtle difference in selection and advanced indexing.
388print("a[(0, 1)] = {}\n".format(a[(0, 1)]))
389print("a[[0, 1]] = \n{}\n".format(a[[0, 1]]))
390
391a = cv3c.Tensor(np.array(range(120)).reshape((2, 3, 4, 5)))
392
393# Interleaving slice and advanced indexing.
394print("a[1, [[1, 2], [2, 1]], 0:4:2, [3, 4]] = \n{}\n".format(
395    a[1, [[1, 2], [2, 1]], 0:4:2, [3, 4]]))
396
397# ### Boolean array indexing
398# Advanced indexing gets triggered when we pass a boolean array as an index, or it is returned
399# from comparision operators. Boolean array should have exactly as many dimensions as it is supposed to work with.
400
401print("\n")
402a = cv3c.Tensor(np.array([1, -1, -2, 3]))
403print("a = {}\n".format(a))
404
405# Add constant to all negative numbers.
406a[a < 0] += 20
407print("a = {}\n".format(a))
408
409# ## Logical operations
410#
411# CloudViewer supports following logical operators:
412# 1. `logical_and` - returns tensor with element wise logical AND.
413# 2. `logical_or`  - returns tensor with element wise logical OR.
414# 3. `logical_xor` - returns tensor with element wise logical XOR.
415# 4. `logical_not` - returns tensor with element wise logical NOT.
416# 5. `all`         - returns true if all elements in the tensor are true.
417# 6. `any`         - returns true if any element in the tensor is true.
418# 7. `allclose`    - returns true if two tensors are element wise equal within a tolerance.
419# 8. `isclose`     - returns tensor with element wise ```allclose``` operation.
420# 9. `issame`      - returns true if and only if two tensors are same(even same underlying memory).
421#
422
423print("\n")
424a = cv3c.Tensor(np.array([True, False, True, False]))
425b = cv3c.Tensor(np.array([True, True, False, False]))
426
427print("a AND b = {}".format(a.logical_and(b)))
428print("a OR b = {}".format(a.logical_or(b)))
429print("a XOR b = {}".format(a.logical_xor(b)))
430print("NOT a = {}\n".format(a.logical_not()))
431
432# Only works for boolean tensors.
433print("a.any = {}".format(a.any()))
434print("a.all = {}\n".format(a.all()))
435
436# If tensor is not boolean, 0 will be treated as False, while non-zero as true.
437# The tensor will be filled with 0 or 1 casted to tensor's dtype.
438c = cv3c.Tensor(np.array([2.0, 0.0, 3.5, 0.0]))
439d = cv3c.Tensor(np.array([0.0, 3.0, 1.5, 0.0]))
440print("c AND d = {}".format(c.logical_and(d)))
441
442a = cv3c.Tensor(np.array([1, 2, 3, 4]), dtype=cv3c.Dtype.Float64)
443b = cv3c.Tensor(np.array([1, 1.99999, 3, 4]))
444
445# Throws exception if the device/dtype is not same.
446# Returns false if the shape is not same.
447print("allclose : {}".format(a.allclose(b)))
448
449# Throws exception if the device/dtype/shape is not same.
450print("isclose : {}".format(a.isclose(b)))
451
452# Returns false if the device/dtype/shape/ is not same.
453print("issame : {}".format(a.issame(b)))
454
455# ## Comparision Operations
456
457print("\n")
458a = cv3c.Tensor([0, 1, -1])
459b = cv3c.Tensor([0, 0, 0])
460
461print("a > b = {}".format(a > b))
462print("a >= b = {}".format(a >= b))
463print("a < b = {}".format(a < b))
464print("a <= b = {}".format(a <= b))
465print("a == b = {}".format(a == b))
466print("a != b = {}".format(a != b))
467
468# Throws exception if device/dtype is not shape.
469# If shape is not same, then tensors should be broadcast compatible.
470print("a > b[0] = {}".format(a > b[0]))
471
472# ## Nonzero operations
473# 1. When ```as_tuple``` is ```False```(default), it returns a tensor indices of the elements that are non-zero.
474# Each row in the result contains the indices of a non-zero element in the input. If the input has $n$ dimensions,
475# then the resulting tensor is of size $(z x n)$,
476# where $z$ is the total number of non-zero elements in the input tensor.
477# 2. When ```as_tuple``` is ```True```, it returns a tuple of 1D tensors,
478# one for each dimension in input, each containing the indices of all non-zero elements of input.
479# If the input has $n$ dimension, then the resulting tuple contains $n$ tensors of size $z$,
480# where $z$ is the total number of non-zero elements in the input tensor.
481#
482
483print("\n")
484a = cv3c.Tensor([[3, 0, 0], [0, 4, 0], [5, 6, 0]])
485
486print("a = \n{}\n".format(a))
487print("a.nonzero() = \n{}\n".format(a.nonzero()))
488print("a.nonzero(as_tuple = 1) = \n{}".format(a.nonzero(as_tuple=1)))
489
490# ## TensorList
491# A tensorlist is a list of tensors of the same shape, similar to ```std::vector<Tensor>```.
492# Internally, a tensorlist stores the tensors in one big internal tensor,
493# where the begin dimension of the internal tensor is extendable.
494# This enables storing of 3D points, colours in a contiguous manner.
495
496print("\n")
497vals = np.array(range(24), dtype=np.float32).reshape((2, 3, 4))
498t = cv3c.Tensor(vals)
499
500# TensorList with single Tensor.
501b = cv3c.TensorList(t)
502print("b = {}".format(b))
503
504## Pickle support
505# Since CloudViewer v0.9.3, tensor can be serialized and deserialized using pickle. This is useful for saving and loading tensors to/from disk.
506
507a = cv3c.Tensor([1, 2, 3, 4])
508print(f'After serialization: {a}\n')
509with tempfile.TemporaryDirectory() as path:
510    file_name = os.path.join(path, 'tensor')
511    pickle.dump(a, open(file_name, 'wb'))
512    b = pickle.load(open(file_name, 'rb'))
513    print(f'After deserialization: {b}\n')
514
515# Pickle tensor on GPU.
516a = cv3c.Tensor([1, 2, 3, 4], device=cv3c.Device('cuda:0'))
517print(f'After serialization: {a}\n')
518with tempfile.TemporaryDirectory() as path:
519    file_name = os.path.join(path, 'tensor')
520    pickle.dump(a, open(file_name, 'wb'))
521    b = pickle.load(open(file_name, 'rb'))
522    print(f'After deserialization: {b}\n')
523
524# Pickle non-contiguous tensor.
525a = cv3c.Tensor.ones((100))
526a = a[::2]
527print(f'Contiguous: {a.is_contiguous()}\n')
528with tempfile.TemporaryDirectory() as path:
529    file_name = os.path.join(path, 'tensor')
530    pickle.dump(a, open(file_name, 'wb'))
531    b = pickle.load(open(file_name, 'rb'))
532    print(f'Contiguous: {b.is_contiguous()}\n')