跳到主要内容Python 驱动的 ADS 自动化仿真框架与 API 实战指南 | 极客日志Python算法
Python 驱动的 ADS 自动化仿真框架与 API 实战指南
本文介绍了一个基于 Python 的 ADS 自动化仿真框架,封装了环境配置、参数更新、批量运行及结果提取的全流程。通过模板方法模式分离电路逻辑与仿真控制,支持多种输入格式(JSON/CSV/Excel),并内置复数处理与事务管理。适合需要高频迭代射频电路设计的工程师,可显著提升仿真效率并减少人工操作误差。
黑客帝国2 浏览 核心架构设计
在长期进行射频电路迭代的过程中,我意识到重复编写 ADS 脚本不仅耗时,还容易引入人为错误。为此,我构建了一个通用的自动化工具库 auto_simulator.py,封装了从环境配置、参数更新、仿真运行到结果提取的全流程。用户只需关注'如何将参数应用到电路'这一核心逻辑,其余繁琐的交互细节由框架处理。
该工具采用 模板方法模式 (Template Method Pattern) 设计:
- 不变的部分:ADS 工作空间的打开/关闭、仿真器的实例化、结果文件的遍历与读取、异常处理、数据保存(追加到原始 JSON)。这些通用逻辑被封装在基类和工具函数中。
- 变化的部分:不同的电路有不同的参数需要更新(如电阻值、信号文件路径、偏置电压)。这部分逻辑通过
ParameterUpdater 接口暴露给用户实现。
核心类说明
utils.auto_simulator.py 中主要包含两个核心类:ParameterUpdater 和 AutoSimulator。
ParameterUpdater (抽象基类)
这是一个策略接口,将'如何更新电路参数'这一变化点从框架中剥离出来。由于每个 ADS 设计的元件名称、变量控件结构、参数命名规则都不同,这部分逻辑无法通用化,必须由用户根据自己的电路原理图来实现。
from abc import ABC, abstractmethod
from keysight.ads.de import db_uu as db
import pandas as pd
class ParameterUpdater(ABC):
"""参数更新抽象接口"""
@abstractmethod
def update_parameters(self, design: db.Design, param_row: pd.Series) -> None:
"""
更新电路参数(用户必须实现)
Args:
design: ADS 设计对象,指向当前打开的原理图
param_row: 包含本次仿真所需全部参数的 Pandas Series
"""
pass
核心方法详解:
| 方法签名 | update_parameters(self, design: db.Design, param_row: pd.Series) -> None |
|---|
| 调用时机 | AutoSimulator 在处理每一行参数样本时,会在执行仿真前调用此方法 |
| design 参数 | 由 setup_ads_environment 打开的设计对象,类型为 keysight.ads.de.db_uu.Design。通过它可以访问原理图中的所有元件实例 |
| param_row 参数 | Pandas Series 对象,键为参数名(如 "R1", "Vbias"),值为参数数值。这些键名对应输入 JSON/CSV 文件中的列名 |
| 返回值 | 无(None)。参数更新通过直接修改 design 对象完成 |
from keysight.ads.de.db import Transaction
with Transaction(design) as transaction:
inst1.vars.update({...})
inst2.parameters["C"].value = "10 pF"
transaction.commit()
inst.update_item_annotation()
更新元件参数(用于电阻、电容、信号源等具体元件):
inst.parameters["参数名"].value = "值(字符串,可带单位)"
更新 VAR 控件中的变量(用于 VAR / VAR2 等变量定义控件):
inst.vars.update({"变量名 1": "值(字符串)", "变量名 2": "值(字符串)",})
inst = design.get_instance("VAR1")
- 所有参数值必须转换为字符串格式,ADS 内部会自行解析单位
- 文件路径类参数(如 DAC1 的 File 参数)需要用双引号包裹,即
"'path'"
- 若更新多个元件,建议包裹在
Transaction 中,确保要么全部成功、要么全部回滚
- 更新后调用
update_item_annotation() 可让原理图界面同步显示新值(非必需但推荐)
AutoSimulator (核心控制器)
这是自动化仿真的主引擎(Driver)。它不关心电路的具体细节,只负责流程控制:加载数据 -> 设置环境 -> 循环仿真 -> 提取结果 -> 保存文件。
__init__(self, parameter_updater: ParameterUpdater)
构造函数,负责将用户实现的参数更新器'注册'到控制器中。
| 参数 | 类型 | 说明 |
|---|
parameter_updater | ParameterUpdater | 用户自定义的参数更新器实例,必须实现 update_parameters 方法 |
内部行为:将 parameter_updater 保存为成员变量,同时初始化 self.design = None(后续由 setup_ads_environment 填充)。
load_parameter_samples(self, filepath: str) -> pd.DataFrame
从文件加载参数样本数据,统一转换为 Pandas DataFrame 供后续遍历使用。
| 参数 | 类型 | 说明 |
|---|
filepath | str | 参数数据文件的完整路径 |
| 扩展名 | 处理逻辑 |
|---|
.json | 使用 json.load 读取。支持三种结构:① 纯列表 [{}, {}];② 字典包裹 {"samples": [...]};③ 单条记录 {}(自动包装为单行 DataFrame) |
.csv | 调用 pd.read_csv(filepath) |
.xlsx / .xls | 调用 pd.read_excel(filepath) |
FileNotFoundError:文件路径不存在
ValueError:JSON 内容既非 dict 也非 list,或扩展名不在上述列表中
注意:虽然加载支持多种格式,但目前结果保存仅实现了 JSON 格式。如需将结果回写到 CSV/Excel,用户可继承 AutoSimulator 并重写保存逻辑。
setup_ads_environment(self, workspace_path: str, library_name: str, design_name: str) -> None
初始化 ADS 工作环境,完成工作空间 -> 库 -> 设计的三级打开流程。
| 参数 | 类型 | 说明 |
|---|
workspace_path | str | ADS 工作空间目录路径,如 E:/ADS2026_wrk/MyProject_wrk |
library_name | str | 库名称,如 MyAmplifier_lib |
design_name | str | 原理图设计名称(不含扩展名),如 Amp_Sim_Schematic |
utils.get_workspace(workspace_path) —— 打开/连接工作空间
utils.get_library(workspace, library_name) —— 加载指定库
utils.get_design(library, design_name, ViewType.SCHEMATIC) —— 打开原理图视图
成功后 self.design 将指向该设计对象,后续的参数更新和仿真都基于此对象进行。
异常情况:路径错误、库/设计不存在时抛出 RuntimeError。
从仿真结果数据集中智能提取用户指定的目标变量,是本工具库最核心的数据处理函数。
| 参数 | 类型 | 说明 |
|---|
results | dataset.Dataset | ADS 仿真结果对象(由 SimulationManager.get_simulation_results() 获取) |
target_variables | List[str] | 想要提取的变量名列表,如 ["S[2,1]", "Gain_dB", "OUT"] |
debug | bool | 是否打印调试信息,默认 True |
- 遍历
results.varblock_names 获取所有 VariableBlock
- 对每个块,通过
varblock.ivars / varblock.dvars 获取其独立变量和依赖变量名列表
- 若目标变量出现在
dvars 中,调用 varblock.to_dataframe(dvar_names=[target_var]) 精准提取该列
- 根据数据长度自动判断类型:
- 长度 = 1 → 标量,转为 Python
float
- 长度 > 1 → 向量,转为 Python
list
返回值:Dict[str, Any],格式为 {变量名:值或列表}。未找到的变量对应值为 None。
run_simulation_and_extract_results(self, target_variables: List[str], debug: bool = True) -> Dict[str, Any]
执行单次仿真并提取结果的快捷方法,内部封装了仿真管理器的创建与结果获取。
| 参数 | 类型 | 说明 |
|---|
target_variables | List[str] | 想要提取的变量名列表 |
debug | bool | 是否打印调试信息 |
- 创建
SimulationManager(self.design)
- 调用
sim_manager.run_design_simulation() 执行仿真
- 使用上下文管理器
with sim_manager.get_simulation_results() as results 获取结果
- 调用
extract_simulation_results(results, target_variables, debug) 提取数据
异常处理:若仿真过程出错(如不收敛、License 问题),捕获异常并返回 {var: None for var in target_variables},不会中断程序。
run_batch_simulation(...)
批量仿真的主入口,串联整个自动化流程。大多数情况下用户只需调用此方法。
| 参数 | 类型 | 默认值 | 说明 |
|---|
data_filepath | str | — | 参数数据文件路径 |
workspace_path | str | — | ADS 工作空间路径 |
library_name | str | — | 库名称 |
design_name | str | — | 设计名称 |
target_variables | List[str] | — | 要提取的仿真结果变量列表 |
goal_variables | List[str] | None | 写入文件时的字段名集合,若为 None 则等于 target_variables |
result_processor | Callable | None | 可选的结果后处理回调函数,签名为 (Dict) -> Dict |
max_samples | int | None | 限制处理的样本数量,用于调试;None 表示处理全部 |
save_to_original | bool | True | 是否将结果合并回写到原始数据文件 |
- 加载数据:调用
load_parameter_samples(data_filepath) 读取参数表
- 限制样本:若设置了
max_samples,取前 N 行
- 初始化环境:调用
setup_ads_environment(...) 打开 ADS 设计
- 循环处理每个样本:
- 调用
parameter_updater.update_parameters(design, row) 更新电路参数
- 删除旧仿真数据:移除
{cell_path}/data/{design_name}.ds,防止读到历史缓存
- 调用
run_simulation_and_extract_results(target_variables) 执行仿真并提取结果
- 若提供了
result_processor,对结果进行后处理
- 将结果追加到列表
- 保存结果:若
save_to_original=True,调用 _save_results_to_original_file 合并写回
- 打印统计:调用
_print_simulation_statistics 输出成功/失败计数与数值范围
返回值:带有 simulation_results 列的 DataFrame。
_save_results_to_original_file(...)
将当前批次的仿真结果与原文件中已有的 simulation_results 字段合并后写回,支持增量覆盖(新结果覆盖同名旧结果,旧结果中不重名的字段保留)。
- 遍历
simulation_results 列表,对每条记录中的 goal_variables 调用 _process_value_for_json 转换
- 遍历原 DataFrame,取出每行的
simulation_results(若不存在则初始化为空字典)
- 使用
dict.update() 将新结果合并进去
- 最终以
json.dump(df.to_dict(orient="records"), ...) 写回文件
限制:目前仅支持 JSON 格式输出。如需 CSV/Excel 输出,可重写此方法或在调用后手动导出。
_process_value_for_json(self, value) -> Any
递归处理各种数据类型,确保最终结果可被 json.dump 序列化。
| 输入类型 | 输出格式 |
|---|
complex | {"real": float, "imag": float, "magnitude": float, "phase_deg": float} |
list / np.ndarray | 递归处理每个元素;若长度为 1 则解包为标量 |
np.number | 转为 Python float(复数走 complex 分支) |
float 且为 NaN/Inf | 转为 None |
| 其他 | 原样返回 |
_print_simulation_statistics(self, df, target_variables) -> None
在批量仿真结束后打印统计摘要,便于快速评估整体质量。
- 总样本数 / 成功数 / 失败数
- 对于每个
target_variable,若为数值型则显示最小值、最大值、均值
实战示例:开发一个自定义仿真器
本节以一个射频放大器增益仿真为例,完整演示从数据准备到结果提取的全流程。
1.3.1 输入数据格式
首先准备参数样本文件(JSON 格式),每条记录代表一组待仿真的电路参数:
[{"sample_id":1,"Vbias":3.3,"R_source":50,"C_coupling":100,"L_match":2.2},{"sample_id":2,"Vbias":3.0,"R_source":75,"C_coupling":150,"L_match":1.8}]
sample_id:样本编号,用于日志输出和结果追踪(可选,若不提供则自动使用行索引)
- 其余字段:与原理图中需要更新的变量/参数一一对应
1.3.2 实现参数更新器
根据你的 ADS 原理图结构,继承 ParameterUpdater 并实现 update_parameters 方法:
""" amplifier_simulator.py 射频放大器批量仿真脚本 """
import sys
from pathlib import Path
project_root = Path(__file__).parent.parent
sys.path.append(str(project_root))
import pandas as pd
from utils.auto_simulator import AutoSimulator, ParameterUpdater
from keysight.ads.de import db_uu as db
from keysight.ads.de.db import Transaction
class AmplifierUpdater(ParameterUpdater):
"""
放大器参数更新器
假设原理图中包含:
- VAR1 控件:定义 Vbias(偏置电压)、R_source(源阻抗)
- C1 元件:耦合电容
- L1 元件:匹配电感
"""
def update_parameters(self, design: db.Design, param_row: pd.Series) -> None:
"""
将 param_row 中的参数值写入 ADS 原理图
Args:
design: ADS 设计对象
param_row: 当前样本的参数(Pandas Series)
"""
with Transaction(design) as transaction:
inst_var = design.get_instance("VAR1")
inst_var.vars.update({
"Vbias": str(param_row["Vbias"]),
"R_source": str(param_row["R_source"]),
})
inst_var.update_item_annotation()
inst_c1 = design.get_instance("C1")
inst_c1.parameters["C"].value = f"{param_row['C_coupling']} pF"
inst_c1.update_item_annotation()
inst_l1 = design.get_instance("L1")
inst_l1.parameters["L"].value = f"{param_row['L_match']} nH"
inst_l1.update_item_annotation()
transaction.commit()
- Transaction 上下文管理器:将所有修改包裹在事务中,调用
commit() 后才真正生效
- VAR 控件 vs 元件参数:VAR 控件用
.vars.update(),普通元件用 .parameters["名称"].value
- 单位处理:电容用
pF、电感用 nH,ADS 会自动解析
- 字符串转换:所有值必须是字符串,使用
str() 或 f-string
1.3.3 编写主控制脚本
def main():
"""主函数:配置参数并启动批量仿真"""
config = {
"data_filepath": "E:/project/data/amplifier_samples.json",
"workspace_path": "E:/ADS2026_wrk/MyAmplifier_wrk",
"library_name": "MyAmplifier_lib",
"design_name": "Amp_Gain_Schematic",
"target_variables": [
"S[2,1]",
"S[1,1]",
"Gain_dB",
"NF_dB",
],
"max_samples": None,
}
print("=" * 60)
print("射频放大器批量仿真")
print("=" * 60)
print(f"参数文件:{config['data_filepath']}")
print(f"目标变量:{config['target_variables']}")
print()
try:
updater = AmplifierUpdater()
simulator = AutoSimulator(updater)
final_df = simulator.run_batch_simulation(
data_filepath=config["data_filepath"],
workspace_path=config["workspace_path"],
library_name=config["library_name"],
design_name=config["design_name"],
target_variables=config["target_variables"],
max_samples=config["max_samples"],
save_to_original=True,
)
print()
print("=" * 60)
print(f"仿真完成!共处理 {len(final_df)} 个样本")
print(f"结果已保存到:{config['data_filepath']}")
print("=" * 60)
except Exception as e:
print(f"\n❌ 仿真过程发生错误:{e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()
1.3.4 输出结果格式
仿真完成后,原始 JSON 文件会被追加 simulation_results 字段:
[
{
"sample_id": 1,
"Vbias": 3.3,
"R_source": 50,
"C_coupling": 100,
"L_match": 2.2,
"simulation_results": {
"S[2,1]": {"real": 2.51, "imag": 1.33, "magnitude": 2.84, "phase_deg": 27.9},
"S[1,1]": {"real": -0.12, "imag": 0.05, "magnitude": 0.13, "phase_deg": 157.4},
"Gain_dB": 9.07,
"NF_dB": 2.3
}
},
{
"sample_id": 2,
"Vbias": 3.0,
"R_source": 75,
"C_coupling": 150,
"L_match": 1.8,
"simulation_results": {
"S[2,1]": {...},
"S[1,1]": {...},
"Gain_dB": 8.45,
"NF_dB": null
}
}
]
- 复数类型(如 S 参数):自动拆分为
real、imag、magnitude、phase_deg
- 标量类型(如
Gain_dB):直接存储数值
- 提取失败:对应字段值为
null
1.3.5 进阶:自定义结果后处理
若需要对原始提取结果进行计算(如将复数 S21 转为 dB),可传入 result_processor 回调:
import numpy as np
def process_results(raw_results: dict) -> dict:
"""
自定义结果后处理函数
Args:
raw_results: extract_simulation_results 返回的原始字典
Returns:
处理后的结果字典
"""
processed = raw_results.copy()
s21 = raw_results.get("S[2,1]")
if s21 is not None and isinstance(s21, complex):
processed["S21_dB"] = 20 * np.log10(abs(s21))
return processed
final_df = simulator.run_batch_simulation(..., result_processor=process_results)
为什么推荐使用此模式?
- 稳健性:内部统一捕获仿真异常,单个样本失败不会拖垮全流程。
- 断点续传:结果实时写回原始文件,结合简单的跳过逻辑即可实现增量仿真。
- 结果对齐:自动将输入参数与输出结果一一对应,便于后续数据分析与模型训练。
辅助工具库 (utils)
先前在自动化工具库中也用到了一些二次封装 ADS API 的类和方法,这里一并给出供参考。
import os
import keysight.ads.dataset as dataset
from keysight.ads import de
from keysight.ads.de import db_uu as db
from enum import Enum
from typing import Literal, Union
from keysight.edatoolbox import ads
from keysight.ads.de.experimental.text_maker import TextMaker
class ViewType(Enum):
SCHEMATIC = "schematic"
SYMBOL = "symbol"
LAYOUT = "layout"
def __str__(self) -> str:
return self.value
ViewNameLiteral = Literal["schematic", "symbol", "layout"]
ViewNameType = Union[ViewType, ViewNameLiteral]
def design_exists(library: de.Library, name: str, view_type: ViewNameType = ViewType.SCHEMATIC) -> bool:
design_ref = f"{library.name}:{name}:{view_type.value}"
return de.design_exists(design_ref)
def get_workspace(workspace_path: str) -> de.Workspace:
if de.workspace_is_open():
de.close_workspace()
if os.path.exists(workspace_path):
print(f"Workspace directory already exists: {workspace_path}")
de.open_workspace(workspace_path)
return de.active_workspace()
workspace = de.create_workspace(workspace_path)
print(f"Workspace created: {workspace_path}")
workspace.open()
return workspace
def get_library(workspace: de.Workspace, name: str) -> de.Library:
assert workspace.is_open
library_name = name
library_path = workspace.path / library_name
if os.path.exists(library_path):
if de.library_is_open(library_name):
de.close_library(library_name)
print(f"Library directory already exists: {library_path}")
return workspace.open_library(library_name, library_path, de.LibraryMode.SHARED)
de.create_new_library(library_name, library_path)
print(f"Library created: {library_path}")
workspace.add_library(library_name, library_path, de.LibraryMode.SHARED)
lib = workspace.open_library(library_name, library_path, de.LibraryMode.SHARED)
return lib
def get_design(
library: de.Library,
name: str,
view_type: ViewNameType = ViewType.SCHEMATIC,
mode: db.DesignMode = db.DesignMode.APPEND,
) -> db.Design:
"""创建设计,如果已存在则打开现有设计"""
design_ref = f"{library.name}:{name}:{view_type.value}"
if design_exists(library, name, view_type):
return db.open_design(design_ref, mode=mode)
else:
if view_type == ViewType.SCHEMATIC:
design = db.create_schematic(design_ref)
elif view_type == ViewType.SYMBOL:
design = db.create_symbol(design_ref)
elif view_type == ViewType.LAYOUT:
design = db.create_layout(design_ref)
else:
raise ValueError(f"Invalid view type: {view_type}")
print(f"Design created: {design_ref}")
return design
def add_measeqn(
design: db.Design,
eq_name: str,
eq_list: list[str],
origin: tuple[float, float] = (0, 0),
angle: float = 0,
) -> None:
measeqn = design.add_instance(("ads_simulation", "MeasEqn", "symbol"), origin, name=eq_name, angle=angle)
measeqn.parameters["Meas"].value = [eq_list[0]]
for i in range(len(eq_list) - 1):
measeqn.parameters["Meas"].repeats.append(
db.ParamItemString("Meas", "SingleTextLine", eq_list[i + 1])
)
measeqn.update_item_annotation()
class ManagedCircuitSimulator:
"""带有完整资源管理的电路仿真器"""
def __init__(self, hpeesof_dir=None):
self.simulator = ads.CircuitSimulator(hpeesof_dir)
def run_netlist(self, netlist: str, output_dir: str, **kwargs) -> None:
working_dir = kwargs.get("working_dir", output_dir)
try:
return self.simulator.run_netlist(
netlist=netlist,
output_dir=output_dir,
**kwargs
)
finally:
self._cleanup_all_temp_files(working_dir)
def _cleanup_all_temp_files(self, directory) -> None:
"""Clean up all ADS temporary files in directory"""
import glob
patterns = ["circ*.ckt", "circ*.out", "*.tmp"]
for pattern in patterns:
for temp_file in glob.glob(os.path.join(directory, pattern)):
try:
os.remove(temp_file)
print(f"Cleaned temporary file: {os.path.basename(temp_file)}")
except OSError:
pass
class SimulationManager:
"""Complete simulation manager with temporary file cleanup and path safety handling"""
def __init__(self, design: db.Design):
self.design = design
self.output_dir = os.path.join(self.design.cell.path, "data")
self.netlist = None
self.result = None
self.result_path = os.path.join(self.output_dir, f"{self.design.cell_name}.ds")
self.managed_simulator = ManagedCircuitSimulator()
self.data_file_is_exist = os.path.exists(self.result_path)
if self.data_file_is_exist:
self.result = dataset.open(self.result_path)
def validate_design_name(self) -> bool:
"""Validate design name, check for uppercase letters that may cause path encoding issues"""
cell_name = self.design.cell_name
has_uppercase = any(c.isupper() for c in cell_name)
if has_uppercase:
print(f"Warning: Cell name '{cell_name}' contains uppercase letters")
print(" ADS maps uppercase letters to %letter format, which may cause path issues")
print(" Recommend using lowercase letters for Cell names")
return False
def run_design_simulation(
self,
output_dir: Union[str, None] = None,
output_file: str = None,
validate_name: bool = True,
**sim_kwargs,
) -> None:
"""Run design simulation with automatic temporary file management"""
if output_dir is None:
output_dir = self.output_dir
os.makedirs(output_dir, exist_ok=True)
print(f"=== Running simulation: {self.design.design_name} ===")
print(f"Output directory: {output_dir}")
if validate_name:
self.validate_design_name()
try:
print("Generating netlist...")
self.netlist = self.design.generate_netlist()
if not self.netlist or len(self.netlist.strip()) == 0:
raise RuntimeError("Generated netlist is empty")
print(f"Netlist generated successfully, length: {len(self.netlist)} characters")
print("Running simulation...")
self.managed_simulator.run_netlist(
netlist=self.netlist,
output_dir=output_dir,
output_file=output_file,
**sim_kwargs,
)
print("Simulation executed successfully")
print(f"Simulation completed, result file: {self.result_path}")
self.data_file_is_exist = True
except Exception as e:
print(f"Simulation failed: {e}")
raise
def get_simulation_results(self) -> dataset.Dataset:
"""Get simulation results
Returns a context manager, should be used like this:
with sim_mgr.get_simulation_results() as results:
print(results.varblock_names) # Process results...
# Dataset will be automatically closed
"""
if not self.data_file_is_exist:
raise RuntimeError("Simulation has not been run or failed, cannot get results")
return dataset.open(self.result_path)
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- curl 转代码
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online