Skip to content

第 41 章:Python 接口

执行器件仿真

执行器件仿真的第一步是创建描述要仿真的器件的 Device 类实例。以下简单示例展示了如何完成此操作:

python
from sentaurus.device import *
device = Device(
    file = File(grid = "diode.tdr", plot = "diode_sim",
                current = "diode_sim"),
    electrodes = [
        Electrode(name="anode", voltage=1),
        Electrode(name="cathode", voltage=0)
    ],
    physics = Physics(
        mobility = Mobility(doping_dependence=DopingDependence(),
                            high_field_saturation=True),
        recombination = Recombination(srh=SRH(doping_dependence=True))
    )
)

器件通过指定通常添加到命令文件中的信息来定义,但以 Python 风格的方式进行。需要注意的是:

  • 在标准命令文件语法中,只有在创建混合模式仿真时才会创建 Device 部分。使用 Python 接口时,即使在单器件仿真中,也会始终创建 Device 类的实例。

  • 构建器件时,会创建对应于命令文件不同部分的不同 Python 对象,例如,对应于 File、Math 和 Physics 部分的 File、Math 和 Physics 对象。

  • 在本例中,只创建了一个 Physics 部分。多个部分通过在 Python 列表中创建多个 Physics 对象来创建,例如 physics = [Physics(...), Physics(...)]。Physics 部分稍后可通过 physics 属性以字典形式访问,其中使用 Physics 部分的 region 或 material 属性作为键。全局 Physics 部分的键为 "global"。除了列表之外,也可以传入字典 {"physics1": Physics(...)},在这种情况下,使用明确定义的键 "physics1" 来访问它们。

创建设备实例后,创建 SimulationSetup 类的实例:

python
setup = SimulationSetup(
    devices = device,
    global_file = GlobalFile(output="diode_sim"),
    global_math = GlobalMath(exit_on_failure=True)
)

此类描述了设置仿真所需的所有信息:

  • 在本例中,创建了单器件仿真,但通过创建多个 Device 实例并将其传入 Python 列表(如 devices=[device1, device2])来创建多器件仿真。

  • 可以创建 GlobalFile 和 GlobalMath 类的实例来定义这些部分的全局设置。在标准命令文件语法中,只有 File 和 Math 部分存在,而这些部分中的某些参数只能在全球级别和器件级别定义。这在本用户指南中有文档说明,但将 GlobalFile 和 GlobalMath 类与特定器件的 File 和 Math 类分开使这种区别更加清晰。

创建 SimulationSetup 实例后,可以创建 Simulation 类的实例来运行实际仿真,如下所示:

python
sim = Simulation(setup)
state = sim.solve(
    Coupled(equations=[Equation.Poisson], iterations=10),
    Coupled(equations=[Equation.Poisson, Equation.Electron,
                        Equation.Hole])),
    QuasiStationary(
        do_zero=True, initial_step=0.1, max_step=0.1, min_step=0.1,
        goal=Goal(name="anode", voltage=2),
        solve_cmd=Coupled(equations=[Equation.Poisson,
                                      Equation.Electron,
                                      Equation.Hole]))
)

在本例中,创建了 Simulation 对象并运行了简单的准稳态扫描(经过几次初始 Coupled 仿真之后)。在此仿真中,使用 Simulation 类的 solve 方法执行仿真。该方法具有以下特性:

  • 这相当于标准命令文件语法中的 Solve 部分,通常出现在 Solve 部分中的语句在 Python 接口中都有等效的类。

  • 调用 solve 方法时,可以传入回调函数,这些函数在仿真的各个阶段被调用。

  • 该函数返回一个 SimulationState 对象。这是一个基本上不透明的对象,可用于稍后在仿真中使用 Simulation 的 load 方法恢复仿真状态。

  • 如果发生收敛失败,此函数会报告 SolveError 异常。

访问仿真结果

除了仿真结果的正常文件输出外,仿真生成的结果也可以通过 Simulation 类的 result 属性从 Python 内部访问。

访问此属性返回一个包含所有 current 和 device plot 结果的 Pandas DataFrame。Pandas 是一个第三方 Python 包,是 Python 数据分析的行业标准。它是一个非常强大的包,提供了大量用于访问和操作数据的功能。

