跳转至主要内容
Version: develop

数据类型

Taichi is a statically typed programming language, meaning that the type of a variable in the Taichi scope is determined at compile time. This means that once a variable has been declared, it cannot be assigned a value of a different type.

我们来看一个简单的例子:

@ti.kernel
def test():
x = 1 # x is the integer 1
x = 3.14 # x is an integer, so the value 3.14 is cast to 3 and x takes the value 3
x = ti.Vector([1, 1]) # Error!
  • Line 3: x is an integer since it is assigned an integer value when it is declared for the first time.
  • Line 4: x is reassigned a floating-point number 3.14. However, the value of x is 3 instead of 3.14. This is because 3.14 is automatically cast to an integer 3 to match the type of x.
  • Line 5: The system throws an error, because ti.Vector cannot be cast into an integer.

The ti.types module in Taichi defines all of the supported data types. These data types are categorized into two groups: primitive and compound.

  • Primitive types encompass commonly utilized numerical data types, such as ti.i32 (int32), ti.u8 (uint8), and ti.f64 (float64).
  • Compound types, on the other hand, encompass array-like or struct-like data types, including ti.types.matrix, ti.types.ndarray, and ti.types.struct. These types are composed of multiple members, which can be primitive or other compound types.

基本类型

The primitive data types in Taichi are scalars, which are the smallest units that make up compound data types. These types are denoted with a letter indicating their category, followed by a number indicating their precision in bits. The category letter can be i for signed integers, u for unsigned integers, or f for floating-point numbers. The precision bits can be 8, 16, 32, or 64. Specially, precision bit 1 on unsigned number u1 is used to represent boolean values. The three most commonly used primitive types are:

  • i32:32 位有符号整数
  • f32 : 32-bit floating-point number
  • u1 : 1-bit unsigned integer, representing boolean values.

The support of Taichi's primitive types by various backends may vary. Consult the following table for detailed information, and note that some backends may require extensions for complete support of a specific primitive type.

后端i8i16i32i64u1u8u16u32u64f16f32f64
CPU✔️✔️✔️✔️✔️✔️✔️✔️✔️✔️✔️✔️
CUDA✔️✔️✔️✔️✔️✔️✔️✔️✔️✔️✔️✔️
OpenGL✔️✔️✔️✔️
Metal✔️✔️✔️✔️✔️✔️✔️✔️
Vulkan✔️✔️✔️✔️✔️

⭕:需要扩展。

自定义默认基本类型

初始化 Taichi 运行时,Taichi 会自动使用以下数据类型作为默认基本数据类型:

  • ti.i32:默认整数类型。
  • ti.f32:默认浮点类型。

Taichi 允许你在调用 init() 时指定默认的基本数据类型:

ti.init(default_ip=ti.i64)  # Sets the default integer type to ti.i64
ti.init(default_fp=ti.f64) # Sets the default floating-point type to ti.f64
note

The numeric literals in Taichi's scope have default integer or floating-point types. For example, if the default floating-point type is ti.f32, a numeric literal 3.14159265358979 will be cast to a 32-bit floating-point number with a precision of approximately seven decimal digits. To ensure high precision in applications such as engineering simulations, it is recommended to set default_fp to ti.f64.

Data type Aliases

In Taichi scope, the two names int and float serve as aliases for the default integer and floating-point types, respectively. These default types can be changed using the configuration option default_ip and default_fp. For instance, setting the default_ip to i64 and default_fp to f64 would allow you to use int as an alias for i64 and float as an alias for f64 in your code.

ti.init(default_ip=ti.i64, default_fp=ti.f64)

@ti.kernel
def example_cast() -> int: # the returned type is ti.i64
x = 3.14 # x is of ti.f64 type
y = int(x) # equivalent to ti.i64(x)
return y

Furthermore, in the Python scope, when declaring Taichi's data containers using ti.field, ti.Vector, ti.Matrix, ti.ndarray, these two names also serve as aliases for the default integer and floating-point types. 例如:

x = ti.field(float, 5)
# Is equivalent to:
x = ti.field(ti.f64, 5)

However, when using int and float outside of Taichi's data containers in regular Python code, they refer to their standard meaning as built-in functions and not aliases for Taichi's default_ip and default_fp. Therefore, in Python scope and outside of Taichi's data containers, int and float have their standard meaning as built-in functions.

