数据类型
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 ofx
is 3 instead of 3.14. This is because 3.14 is automatically cast to an integer 3 to match the type ofx
. - 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
), andti.f64
(float64
). - Compound types, on the other hand, encompass array-like or struct-like data types, including
ti.types.matrix
,ti.types.ndarray
, andti.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 numberu1
: 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.
后端 | i8 | i16 | i32 | i64 | u1 | u8 | u16 | u32 | u64 | f16 | f32 | f64 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
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 useti.cast(x, float)
to transform a variablex
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:
整数 + 浮点数 -> 浮点数
i32 + f32 -> f32
i16 + f16 -> f16
低精度位 + 高精度位 -> 高精度位
i16 + i32 -> i32
f16 + f32 -> f32
u8 + u16 -> u16
带符号整数 + 无符号整数 -> 无符号整数
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 typefloat
, and then immediately reassigned the value 1. This reassignment implicitly converts the data type of 1 fromint
tofloat
without generating a warning.@ti.kernel
def foo():
a = 3.14
a = 1
print(a) # 1.0Example 2: The variable
a
is initialized with the data typeint
, and is immediately reassigned the value 3.14. This reassignment implicitly converts the data type of 3.14 fromfloat
toint
, which results in a loss of precision due toint
having a lower precision thanfloat
. 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.