迟滞不能只用一张电压位移曲线判断,也不能与重复性、非线性、蠕变或动态相位滞后混为一谈。测试报告必须说明数据同步、稳定循环选择、插值或电压分箱方式、位移参考仪器和不确定度。

本文提供一套只依赖Python标准库的离线分析代码。代码读取真实测试CSV并输出逐循环和逐电压汇总,不连接高压驱动器,不生成产品测试数据,也不执行迟滞补偿。

  1. 什么是压电陶瓷迟滞

压电陶瓷迟滞是指在其他条件相同的情况下,压电执行器的位移不仅取决于当前驱动电压,还与此前经历的电压路径有关。相同电压点在升压支路和降压支路可能对应不同位移,因此在电压位移平面形成回线。

多层压电陶瓷中的迟滞与铁电畴响应、介电和机械耦合、驱动电场、温度、机械边界以及时间尺度有关。对具体器件作定量结论时,必须以对应型号和测试条件下的测量记录为依据。

1.1 迟滞不是重复性

迟滞比较同一循环中不同路径的输出差异;重复性比较相同测试程序重复执行时结果的离散程度。回线每次都很一致,仍然可能存在明显迟滞。

1.2 迟滞不是蠕变

蠕变通常描述电压保持不变后位移随时间继续变化。迟滞测试使用连续升降压路径,若在每个点停留时间很长,蠕变也可能进入测量结果,因此必须记录波形和时间。

1.3 迟滞不是动态相位差

频率升高时,驱动器带宽、传感器带宽、采集延迟和机械动态都会使电压与位移出现相位差。动态相位差会放大观察到的回线,但不应被全部解释为材料静态迟滞。

  1. 压电陶瓷迟滞指标怎么定义
    2.1 同电压支路差

在同一个循环、同一个电压点,降压支路平均位移减去升压支路平均位移,得到该电压点的支路差。报告中必须固定符号方向。

2.2 最大绝对迟滞差

对所有匹配电压点的支路差取绝对值,最大值就是该循环的最大绝对迟滞差。它具有位移单位,不能脱离位移量程解释。

2.3 位移跨度归一化

可以用最大绝对迟滞差除以该循环的实测位移跨度,再乘以100百分比,得到相对于实测跨度的归一化指标。分母必须写清,不能把它直接称为通用“迟滞率”。

2.4 回线面积

沿完整电压位移路径进行梯形积分,可以得到带有电压乘位移单位的有向回线面积。面积对采样顺序、波形、频率和同步误差敏感,适合在相同测试条件下比较,不等于材料能量损耗的完整测量。

定义边界:不同资料可能使用不同归一化分母、参考直线或迟滞百分比定义。比较数据前必须先统一公式、波形、电压范围、频率、负载和温度。
3. 压电陶瓷迟滞测试系统怎么搭建
压电陶瓷电压位移迟滞测试链路
波形发生
升压与降压
高压驱动器
电压与电流限制
压电执行器
夹具 预载 负载
位移测量
独立参考传感器
温度与力
环境和机械边界
同步采集
时间 电压 位移
离线分析
支路差与回线面积
迟滞结果必须与电压范围、波形、频率、预载、温度、传感器和同步方式绑定
压电陶瓷迟滞测试架构示意。该图不代表任何具体产品内部结构或测试参数

3.1 驱动电压
记录最小电压、最大电压、直流偏置、波形和频率。
同步测量实际施加到压电负载的电压,不要只记录软件命令。
确认驱动器峰值电流、连续电流、压摆率和限流状态。
反向电压必须遵守器件规格,不能为了形成对称回线自行施加。
3.2 位移测量
使用激光干涉仪、电容传感器或其他适用的独立位移参考。
记录传感器量程、分辨率、带宽、校准状态和采样率。
固定测量轴线、安装刚度、阿贝偏置和线缆布置。
不要把驱动器内部估算位移当作独立实测位移。
3.3 机械和环境条件
记录自由位移、预载执行器或带机构系统中的具体被测对象。
记录预载、动态负载、安装方向、夹具刚度和接触方式。
记录环境温度、执行器温度、预热时间和振动条件。
每个测试版本保持相同的稳定循环选择规则。
4. 测试波形和采样怎样设置
4.1 三角波适合支路配对
压电陶瓷迟滞测试架构图