x = np.array([1, 2, 3, 4], dtype=int)  # NumPy's int64 type
y = int(3.14) # Python's built-in int type

显式类型转换

As mentioned at the beginning of this document, the type of a variable in the Taichi scope is determined at compile time, meaning that it is statically typed. The Taichi compiler performs type checking at compile time and therefore, once a variable is declared, you cannot assign to it a value of a different type. However, in certain situations, you may need to switch to a different data type due to the unavailability of the original type for an assignment or calculation. In these cases, you must perform an explicit type casting.

  • The ti.cast() function allows you to convert a given value to a specific target type. For instance, you can use ti.cast(x, float) to transform a variable x into a floating-point type.

    @ti.kernel
    def foo():
    a = 3.14
    b = ti.cast(a, ti.i32) # 3
    c = ti.cast(b, ti.f32) # 3.0

As of Taichi v1.1.0, the capability to perform type casting on scalar variables has been introduced using primitive types such as ti.f32 and ti.i64. This allows you to convert scalar variables to different scalar types with ease.

@ti.kernel
def foo():
a = 3.14
x = int(a) # 3
y = float(a) # 3.14
z = ti.i32(a) # 3
w = ti.f64(a) # 3.14

隐式类型转换

Implicit type casting occurs when a value is placed or assigned where a different data type is expected.

WARNING

As a general principle, implicit type casting can be a significant source of bugs. As such, Taichi strongly discourages the use of this mechanism and recommends that you explicitly specify the desired data types for all variables and operations.

二元运算中的隐式类型转换。

In Taichi, implicit type casting can occur during binary operations or assignments. The casting rules are implemented specifically for Taichi and are slightly different from those for the C programming language. These rules are prioritized as follows:

  1. 整数 + 浮点数 -> 浮点数

    • i32 + f32 -> f32
    • i16 + f16 -> f16
  2. 低精度位 + 高精度位 -> 高精度位

    • i16 + i32 -> i32
    • f16 + f32 -> f32
    • u8 + u16 -> u16
  3. 带符号整数 + 无符号整数 -> 无符号整数

    • u32 + i32 -> u32
    • u8 + i8 -> u8

出现规则冲突时,最高优先级的规则适用:

  • u8 + i16 -> i16(当第二条规则与第三条规则冲突时,规则二适用。)
  • f16 + i32 -> f16(当第一条规则与第二条规则冲突时,规则一适用。)

几个例外:

  • 位移运算返回 lhs(左侧)数据类型:
    • u8 << i32 -> u8
    • i16 << i8 -> i16
  • 逻辑运算返回 i32
  • 比较运算返回 i32

赋值时的隐式类型转换

In Taichi, implicit type casting is performed when assigning a value to a variable with a different data type. In cases where the value has a higher precision than the target variable, a warning indicating potential precision loss will be displayed.

  • Example 1: The variable a is initialized with the data type float, and then immediately reassigned the value 1. This reassignment implicitly converts the data type of 1 from int to float without generating a warning.

    @ti.kernel
    def foo():
    a = 3.14
    a = 1
    print(a) # 1.0
  • Example 2: The variable a is initialized with the data type int, and is immediately reassigned the value 3.14. This reassignment implicitly converts the data type of 3.14 from float to int, which results in a loss of precision due to int having a lower precision than float. As a result, a warning is generated.

    @ti.kernel
    def foo():
    a = 1
    a = 3.14
    print(a) # 3

复合类型

复合类型是用户自定义的数据类型,由多个元素组成。 支持的复合类型包括向量、矩阵、ndarray 和结构体:

Taichi 允许你将 ti.types 模块中提供的所有类型作为脚手架来自定义更高等级的复合类型。

note

ndarray 类型在另一篇文档 与外部数组交互 中讨论。

矩阵和向量

你可以使用两个函数 ti.types.matrix()ti.types.vector() 来创建自己的矩阵和向量类型:

vec4d = ti.types.vector(4, ti.f64)  # a 64-bit floating-point 4D vector type
mat4x3i = ti.types.matrix(4, 3, int) # a 4x3 integer matrix type