最简单的情形下,可以将 Pandas DataFrame 视为一个表格,其中行是仿真中的时间点,列是 current plot 数据集,如 "drain OuterVoltage"

下面的简单示例展示了一些非常基本的使用方法:

python
# 获取包含所有仿真结果的 Pandas DataFrame
res = sim.result
# 获取栅极电压和漏极电流的列
vg = res["gate OuterVoltage"]
id = res["drain TotalCurrent"]
# 获取栅极电压和漏极电流的最新值
v = vg.iloc[-1]
I = id.iloc[-1]

如果在仿真中使用了 NewCurrentPrefix 功能,前缀会作为单独的列添加到 DataFrame 中。在 Pandas 中,可以按如下方式过滤与给定 current 前缀对应的仿真时间数据:

python
# 过滤结果以获取包含与 current 前缀 "ramp1" 对应的所有行的 DataFrame
res = sim.result
res_ramp1 = res[res["prefix"] == "ramp1"]

除了 current plot 数据外,结果 DataFrame 还包含表示由 plot 语句创建的器件状态快照的对象。DeviceSnapshot 类封装了器件的 2D/3D 内部场数据,例如电子密度。快照对象出现在其被绘制的时间点的行中,从而可以轻松地将器件快照与 current plot 数据中的数据点关联起来。例如:

python
# 选取时间为 0.5 的行,然后读取栅极电压、漏极电流并获取对应的器件快照
res = sim.result
row = res[res["time"] == 0.5]
vg = row["gate OuterVoltage"].iloc[0]
id = row["drain TotalCurrent"].iloc[0]
sn = row["DeviceSnapshot"].iloc[0]

在 Solve 中使用时间步回调

Simulation 类的 solve 方法有一个名为 timestep_callback 的可选命名参数。它允许您注入一个用户定义的 Python 方法,该方法在 QuasiStationary 或 Transient 语句中的每个时间步之后被调用。回调函数可用于访问仿真状态,并可跳出当前的 QuasiStationary 或 Transient 语句以及执行任何其他 Python 代码。因此,timestep_callback 提供了标准 Sentaurus Device 仿真中不可用的功能,并允许复杂的仿真流程。

在回调函数中,您可以访问仿真数据,例如有关上一个 QuasiStationary 或 Transient 步长的信息、仿真统计信息(另请参阅第 231 页仿真统计的绘图和输出中的 SimStats),以及到该时间点之前的完整仿真结果。

回调函数的第一个参数是仿真的 Simulation 对象,第二个参数是 TimestepInfo 类的对象,它提供对 respective TransientTimestepInfo 或 QuasistatTimestepInfor 对象以及 SimStats 对象的访问。

TimestepInfo 对象具有 request_break() 方法,用于通知程序从当前的 QuasiStationary 或 Transient 语句返回,即使指定的目标尚未达到。这与 Math 部分中指定的静态 BreakCriteria 类似。可选的字符串参数允许您指定自定义消息,该消息会被打印到日志文件中作为结束当前 QuasiStationary 或 Transient 语句的原因。

注意

您可以同时在 Math 部分中指定静态 BreakCriteria,以及使用 solve() 方法的 timestep_callback 的动态中断条件。

您可以使用 help() 方法获取可通过 TimestepInfo 类访问的仿真状态的详细信息。例如,使用 help(sdevice.TransientTimestepInfo)

以下代码片段展示了如何在漏极的外电压超过某个值时从当前的 QuasiStationary 语句返回,以及如何发起器件 plot:

python
# 定义 break 条件的回调函数
def timestep_callback(sim, solve):
    print("timestep_callback:", solve.info.total_time,
          solve.info.step, solve.info.converged, flush=True)