三角波在升压段和降压段具有相对明确的方向,便于在相同电压点比较位移。实际电压仍应被测量,因为驱动器限流或带宽不足可能使输出偏离命令波形。

4.2 频率必须固定

低频测试可以减小部分动态影响,但停留时间增加后蠕变和热漂移可能更明显。高频测试更容易受到驱动器电流、传感器带宽、采集延迟和机械共振影响。没有一个频率可以代表所有应用。

4.3 电压和位移必须同步

若电压通道与位移通道存在固定时间偏移,相同时间戳对应的两个量并不属于同一物理状态,计算得到的回线会被人为放大。应使用统一时钟或经过验证的时间对齐方法。

4.4 转折点需要单独处理

波形峰值和谷值处方向发生切换,可能存在重复采样或短暂停留。CSV应明确标记rising和falling支路,不能只依靠相邻电压差在噪声中自动判断方向。

  1. CSV原始数据格式

每行记录一个同步采样点。cycle表示完整升降压循环编号,branch只能使用rising或falling。

cycle,branch,timestamp_s,voltage_v,displacement_um,temperature_c,load_n
1,rising,0.00,0.0,0.000,23.0,100.0
1,rising,0.50,50.0,5.000,23.0,100.0
1,falling,1.50,50.0,5.500,23.0,100.0

以上三行只演示格式,不是产品实测、额定参数或推荐测试条件。真实数据应满足以下要求:

同一cycle内时间戳严格递增。
每个电压分箱同时具备升压和降压样本。
电压与位移必须来自同步采集链路。
温度和负载单位在整个文件中保持一致。
所有剔除点保留原始记录和排除原因。
6. 软件、硬件和验证条件
类别 本文代码验证条件 真实测试需要补充
操作系统 Windows环境 操作系统和采集软件版本
Python Python 3.12.13,仅标准库 程序版本、文件哈希和运行命令
被测对象 未连接压电陶瓷或驱动器 型号、版本、安装、预载和负载
驱动链路 CSV离线接口 驱动器、探头、电压范围、波形和频率
位移测量 未进行产品实测 传感器、校准、带宽、采样率和不确定度
统计 按循环和电压分箱配对 循环选择、电压分箱和异常规则
数据边界:本文没有压电陶瓷产品实测数据,也不提供通用迟滞合格阈值。迟滞指标必须由适用规范、产品定义、测量能力和应用需求共同确定。
升压与降压支路配对原理图

  1. 可运行的Python迟滞分析代码

下面程序只做离线CSV分析,不产生驱动波形,不连接高压驱动器,不控制压电执行器,也不实现迟滞补偿。

#!/usr/bin/env python3
“”"Analyze paired rising and falling branches of piezo displacement data.

This offline example does not drive hardware or claim product performance.
“”"

from future import annotations

import argparse
import csv
import json
import math
import statistics
import tempfile
from dataclasses import asdict, dataclass
from pathlib import Path

VALID_BRANCHES = {“rising”, “falling”}
REQUIRED_COLUMNS = {
“cycle”,
“branch”,
“timestamp_s”,
“voltage_v”,
“displacement_um”,
“temperature_c”,
“load_n”,
}

@dataclass(frozen=True)
class Sample:
cycle: int
branch: str
timestamp_s: float
voltage_v: float
displacement_um: float
temperature_c: float
load_n: float

@dataclass(frozen=True)
class CycleResult:
cycle: int
matched_voltage_count: int
displacement_span_um: float
max_abs_branch_difference_um: float
voltage_at_max_difference_v: float
max_difference_percent_span: float
loop_area_v_um: float
temperature_range_c: float
load_range_n: float