You can utilize the customized compound types to instantiate vectors and matrices, as well as annotate the data types of function arguments and struct members. For instance:

v = vec4d(1, 2, 3, 4)  # Create a vector instance, here v = [1.0 2.0 3.0 4.0]

@ti.func
def length(w: vec4d): # vec4d as type hint
return w.norm()

@ti.kernel
def test():
print(length(v))

Note that in Taichi, there is no distinction between row vector and column vector. This implies that a vector is handled as a column vector when multiplying a matrix by a vector. In contrast, a vector is considered as a row vector when multiplied by a matrix. This is demonstrated in the following example.

mat = ti.types.matrix(n=3, m=3, dtype=ti.i32)([[1, 1, 1], [0, 0, 0], [0, 0, 0]])
vec = ti.types.vector(n=3, dtype=ti.i32)([1, 1, 1])

print(mat @ vec) # [3 0 0]
print(vec @ mat) # [1 1 1]

结构体类型和数据类(dataclass)

You can use the function ti.types.struct() to create a struct type, which can be utilized to represent a sphere in 3D space, abstracted by its center and radius. To achieve this, you can call ti.types.vector() and ti.types.struct() to create two higher-level compound types: vec3 and sphere_type, respectively. These types can then be used as templates to initialize two local variables, sphere1 and sphere2, to represent two instances of spheres.

# Define a compound type vec3 to represent a sphere's center
vec3 = ti.types.vector(3, float)
# Define a compound type sphere_type to represent a sphere
sphere_type = ti.types.struct(center=vec3, radius=float)
# Initialize sphere1, whose center is at [0,0,0] and whose radius is 1.0
sphere1 = sphere_type(center=vec3([0, 0, 0]), radius=1.0)
# Initialize sphere2, whose center is at [1,1,1] and whose radius is 1.0
sphere2 = sphere_type(center=vec3([1, 1, 1]), radius=1.0)

When defining a struct with numerous members, the use of ti.types.struct can lead to cluttered and unorganized code. Taichi provides a more elegant solution with the @ti.dataclass decorator, which acts as a lightweight wrapper around the struct type.

@ti.dataclass
class Sphere:
center: vec3
radius: float

The code above accomplishes the same task as the following line, however it offers improved comprehensibility:

Sphere = ti.types.struct(center=vec3, radius=float)

Another benefit of utilizing the @ti.dataclass over the ti.types.struct is the ability to define member functions within a dataclass, enabling object-oriented programming (OOP) capabilities. For more information on the topic of objective data-oriented programming, refer to the objective data-oriented programming documentation.

初始化

In Taichi, creating instances of vector, matrix, or struct compound types can be achieved by directly calling the type, similar to how it is done with any other data type.

As of Taichi v1.1.0, multiple options are available for initializing instances of structs or dataclasses. The conventional method of calling a compound type directly still holds true. In addition, the following alternatives are also supported:

  • Pass positional arguments to the struct, in the order in which the members are defined.
  • Utilize keyword arguments to set the specific struct members.
  • Members that are not specified will be automatically set to zero.

例如:

vec3 = ti.types.vector(3, float)

@ti.dataclass
class Ray:
ro: vec3
rd: vec3
t: float

# The definition above is equivalent to
#Ray = ti.types.struct(ro=vec3, rd=vec3, t=float)
# Use positional arguments to set struct members in order
ray = Ray(vec3(0), vec3(1, 0, 0), 1.0)
# ro is set to vec3(0) and t will be set to 0
ray = Ray(vec3(0), rd=vec3(1, 0, 0))
# both ro and rd are set to vec3(0)
ray = Ray(t=1.0)
# ro is set to vec3(1), rd=vec3(0) and t=0.0
ray = Ray(1)
# All members are set to 0
ray = Ray()
note

你可以用 GLSL 式的广播(broadcast)语法来创建向量、矩阵和结构体,因为它们的形状是已知的。

类型转换

For now, the only compound data types that support type casting in Taichi are vectors and matrices. When casting the type of a vector or matrix, it is performed element-wise, resulting in the creation of new vectors and matrices.

@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])

Argument Pack Type

Argument packs, also known as argpacks, are user-defined data types that act as wrappers for multiple parameters. The argpack type is discussed in Argument Pack in detail.