# 从 current plot 管理器获取 plot 数据
    res = sim.result
    vd = res["drain OuterVoltage"].iloc[-1]
    if abs(vd) > 1.5:
        solve.request_break("timestep_callback: abs(drain OuterVoltage)
> 1.5")

setup = SimulationSetup(...)
sim = Simulation(setup)
sim.solve(QuasiStationary(...),
          timestep_callback=timestep_callback)

在 Solve 中使用插件步回调

timestep_callback 类似,Simulation 类的 solve() 方法有选项 solver_callback,它允许您插入一个用户定义的 Python 方法,该方法在每个插件迭代之后被调用。

在回调函数中,您可以访问时间步回调函数可访问的所有信息。此外,您还可以访问与插件迭代相关的信息,包括当前迭代次数、当前和先前迭代的方程误差,以及上一次迭代中的所有求解是否已收敛。

插件步回调函数的使用方式与时间步回调函数类似。在 request_break() 函数中有一个额外的选项 keep_solution,它控制后续的 solve() 调用是否从插件迭代计算出的解继续。默认情况下,keep_solution 为 false。

以下代码片段展示了如何实现混合求解方法,该方法从 20 次插件迭代开始,然后再进行 Newton 求解。如果插件求解没有出现异常结果,则流程转到 Newton 求解,从插件步的解开始。否则,流程在异常的插件迭代处停止,表明求解设置需要进一步调整:

python
from sentaurus.device import *

num_iters = 20
move_to_newton = False

# 插件回调函数
def pcb(sim, solve):
    if not solve.info.converged:
        solve.request_break("Stop plugin iterations. Some equations did
not converge.", keep_solution=False)
    elif solve.info.iteration > 0 and
        solve.info.error > solve.info.error_prev:
        solve.request_break("Stop plugin iterations due to error
increase." , keep_solution=False)
    elif solve.info.iteration == num_iters - 1 and
        solve.info.error >= 1:
        move_to_newton = True
        solve.request_break("Plugin iterations exhausted before system
converge.", keep_solution=True)

setup = SimulationSetup(...)
sim = Simulation(setup)
sim.solve(Plugin(iterations=niters, ...), solver_callback=pcb)
if move_to_newton:
    sim.solve(Coupled(...))

求解和加载仿真状态

Simulation 类的 solve 方法返回一个表示仿真当前状态的对象。此对象可以传递给 Simulation 类的 load 方法,以便稍后恢复状态:

python
...
sim = Simulation(setup)
# 执行 ramp #1 并保留状态以供稍后恢复
state_ramp1 = sim.solve(QuasiStationary(<ramp #1 的设置>))
# 现在执行第二个 ramp
sim.solve(QuasiStationary(<ramp #2 的设置>))
# 恢复 ramp #1 的仿真状态
sim.load(state_ramp1)
# 现在执行第三个 ramp
sim.solve(QuasiStationary(<ramp #3 的设置>))

SimulationState 类基本上是不透明的,底层数据保存在内存中,而不是存储在磁盘上。但是,各个器件的状态可作为内存中的 Tdr_Collection 访问,然后可以保存到 TDR 文件或传递到其他 TCAD Sentaurus™ Python 模块。

创建自定义数据集

Sentaurus Device 的 Python 接口提供了 DeviceSnapshot 类,它允许访问在仿真过程中构建的许多数据集,这些数据集可用于创建自定义数据集。本用户指南中其他地方的示例是电子电导率 σn = qnμn 的计算。在此示例中,每个顶点处的电子电导率是电子电荷 q、电子密度 n 和该顶点处电子迁移率 μn 的简单乘积。

以下示例演示了这一点:

python
from sentaurus.device import *
import numpy as np
...

sim = Simulation(setup)
sim.solve(QuasiStationary(...))
# 获取仿真的最终快照并构建电子电导率
snp = sim.result["DeviceSnapshot"].iloc[-1]
eDensity = snp.get_dataset(Dataset.eDensity)
eMobility = snp.get_dataset(Dataset.eMobility)
mesh = snp.mesh
idxs = mesh.get_vertices(material_group="Semiconductor")
eCond = np.zeros(len(eDensity))
eCond[idxs] = eDensity[idxs] * eMobility[idxs] * 1.602e-19
snp.set_user_field(Dataset.PMIUserField20, eCond)

在此示例中,DeviceSnapshot 类的 get_dataset 方法用于访问仿真产生的数据集,set_user_field 方法用于设置具有电子电导率计算结果的 PMIUserField20 数据集。

除此之外,DeviceSnapshot 类的 mesh 属性用于访问与器件半导体区域对应的顶点索引,电子电导率对这些区域有效。可以从 gpythonsh Python 解释器中输入 help(sentaurus.device.DeviceSnapshot) 来查看有关 DeviceSnapshot 类的更多文档。

NumPy 使用

Sentaurus Device Python 接口将数据集表示为 NumPy 数组,这有很多好处。NumPy Python 模块是一个第三方 Python 模块,已被广泛应用于许多数值领域,功能非常强大。

在前面计算电子电导率的示例中,利用了 NumPy 的两个特性。第一个是元素方式的数组运算可以简单地用标准运算符执行。例如:

python
>>> import numpy as np
>>> a = np.array([1, 2, 3])
>>> b = np.array([4, 5, 6])
>>> a + b
array([5, 7, 9])
>>>

执行元素运算的方式简单而高效,因为对数组的循环是在优化的 C 代码中执行的,而不是在 Python 中。第二个特性是高级索引,它仅对数组中的某些元素应用操作。例如:

python
>>> import numpy as np
>>> a = np.array([1, 2, 3, 4, 5, 6])
>>> b = np.array([1, 2, 3, 4, 5, 6])
>>> c = np.array([0, 0, 0, 0, 0, 0])
>>> idxs = [0, 3, 5]
>>> c[idxs] = a[idxs] + b[idxs]
>>> c
array([ 2, 0, 0, 8, 0, 12])
>>>

在此示例中,元素运算仅针对数组索引 0、3 和 5 执行。这对于对数据集中的特定网格位置执行操作非常有用。

这是对 NumPy 的简要介绍,但网上有大量文档和教程。NumPy 也在 TCAD Sentaurus 教程的 Python Language 模块中讨论(请参阅第 57 页的 TCAD Sentaurus 教程:仿真项目)。

提取参数

除了 Sentaurus Device Python 接口之外,通用 Python shell 还提供对 Extraction 模块的访问,该模块是 Sentaurus Visual Python 模块 svisualpylib 包的一部分。因此,您可以轻松分析由 Sentaurus Device 生成的 .plt 文件并提取电气参数。

以下示例展示了栅极扫描,然后提取各种电气参数,如阈值电压、最大跨导和亚阈值电压摆幅:

python
from sentaurus.device import *

# 运行仿真
setup = SimulationSetup(...)
sim = Simulation(setup)
sim.solve(QuasiStationary(...))

# 获取结果
res = sim.result
vgs = res["gate OuterVoltage"]
ids = res["drain Voltage"]

# 提取电气参数
import svisualpylib.extract as ext

lgate = 0.25 # 栅极长度,单位 um
io = 100e-9/lgate # [A/um]
vtgm = ext.extract_vtgm(vgs, ids, name="Vtgm")
vti = ext.extract_vti(vgs, ids, io, name='VtiLin')
ss = ext.extract_ss(vgs, ids, v_o=0.01, name="SS")
gm = ext.extract_gm(vgs, ids, name="gm")

要访问提取模块函数的文档:

  • 在 Sentaurus Workbench GUI 中,选择 Help > Python API Documentation > S-Visual。
  • 在 Sentaurus Visual GUI 中,选择 Help > Python API Reference。

或者,您可以在 gpythonsh 中使用 Python help 函数。例如:

python
import svisualpylib.extract as ext
help(ext.extract_vtgm)

局限性

Python 接口使用面向对象的方法来定义在命令文件中完成的仿真设置。目前,Python 接口中仅支持命令文件语法支持的命令和参数的子集。

要向任何命令添加任意命令字符串,您可以使用 set_generic_commandset_generic_option 方法。例如,要添加 CurrentPlot 部分:

python
sim = Simulation(…)
sim.devices[""].set_generic_commands("""
CurrentPlot{
SRH(Integrate(Semiconductor))
}
""")

除此之外,以下一般局限性也适用:

  • 目前不支持创建 Simulation 类的多个实例。

  • 如果执行瞬态仿真并多次调用 Simulation.solve() 方法,则必须指定正确初始时间,即上一次 Simulation.solve() 调用结束时的最终时间,因为 Sentaurus Device 不会保留上一次调用中的最后瞬态时间。

基于 Sentaurus TCAD 官方文档构建

代码块