@dataclass(frozen=True)
class VoltageResult:
voltage_v: float
cycle_count: int
rising_mean_um: float
falling_mean_um: float
branch_difference_mean_um: float
branch_difference_std_um: float

def finite_float(text: str, field: str, line_number: int) -> float:
try:
value = float(text)
except ValueError as exc:
raise ValueError(
f"line {line_number}: {field} is not numeric"
) from exc
if not math.isfinite(value):
raise ValueError(f"line {line_number}: {field} must be finite")
return value

def positive_int(text: str, field: str, line_number: int) -> int:
try:
value = int(text)
except ValueError as exc:
raise ValueError(
f"line {line_number}: {field} is not an integer"
) from exc
if value <= 0:
raise ValueError(f"line {line_number}: {field} must be positive")
return value

def load_csv(path: Path) -> list[Sample]:
samples: list[Sample] = []
with path.open(“r”, encoding=“utf-8-sig”, newline=“”) as handle:
reader = csv.DictReader(handle)
fields = set(reader.fieldnames or [])
missing = REQUIRED_COLUMNS - fields
if missing:
raise ValueError(f"missing CSV columns: {sorted(missing)}“)
for line_number, row in enumerate(reader, start=2):
branch = (row[“branch”] or “”).strip().lower()
if branch not in VALID_BRANCHES:
raise ValueError(
f"line {line_number}: branch must be rising or falling”
)
samples.append(
Sample(
cycle=positive_int(row[“cycle”], “cycle”, line_number),
branch=branch,
timestamp_s=finite_float(
row[“timestamp_s”], “timestamp_s”, line_number
),
voltage_v=finite_float(
row[“voltage_v”], “voltage_v”, line_number
),
displacement_um=finite_float(
row[“displacement_um”],
“displacement_um”,
line_number,
),
temperature_c=finite_float(
row[“temperature_c”], “temperature_c”, line_number
),
load_n=finite_float(row[“load_n”], “load_n”, line_number),
)
)
if not samples:
raise ValueError(“CSV contains no samples”)
return samples

def sample_std(values: list[float]) -> float:
return statistics.stdev(values) if len(values) >= 2 else 0.0

def voltage_bin(value: float, width: float) -> float:
return round(value / width) * width

def loop_area(samples: list[Sample]) -> float:
ordered = sorted(samples, key=lambda item: item.timestamp_s)
for previous, current in zip(ordered, ordered[1:]):
if current.timestamp_s <= previous.timestamp_s:
raise ValueError(
f"cycle {current.cycle}: timestamp must increase"
)
area = 0.0
for previous, current in zip(ordered, ordered[1:]):
mean_displacement = 0.5 * (
previous.displacement_um + current.displacement_um
)
area += mean_displacement * (current.voltage_v - previous.voltage_v)
return abs(area)

def analyze_cycle(
cycle: int, samples: list[Sample], bin_width_v: float
) -> tuple[CycleResult, dict[float, tuple[float, float]]]:
branch_groups: dict[tuple[str, float], list[float]] = {}
for sample in samples:
key = (sample.branch, voltage_bin(sample.voltage_v, bin_width_v))
branch_groups.setdefault(key, []).append(sample.displacement_um)

rising_voltages = {
    voltage for branch, voltage in branch_groups if branch == "rising"
}
falling_voltages = {
    voltage for branch, voltage in branch_groups if branch == "falling"
}
matched_voltages = sorted(rising_voltages & falling_voltages)
if len(matched_voltages) < 3:
    raise ValueError(
        f"cycle {cycle}: at least three matched voltage bins are required"
    )

pairs: dict[float, tuple[float, float]] = {}
differences: list[tuple[float, float]] = []
for voltage in matched_voltages:
    rising_mean = statistics.fmean(branch_groups[("rising", voltage)])
    falling_mean = statistics.fmean(branch_groups[("falling", voltage)])
    pairs[voltage] = (rising_mean, falling_mean)
    differences.append((voltage, falling_mean - rising_mean))

displacement_values = [item.displacement_um for item in samples]
displacement_span = max(displacement_values) - min(displacement_values)
if displacement_span <= 0:
    raise ValueError(f"cycle {cycle}: displacement span must be positive")
voltage_at_max, signed_max = max(
    differences, key=lambda item: abs(item[1])
)
temperatures = [item.temperature_c for item in samples]
loads = [item.load_n for item in samples]
result = CycleResult(
    cycle=cycle,
    matched_voltage_count=len(matched_voltages),
    displacement_span_um=displacement_span,
    max_abs_branch_difference_um=abs(signed_max),
    voltage_at_max_difference_v=voltage_at_max,
    max_difference_percent_span=(100.0 * abs(signed_max) / displacement_span),
    loop_area_v_um=loop_area(samples),
    temperature_range_c=max(temperatures) - min(temperatures),
    load_range_n=max(loads) - min(loads),
)
return result, pairs

def analyze(
samples: list[Sample], bin_width_v: float
) -> tuple[list[CycleResult], list[VoltageResult]]:
if bin_width_v <= 0:
raise ValueError(“voltage bin width must be greater than zero”)
by_cycle: dict[int, list[Sample]] = {}
for sample in samples:
by_cycle.setdefault(sample.cycle, []).append(sample)

cycle_results: list[CycleResult] = []
all_pairs: dict[float, list[tuple[float, float]]] = {}
for cycle, group in sorted(by_cycle.items()):
    result, pairs = analyze_cycle(cycle, group, bin_width_v)
    cycle_results.append(result)
    for voltage, pair in pairs.items():
        all_pairs.setdefault(voltage, []).append(pair)

voltage_results: list[VoltageResult] = []
for voltage, pairs in sorted(all_pairs.items()):
    rising_values = [pair[0] for pair in pairs]
    falling_values = [pair[1] for pair in pairs]
    differences = [falling - rising for rising, falling in pairs]
    voltage_results.append(
        VoltageResult(
            voltage_v=voltage,
            cycle_count=len(pairs),
            rising_mean_um=statistics.fmean(rising_values),
            falling_mean_um=statistics.fmean(falling_values),
            branch_difference_mean_um=statistics.fmean(differences),
            branch_difference_std_um=sample_std(differences),
        )
    )
return cycle_results, voltage_results

def write_csv(path: Path, rows: list[object], fieldnames: list[str]) -> None:
with path.open(“w”, encoding=“utf-8-sig”, newline=“”) as handle:
writer = csv.DictWriter(handle, fieldnames=fieldnames)
writer.writeheader()
for row in rows:
writer.writerow(asdict(row))

def run_self_test() -> None:
rows = [
“cycle,branch,timestamp_s,voltage_v,displacement_um,”
“temperature_c,load_n”,
“1,rising,0,0,0,23,100”,
“1,rising,1,50,5,23,100”,
“1,rising,2,100,10,23,100”,
“1,falling,3,100,10,23,100”,
“1,falling,4,50,5.5,23,100”,
“1,falling,5,0,0.5,23,100”,
]
with tempfile.TemporaryDirectory() as temporary_directory:
path = Path(temporary_directory) / “self_test.csv”
path.write_text(“\n”.join(rows) + “\n”, encoding=“utf-8”)
cycle_results, voltage_results = analyze(load_csv(path), 1.0)
assert len(cycle_results) == 1
assert len(voltage_results) == 3
result = cycle_results[0]
assert result.matched_voltage_count == 3
assert math.isclose(result.displacement_span_um, 10.0)
assert math.isclose(result.max_abs_branch_difference_um, 0.5)
assert math.isclose(result.max_difference_percent_span, 5.0)

def main() -> None:
parser = argparse.ArgumentParser(
description=“Analyze piezo voltage-displacement hysteresis branches.”
)
parser.add_argument(“–input”, type=Path, help=“input CSV path”)
parser.add_argument(
“–voltage-bin-v”,
type=float,
default=0.1,
help=“voltage bin width used to pair rising and falling samples”,
)
parser.add_argument(
“–output-dir”, type=Path, default=Path(“piezo_hysteresis_output”)
)
parser.add_argument(“–self-test”, action=“store_true”)
args = parser.parse_args()

if args.self_test:
    run_self_test()
    print("self-test: PASS")
    if args.input is None:
        return

if args.input is None:
    parser.error("--input is required")

samples = load_csv(args.input)
cycle_results, voltage_results = analyze(samples, args.voltage_bin_v)
args.output_dir.mkdir(parents=True, exist_ok=True)
cycle_path = args.output_dir / "piezo_hysteresis_by_cycle.csv"
voltage_path = args.output_dir / "piezo_hysteresis_by_voltage.csv"
report_path = args.output_dir / "piezo_hysteresis_report.json"
write_csv(cycle_path, cycle_results, list(CycleResult.__dataclass_fields__))
write_csv(voltage_path, voltage_results, list(VoltageResult.__dataclass_fields__))
payload = {
    "data_classification": "user_measurement_input",
    "branch_difference_definition": "falling minus rising",
    "cycle_results": [asdict(item) for item in cycle_results],
    "voltage_results": [asdict(item) for item in voltage_results],
}
report_path.write_text(
    json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"
)
print(f"cycles: {len(cycle_results)}")
print(f"cycle summary: {cycle_path}")
print(f"voltage summary: {voltage_path}")
print(f"report: {report_path}")

if name == “main”:
main()

  1. 代码如何运行和验证
    8.1 运行环境
    操作系统:Windows环境。
    Python版本:Python 3.12.13。
    依赖:只使用Python标准库。
    输入:UTF-8或UTF-8 BOM编码CSV。
    输出:逐循环CSV、逐电压CSV和JSON报告。
    8.2 运行自检
    python piezo_hysteresis_analyzer.py --self-test

预期输出:

self-test: PASS

8.3 分析真实测量数据
python piezo_hysteresis_analyzer.py --input actual_piezo_hysteresis.csv --voltage-bin-v 0.1 --output-dir result

命令中的电压分箱宽度只是格式示例。真实值应结合电压采样噪声、目标电压间隔和测量分辨率确定。分箱过宽会掩盖曲线细节,过窄则可能无法匹配升压和降压样本。
压电迟滞数据处理流程图

  1. 如何解释分析结果
    9.1 支路差随电压增大

这可能与驱动电场、材料响应和机械边界有关,但不能只凭曲线形状判断失效机理。应检查不同循环、温度、负载和频率下是否保持一致。

9.2 回线随频率明显变宽

优先检查驱动器电流能力、实际输出电压、位移传感器带宽、采集同步和机械动态。只有排除测量链路动态影响后,才能进一步讨论材料和结构贡献。

9.3 不同循环的回线整体漂移

检查温升、零点漂移、夹具松动、预载变化和蠕变。迟滞支路差与回线中心漂移是不同问题,应分别报告。

9.4 回线局部出现尖点或跳变

检查采样丢点、电压限流、传感器量程切换、接触不稳定和数据排序。不能在没有原始波形的情况下直接判定为压电陶瓷异常。

9.5 闭环后回线变窄

闭环控制可以利用位移反馈减小命令与输出之间的路径差异,但效果取决于传感器、控制带宽、噪声、稳定性和轨迹。闭环不等于迟滞物理机制消失,也不保证所有频率和负载下误差为零。
压电迟滞测量误差来源图

  1. 常见误区与安全风险
    只记录命令电压,没有测量压电负载两端的实际电压。
    电压与位移通道没有同步,却直接绘制回线。
    只测试一个循环,并选择看起来最平滑的一次。
    把回线宽度全部解释为材料迟滞,忽略驱动和机械动态。
    不同频率、预载或温度的数据使用同一个迟滞结论。
    使用内部传感器自证系统位移准确度。
    没有说明归一化分母,却直接比较“迟滞百分比”。
    为了形成对称波形,对不允许反向电压的器件施加负压。
    高压与机械安全:压电驱动器可能输出危险高压,断电后的压电负载仍可能保留电荷。夹具和预载结构可能储存机械能。测试系统应具备绝缘、接地、联锁、放电、限位、过流和异常停机措施。本文代码不能承担驱动控制或安全保护功能。
  2. 常见问题
    11.1 压电陶瓷迟滞怎么测试

在固定电压范围、波形、频率、预载、温度和安装条件下,施加完整升降压循环,同步测量实际电压与独立位移,在相同电压点比较升压和降压支路位移,并报告最大支路差、位移跨度和回线面积。

11.2 压电陶瓷迟滞和非线性有什么区别

迟滞描述相同输入在不同历史路径下的输出差异;非线性描述输入输出关系偏离所选参考模型。两者可能同时存在,但定义和计算方法不同。

11.3 压电迟滞百分比应该怎么计算

必须先定义分子和分母。本文使用最大绝对升降支路差除以该循环实测位移跨度。其他资料可能采用不同定义,比较前需要统一口径。

11.4 为什么压电迟滞回线会随频率变化

频率变化会同时影响材料时间响应、驱动器电流与带宽、温升、传感器带宽、采集延迟和机械动态,因此需要在固定条件下测试并分别排查。

11.5 压电陶瓷闭环控制能完全消除迟滞吗

不能这样保证。闭环可以在传感器和控制带宽允许的范围内减小输出误差,但仍受噪声、稳定性、延迟、饱和、负载和轨迹限制。

11.6 测压电迟滞需要多少个循环

没有适用于所有器件的固定数量。应由测试规范、预处理要求、重复性目标和统计置信度确定,并预先规定舍弃哪些稳定前循环。

11.7 可以用驱动器内部反馈测迟滞吗

可以用于内部控制状态分析,但不能自动作为独立位移验证。对外报告位移迟滞时,应说明传感器来源、校准、带宽和是否独立于控制反馈。

11.8 Python代码可以直接补偿压电迟滞吗

本文代码不能。它只分析离线电压位移数据。迟滞补偿还需要模型辨识、实时实现、稳定性验证、饱和限制和目标工况测试。

  1. 结论

压电陶瓷迟滞测试的关键不是画出一条闭合曲线,而是保证实际电压和独立位移同步、升降支路在相同电压点可配对,并把电压范围、频率、预载、温度、传感器和循环选择规则写清楚。

最大绝对支路差、相对实测位移跨度的归一化指标和回线面积回答不同问题。只有在定义和测试条件一致时,这些指标才能用于不同循环、工况或器件之间的比较。

  1. 参考资料
    IEEE Std 176-1987,IEEE Standard on Piezoelectricity,Institute of Electrical and Electronics Engineers。
    IEC 60483:1976,Guide to dynamic measurements of piezoelectric ceramics with high electromechanical coupling,International Electrotechnical Commission。
    Bernard Jaffe,William R. Cook Jr.,Hans Jaffe,Piezoelectric Ceramics,Academic Press,1971。
    Kenji Uchino,Ferroelectric Devices,Second Edition,CRC Press,2009。
    JCGM 100:2008,Evaluation of measurement data - Guide to the expression of uncertainty in measurement,Joint Committee for Guides in Metrology,https://www.bipm.org/en/committees/jc/jcgm/publications。
    Python Software Foundation,Python 3.12 Standard Library Documentation,https://docs.python.org/3.12/library/,访问日期:2026年7月29日。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