Type system
Data types in Taichi consist of primitive types and compound types. Primitive types are the numerical data types used by different backends, while compound types are user-defined types of data records composed of multiple members.
Primitive types
Taichi supports common numerical data types as its primitive types. Each type is denoted as a
character indicating its category followed by a number indicating its precision bits. The
category can be either i
(for signed integers), u
(for unsigned integers), or f
(for floating-point numbers). The precision bits can be either 8
, 16
, 32
, or 64
,
which represents the number of bits for storing the data. For example, the two most commonly used types:
i32
represents a 32-bit signed integer;f32
represents a 32-bit floating-point number.
Supported primitive types on each backend
type | CPU | CUDA | OpenGL | Metal | Vulkan |
---|---|---|---|---|---|
i8 | ✔️ | ✔️ | ❌ | ✔️ | 🔶 |
i16 | ✔️ | ✔️ | ❌ | ✔️ | 🔶 |
i32 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
i64 | ✔️ | ✔️ | 🔶 | ❌ | 🔶 |
u8 | ✔️ | ✔️ | ❌ | ✔️ | 🔶 |
u16 | ✔️ | ✔️ | ❌ | ✔️ | 🔶 |
u32 | ✔️ | ✔️ | ❌ | ✔️ | ✔️ |
u64 | ✔️ | ✔️ | ❌ | ❌ | 🔶 |
f16 | ✔️ | ✔️ | ❌ | ❌ | ✔️ |
f32 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
f64 | ✔️ | ✔️ | ✔️ | ❌ | 🔶 |
(🔶 Requiring extensions of the backend)
Default types for integers and floating-point numbers
An integer literal, e.g., 42
, has default type ti.i32
, while a floating-point literal,
e.g., 3.14
, has default type ti.f32
. This behavior can be changed by explicitly specifying
default types when initializing Taichi:
ti.init(default_ip=ti.i64) # set default integer type to ti.i64
ti.init(default_fp=ti.f64) # set default floating-point type to ti.f64
In addition, you can use int
as an alias for the default integer type, and float
as an alias
for the default floating-point type:
ti.init(default_ip=ti.i64, default_fp=ti.f32)
x = ti.field(float, 5)
y = ti.field(int, 5)
# is equivalent to:
x = ti.field(ti.f32, 5)
y = ti.field(ti.i64, 5)
def func(a: float) -> int:
...
# is equivalent to:
def func(a: ti.f32) -> ti.i64:
...
Explicit type casting
Just like programming in other languages, you may encounter situations where you have a certain type of data, but it is not feasible for the assignment or calculation you want to perform. In this case, you can do explicit type casting. There are two kinds of explicit type casting in Taichi, namely normal casting and bit casting.
caution
In Taichi-scope, the type of a variable is static and determined on its initialization. That is, you can never change the type of a variable. The compiler relies on this compile-time information to check the validity of expressions in Taichi programs.
Normal casting
ti.cast()
is used for normal type casting as in other programming languages:
@ti.kernel
def foo():
a = 3.14
b = ti.cast(a, ti.i32) # 3
c = ti.cast(b, ti.f32) # 3.0
You can also use int()
and float()
to convert values to default integer and floating-point
types:
@ti.kernel
def foo():
a = 3.14
b = int(a) # 3
c = float(b) # 3.0
Bit casting
Use ti.bit_cast()
to cast a value into another type with its underlying bits preserved:
@ti.kernel
def foo():
a = 3.14
b = ti.bit_cast(a, ti.i32) # 1078523331
c = ti.bit_cast(b, ti.f32) # 3.14
Note that the new type must have the same precision bits as the old type (i32
->f64
is not
allowed). Use this operation with caution.
note
ti.bit_cast
is equivalent to reinterpret_cast
in C++.
Implicit type casting
When you accidentally use a value in a place where a different type is expected, implicit type casting is triggered for the following cases.
caution
Relying on implicit type casting is bad practice and one major source of bugs.
In binary operations
Following the implicit conversion rules of the C programming language, Taichi implicitly casts binary operation operands into a common type if they have different types. Some simple but most commonly used rules to determine the common type of two types are listed below:
i32 + f32 = f32
(int + float = float)i32 + i64 = i64
(low precision bits + high precision bits = high precision bits)
In assignments
When a value is assigned to a variable with a different type, the value is implicitly cast into that type. If the type of the variable differs from the common type of the variable and the value, a warning about losing precisions is raised.
In the following example, variable a
is initialized with type float
. On the next line, the
assignment casts 1
from int
to float
implicitly without any warning because the type of the
variable is the same as the common type float
:
@ti.kernel
def foo():
a = 3.14
a = 1
print(a) # 1.0
In the following example, variable a
is initialized with type int
. On the next line, the
assignment casts 3.14
from float
to int
implicitly with a warning because the type of the
variable differs from the common type float
:
@ti.kernel
def foo():
a = 1
a = 3.14
print(a) # 3
Compound types
User-defined compound types can be created using the ti.types
module. Supported compound types include vectors, matrices, and structs:
my_vec2i = ti.types.vector(2, ti.i32)
my_vec3f = ti.types.vector(3, float)
my_mat2f = ti.types.matrix(2, 2, float)
my_ray3f = ti.types.struct(ro=my_vec3f, rd=my_vec3f, l=ti.f32)
In this example, we define four compound types for creating fields and local variables.
Creating fields
Fields of a user-defined compound type can be created with the .field()
method of a Compound Type:
vec1 = my_vec2i.field(shape=(128, 128, 128))
mat2 = my_mat2f.field(shape=(24, 32))
ray3 = my_ray3f.field(shape=(1024, 768))
# is equivalent to:
vec1 = ti.Vector.field(2, dtype=ti.i32, shape=(128, 128, 128))
mat2 = ti.Matrix.field(2, 2, dtype=ti.i32, shape=(24, 32))
ray3 = ti.Struct.field({'ro': my_vec3f, 'rd': my_vec3f, 'l': ti.f32}, shape=(1024, 768))
In this example, we define three fields in two different ways but of exactly the same effect.
Creating local variables
Compound types can be directly called to create vector, matrix or struct instances. Vectors, matrices and structs can be created using GLSL-like broadcast syntax since their shapes are already known:
ray1 = my_ray3f(0.0) # ti.Struct(ro=[0.0, 0.0, 0.0], rd=[0.0, 0.0, 0.0], l=0.0)
vec1 = my_vec3f(0.0) # ti.Vector([0.0, 0.0, 0.0])
mat1 = my_mat2f(1.0) # ti.Matrix([[1.0, 1.0], [1.0, 1.0]])
vec2 = my_vec3f(my_vec2i(0), 1) # ti.Vector([0.0, 0.0, 1.0]), will perform implicit cast
ray2 = my_ray3f(ro=vec1, rd=vec2, l=1.0)
In this example, we define five local variables, each of a different type. In the definition statement of vec2
, my_vec3f()
performs an implicit cast operation when combining my_vec2i(0)
with 1
.
Type casting on vectors and matrices
Type casting on vectors/matrices is element-wise:
@ti.kernel
def foo():
u = ti.Vector([2.3, 4.7])
v = int(u) # ti.Vector([2, 4])
# If you are using ti.i32 as default_ip, this is equivalent to:
v = ti.cast(u, ti.i32) # ti.Vector([2, 4])