已解决TypeError: Can only append a dict if ignore_index=True

已解决pandas使用df.append()方法以dict形式添加数据时,抛出异常TypeError: Can only append a dict if ignore_index=True的正确解决方法,亲测有效!!!









报错问题



一个小伙伴遇到问题跑来私信我,在pandas使用df.append()方法以dict形式添加数据时,但是发生了报错(当时他心里瞬间凉了一大截,跑来找我求助,然后顺利帮助他解决了,顺便记录一下希望可以帮助到更多遇到这个bug不会解决的小伙伴),报错代码如下所示:

import pandas as pd

df1 = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]}, index=list('abc'))
print(df1)
print(df1.append({'B':7,'C':8}))

报错信息截图如下所示


在这里插入图片描述




报错翻译



报错信息内容翻译如下所示

类型错误:如果ignore_index=True,则只能追加dict




报错原因



报错原因

使用df.append()方法以dict形式添加数据,需要写一个必要参数。

小伙伴们按下面的方法修改代码即可!!!




解决方法



append函数添加一个ignore_index=True的参数即可解决:

import pandas as pd

df1 = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]}, index=list('abc'))
print(df1)
print(df1.append({'B':7,'C':8},ignore_index=True))

再次运行代码成功了:


在这里插入图片描述

以上是此问题报错原因的解决方法,欢迎评论区留言讨论是否能解决,如果有用欢迎点赞收藏文章谢谢支持,博主才有动力持续记录遇到的问题!!!

千人全栈VIP答疑群联系博主帮忙解决报错

由于博主时间精力有限,每天私信人数太多,没办法每个粉丝都及时回复,所以优先回复VIP粉丝,可以通过订阅限时9.9付费专栏《100天精通Python从入门到就业》进入千人全栈VIP答疑群,获得优先解答机会(代码指导、远程服务),白嫖80G学习资料大礼包,专栏订阅地址:https://blog.youkuaiyun.com/yuan2019035055/category_11466020.html

  • 优点作者优先解答机会(代码指导、远程服务),群里大佬众多可以抱团取暖(大厂内推机会),此专栏文章是专门针对零基础和需要进阶提升的同学所准备的一套完整教学,从0到100的不断进阶深入,后续还有实战项目,轻松应对面试!

  • 专栏福利简历指导、招聘内推、每周送实体书、80G全栈学习视频、300本IT电子书:Python、Java、前端、大数据、数据库、算法、爬虫、数据分析、机器学习、面试题库等等

  • 注意:如果希望得到及时回复,和大佬们交流学习,订阅专栏后私信博主进千人VIP答疑群在这里插入图片描述
    在这里插入图片描述

免费资料获取,更多粉丝福利,关注下方公众号获取

在这里插入图片描述

import torch import json import os import argparse import numpy as np import re from torch.utils.data import Dataset, DataLoader from tqdm import tqdm from PIL import Image from peft import LoraConfig, get_peft_model from transformers import ( AutoModelForCausalLM, AutoProcessor, TrainingArguments, BitsAndBytesConfig, GenerationConfig, AutoTokenizer, AutoImageProcessor, get_cosine_schedule_with_warmup ) import torch.optim as optim from sklearn.metrics import f1_score, accuracy_score, precision_score, recall_score, classification_report import warnings warnings.filterwarnings("ignore", message="Could not find a config file") # 标签映射定义 TASK1_LABEL_MAP = {"无害": 0, "有害": 1} TASK2_LABEL_MAP = { 0: "无害", 1: "针对性有害", 2: "一般性有害", 3: "性暗示", 4: "沮丧文化" } class HarmfulMemeDataset(Dataset): def __init__(self, annotation_path, tokenizer, image_processor, task_name, image_root=None): print(f"Loading annotations from: {annotation_path}") self.image_root = image_root self.tokenizer = tokenizer self.image_processor = image_processor self.processor = tokenizer # 关键修复:添加processor属性引用 with open(annotation_path, 'r', encoding='utf-8') as f: self.data = json.load(f) self.task_name = task_name self.label_token_ids = self._get_label_tokens() def _get_label_tokens(self): """获取标签对应的token ID""" if self.task_name == "task_1": labels = ["无害", "有害"] else: labels = [str(i) for i in range(5)] # 0-4 token_ids = {} tokenizer = self.processor.tokenizer for label in labels: tokens = tokenizer.encode(label) token_ids[label] = tokens # 保存完整的token序列 if len(tokens) > 1: print(f"警告: 标签 '{label}' 被分词为多个token: {tokens}") return token_ids def __len__(self): return len(self.data) def __getitem__(self, idx): sample = self.data[idx] if self.image_root: image_path = os.path.join(self.image_root, sample["path"]) else: image_path = sample["path"] if not os.path.exists(image_path): raise FileNotFoundError(f"图片不存在:{image_path}") image = Image.open(image_path).convert("RGB") if self.task_name == "task_1": user_content = f"任务:有害模因检测(是否有害)。\n输入文本:'{sample['text']}'\n请判断此图文组合是否有害。" raw_label = sample['label']# 原始标签是字符串("无害"或"有害") # 根据TASK1_LABEL_MAP进行映射 if isinstance(raw_label, int): # 如果是整数,转换为对应的字符串标签 label_map = {v: k for k, v in TASK1_LABEL_MAP.items()} # 反转映射 label = label_map.get(raw_label, "无害") # 默认值为"无害" else: # 如果已经是字符串,直接使用 label = raw_label label_token = self.label_token_ids[label] assistant_content = f"结论:{label}。\n理由:{sample['explanation']}" else: user_content = f"任务:有害模因类型分类。\n输入文本:'{sample['text']}'\n请判断此图文组合的有害类型(0-4)。" raw_label = str(sample['type'])# 将整数标签转换为字符串 label = str(raw_label) label_token = self.label_token_ids[label] assistant_content = f"结论:{label}。\n理由:{sample['explanation']}" messages = [ {"role": "user", "content": [{"type": "image"}, {"type": "text", "text": user_content}]}, {"role": "assistant", "content": [{"type": "text", "text": assistant_content}]} ] prompt = self.processor.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, chat_format="chatml" ) # 单独处理图像 image = self.image_processor( images=image, return_tensors="pt" )["pixel_values"].squeeze(0) # 单独处理文本 encoding = self.tokenizer( text=prompt, return_tensors="pt", padding=False, truncation=False ) prompt_tokens = encoding["input_ids"][0].tolist() # 找到结论标签的位置 conclusion_start = self.processor.tokenizer.encode("结论:") # 在prompt中查找"结论:"的位置 start_idx = -1 for i in range(len(prompt_tokens) - len(conclusion_start) + 1): if prompt_tokens[i:i+len(conclusion_start)] == conclusion_start: start_idx = i + len(conclusion_start) break inputs = self.processor( text=prompt, images=image, return_tensors="pt", padding="max_length", truncation=True, max_length=512 ) inputs = {k: v.squeeze(0) for k, v in inputs.items()} # 创建标签张量,只标记结论位置 labels = torch.full_like(inputs["input_ids"], fill_value=-100, dtype=torch.long) if start_idx != -1 and start_idx < len(labels): # 标记整个标签token序列 label_tokens = self.label_token_ids[label] for i, token_id in enumerate(label_tokens): if start_idx + i < len(labels): labels[start_idx + i] = token_id inputs["labels"] = labels return inputs def parse_generated_text(self,text): """解析生成的文本,提取结论标签""" conclusion_match = re.search(r"结论[::]\s*(\S+)", text) if not conclusion_match: return None conclusion = conclusion_match.group(1).strip().rstrip('。.') # 处理多token标签 if conclusion in ["无害", "有害"]: # 任务1标签 return conclusion elif conclusion.isdigit() and 0 <= int(conclusion) <= 4: # 任务2标签 return conclusion # 尝试分词匹配 tokenizer = AutoProcessor.from_pretrained(args.model_id).tokenizer conclusion_tokens = tokenizer.encode(conclusion, add_special_tokens=False) # 与已知标签的token序列匹配 for label, tokens in self.label_token_ids.items(): if conclusion_tokens == tokens: return label return None def compute_metrics(task_name, preds, labels): """计算评估指标""" mask = labels != -100 preds = preds[mask] labels = labels[mask] if task_name == "task_1": # 二分类任务 return { "accuracy": accuracy_score(labels, preds), "f1": f1_score(labels, preds, average="binary"), "precision": precision_score(labels, preds, average="binary"), "recall": recall_score(labels, preds, average="binary") } else: # 多分类任务 report = classification_report(labels, preds, output_dict=True, zero_division=0) return { "accuracy": accuracy_score(labels, preds), "f1_macro": f1_score(labels, preds, average="macro"), "precision_macro": precision_score(labels, preds, average="macro"), "recall_macro": recall_score(labels, preds, average="macro"), "class_report": report } def main(args): os.environ["TOKENIZERS_PARALLELISM"] = "false" # 1. 加载模型和预处理器 print("Loading model and processor...") quantization_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16 ) model = AutoModelForCausalLM.from_pretrained( args.model_id, quantization_config=quantization_config, trust_remote_code=True, device_map="auto", bf16=True ) model.generation_config = GenerationConfig.from_pretrained( args.model_id, trust_remote_code=True, chat_format="chatml", max_new_tokens=100, pad_token_id=model.generation_config.eos_token_id ) # 分别初始化文本和图像处理器 tokenizer = AutoTokenizer.from_pretrained( args.model_id, trust_remote_code=True, pad_token='<|endoftext|>' # 显式设置pad_token ) image_processor = AutoImageProcessor.from_pretrained( args.model_id, trust_remote_code=True ) tokenizer.chat_template = """{% for message in messages %} <|im_start|>{{ message['role'] }} {{ message['content'] }} <|im_end|> {% endfor %} {% if add_generation_prompt %} <|im_start|>assistant {% endif %}""" # 设置pad token # 确保pad_token正确设置 if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token tokenizer.pad_token_id = tokenizer.eos_token_id # 2. LoRA配置 print("Configuring LoRA...") lora_config = LoraConfig( r=args.lora_rank, lora_alpha=args.lora_alpha, lora_dropout=args.lora_dropout, bias="none", task_type="CAUSAL_LM", target_modules=[ "c_attn", "c_proj", "w1", "w2", "w3", "visual.proj", "visual.image_encoder" ] ) peft_model = get_peft_model(model, lora_config) peft_model.print_trainable_parameters() # 3. 初始化优化器和调度器 optimizer = optim.AdamW( peft_model.parameters(), lr=args.learning_rate, weight_decay=args.weight_decay ) # 4. 训练参数配置 training_args = TrainingArguments( output_dir=os.path.join(args.output_dir, args.task), num_train_epochs=args.epochs, per_device_train_batch_size=args.batch_size, per_device_eval_batch_size=args.eval_batch_size, gradient_accumulation_steps=args.grad_accum_steps, learning_rate=args.learning_rate, weight_decay=args.weight_decay, lr_scheduler_type="cosine", logging_strategy="steps", logging_steps=10, save_strategy="epoch", eval_strategy="epoch", eval_accumulation_steps=1, metric_for_best_model="f1" if args.task == "task_1" else "f1_macro", greater_is_better=True, load_best_model_at_end=True, bf16=True, report_to="none", remove_unused_columns=False, disable_tqdm=False, skip_memory_metrics=True, dataloader_pin_memory=False, ) # 5. 加载数据集 print(f"Loading datasets for {args.task}...") train_dataset = HarmfulMemeDataset( annotation_path=args.train_annotation_path, tokenizer=tokenizer, image_processor=image_processor, task_name=args.task, image_root=args.image_root ) test_dataset = HarmfulMemeDataset( annotation_path=args.test_annotation_path, tokenizer=tokenizer, image_processor=image_processor, task_name=args.task, image_root=args.image_root ) # 创建数据加载器 train_loader = DataLoader( train_dataset, batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers, pin_memory=True ) eval_loader = DataLoader( test_dataset, batch_size=args.eval_batch_size, shuffle=False, num_workers=args.num_workers, pin_memory=True ) # 计算总步数,初始化学习率调度器 total_train_steps = len(train_loader) // args.grad_accum_steps * args.epochs scheduler = get_cosine_schedule_with_warmup( optimizer, num_warmup_steps=args.warmup_steps, num_training_steps=total_train_steps ) # 6. 训练循环 print(f"Starting {args.task} training...") best_metric = -1 for epoch in range(args.epochs): print(f"\n===== Epoch {epoch + 1}/{args.epochs} =====") # 训练阶段 peft_model.train() total_train_loss = 0.0 train_pbar = tqdm(train_loader, desc=f"Training Epoch {epoch + 1}", unit="batch") for step, batch in enumerate(train_pbar): batch = {k: v.to(peft_model.device) for k, v in batch.items()} # 前向传播 outputs = peft_model(**batch) loss = outputs.loss total_train_loss += loss.item() # 梯度累积 loss = loss / args.grad_accum_steps loss.backward() # 参数更新 if (step + 1) % args.grad_accum_steps == 0: optimizer.step() scheduler.step() optimizer.zero_grad() # 更新进度条 train_pbar.set_postfix({"loss": f"{loss.item() * args.grad_accum_steps:.4f}"}) avg_train_loss = total_train_loss / len(train_loader) print(f"Epoch {epoch + 1} 平均训练损失: {avg_train_loss:.4f}") # 评估阶段 peft_model.eval() all_preds = [] all_labels = [] all_generated_texts = [] eval_pbar = tqdm(eval_loader, desc=f"Evaluating Epoch {epoch + 1}", unit="batch") with torch.no_grad(): for batch in eval_pbar: # 获取真实标签 labels = batch["labels"].cpu().numpy() mask = labels != -100 valid_labels = labels[mask].reshape(-1) # 生成文本 inputs = {k: v.to(peft_model.device) for k, v in batch.items() if k != "labels"} pad_token_id = tokenizer.pad_token_id or tokenizer.eos_token_id generated_ids = peft_model.generate( **inputs, generation_config=model.generation_config, pad_token_id=pad_token_id # 使用修正后的值 ) # 解码生成的文本 generated_texts = tokenizer.batch_decode( generated_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True ) # 解析生成的文本获取预测标签 batch_preds = [] for text in generated_texts: # 提取assistant的响应部分 if "<|im_start|>assistant" in text: response = text.split("<|im_start|>assistant")[-1].strip() else: response = text # 解析结论 conclusion = parse_generated_text(response) if conclusion is None: # 无法解析结论,使用默认值 pred_label = 0 if args.task == "task_1" else "0" else: pred_label = conclusion # 转换为数字标签 if args.task == "task_1": # 二分类任务 if "无害" in pred_label: pred_value = 0 elif "有害" in pred_label: pred_value = 1 else: # 无法解析,使用默认值 pred_value = 0 else: # 多分类任务 if pred_label in ["0", "1", "2", "3", "4"]: pred_value = int(pred_label) else: # 无法解析,使用默认值 pred_value = 0 batch_preds.append(pred_value) all_preds.extend(batch_preds) all_labels.extend(valid_labels.tolist()) all_generated_texts.extend(generated_texts) # 计算评估指标 metrics = compute_metrics(args.task, np.array(all_preds), np.array(all_labels)) # 打印评估结果 print("\n评估指标:") print("=" * 50) if args.task == "task_1": print(f"Accuracy: {metrics['accuracy']:.4f}") print(f"F1 Score: {metrics['f1']:.4f}") print(f"Precision: {metrics['precision']:.4f}") print(f"Recall: {metrics['recall']:.4f}") else: print(f"Accuracy: {metrics['accuracy']:.4f}") print(f"Macro F1: {metrics['f1_macro']:.4f}") print(f"Macro Precision: {metrics['precision_macro']:.4f}") print(f"Macro Recall: {metrics['recall_macro']:.4f}") print("\n分类报告:") print(classification_report(all_labels, all_preds, target_names=list(TASK2_LABEL_MAP.values()), zero_division=0)) print("=" * 50) # 保存最佳模型 current_metric = metrics["f1"] if args.task == "task_1" else metrics["f1_macro"] if current_metric > best_metric: best_metric = current_metric save_path = os.path.join(training_args.output_dir, f"best_model_epoch{epoch+1}") print(f"保存最佳模型(指标 {current_metric:.4f})到 {save_path}") peft_model.save_pretrained(save_path) # 保存生成的文本示例 sample_output_path = os.path.join(save_path, "sample_outputs.txt") with open(sample_output_path, "w", encoding="utf-8") as f: for i, text in enumerate(all_generated_texts[:10]): f.write(f"样本 {i+1}:\n") f.write(text) f.write("\n" + "-"*80 + "\n") print(f"训练完成!最佳指标: {best_metric:.4f}") if __name__ == "__main__": parser = argparse.ArgumentParser(description="训练有害模因检测模型") parser.add_argument("--model_id", default="/xzwu/Qwen-VL-Chat", help="预训练模型路径") parser.add_argument("--output_dir", default="/xzwu/explain-m3-adapter", help="输出目录") parser.add_argument("--epochs", type=int, default=5, help="训练轮数") parser.add_argument("--batch_size", type=int, default=4, help="训练批次大小") parser.add_argument("--eval_batch_size", type=int, default=4, help="评估批次大小") parser.add_argument("--grad_accum_steps", type=int, default=2, help="梯度累积步数") parser.add_argument("--learning_rate", type=float, default=1e-5, help="学习率") parser.add_argument("--weight_decay", type=float, default=0.01, help="权重衰减") parser.add_argument("--warmup_steps", type=int, default=100, help="预热步数") parser.add_argument("--lora_rank", type=int, default=8, help="LoRA秩") parser.add_argument("--lora_alpha", type=int, default=16, help="LoRA alpha") parser.add_argument("--lora_dropout", type=float, default=0.1, help="LoRA dropout") parser.add_argument("--num_workers", type=int, default=4, help="数据加载工作线程数") parser.add_argument("--task", choices=["task_1", "task_2"], default="task_1", help="任务类型") parser.add_argument("--train_annotation_path", default="/xzwu/data/data/train_data_explanation.json", help="训练标注路径") parser.add_argument("--test_annotation_path", default="/xzwu/data/data/test_data_explanation.json", help="测试标注路径") parser.add_argument("--image_root", default="/xzwu/data/meme", help="图片根目录") args = parser.parse_args() # 打印配置 print("=" * 50) print("训练配置:") for arg in vars(args): print(f"{arg}: {getattr(args, arg)}") print("=" * 50) main(args)运行代码后报错:Epoch 1 平均训练损失: 0.4206 Evaluating Epoch 1: 0%| | 0/600 [00:00<?, ?batch/s]A decoder-only architecture is being used, but right-padding was detected! For correct generation results, please set `padding_side='left'` when initializing the tokenizer. Evaluating Epoch 1: 0%| | 0/600 [00:12<?, ?batch/s] Traceback (most recent call last): File "/xzwu/explain-m3/explain-m3-project/train2.py", line 531, in <module> main(args) File "/xzwu/explain-m3/explain-m3-project/train2.py", line 431, in main conclusion = parse_generated_text(response) TypeError: parse_generated_text() missing 1 required positional argument: 'text'
07-29
如下代码,有一个BUG。 我的眼图PASS区域顶上和底下夹住PASS区域的Y轴范围,向X轴方向,有两条虚线,这两条我不需要。请帮忙去除。只去掉多的两根横线。眼图中间找中心点的辅助线仍然保留。我勾勒原始数据边界线的闭合曲线仍然要保留,只是说这个功能多产生了两条横线。 有一个新需求。请输入日志文件路径:,这个位置,如果是输入文件夹,就批量处理文件夹下面所有的.txt或者.log文件。如果输入的路径是到文件名,就只处理这一个文件。 import numpy as np “”" NumPy (Numerical Python) - 科学计算基础库 主要功能: 提供高效的N维数组对象(ndarray) 支持广播功能函数 提供线性代数、傅里叶变换、随机数生成等功能 在本代码中主要用于: 数值计算和数组操作 科学计算支持 “”" import matplotlib.pyplot as plt “”" Matplotlib - Python中最强大的绘图库 主要功能: 创建静态、动态和交互式图表 支持多种图表类型(线图、散点图、柱状图等) 高度可定制化(颜色、线型、标签等) 在本代码中主要用于: 绘制眼图 可视化DDR校准数据 创建图表和图形界面 “”" import re “”" re (Regular Expression) - 正则表达式模块 主要功能: 文本搜索和模式匹配 文本替换 复杂字符串处理 在本代码中主要用于: 解析日志文件中的关键数据 提取VREF、偏移量、数据点等信息 处理复杂的文本匹配任务 “”" import datetime “”" datetime - 日期和时间处理模块 主要功能: 日期和时间的表示 日期和时间的计算 日期和时间的格式化 在本代码中主要用于: 生成时间戳 创建带时间戳的文件名 记录报告生成时间 “”" from matplotlib.lines import Line2D “”" Line2D - 用于创建二维线条对象 主要功能: 表示二维坐标系中的线条 控制线条属性(颜色、线宽、样式等) 在本代码中主要用于: 创建图例元素 自定义图表中的线条样式 “”" import os “”" os (Operating System) - 操作系统接口模块 主要功能: 文件和目录操作 路径操作 进程管理 在本代码中主要用于: 文件路径处理 目录创建 文件存在性检查 “”" from collections import defaultdict “”" defaultdict - 带默认值的字典 主要功能: 当访问不存在的键时返回默认值 避免KeyError异常 在本代码中主要用于: 存储电压-窗口映射关系 简化字典初始化操作 “”" import math “”" math - 数学函数模块 主要功能: 提供数学运算函数 包括三角函数、对数、取整等 在本代码中新增用于: 计算中位数 “”" 新旧日志格式识别标志 OLD_LOG_FORMAT = 0 NEW_LOG_FORMAT = 1 def detect_log_format(log_content): “”“自动检测日志格式”“” # 检测新日志的时间戳模式 if re.search(r’ ParseError: KaTeX parse error: Undefined control sequence: \d at position 1: \̲d̲{4}-\d{2}-\d{2}… ‘, log_content): return NEW_LOG_FORMAT # 检测旧日志的特征行 elif re.search(r’NOTICE:\s+Booting’, log_content): return OLD_LOG_FORMAT # 默认作为新日志处理 return NEW_LOG_FORMAT 健壮的文件读取函数 - 详细解释每个编程概念 def robust_read_file(file_path): “”" 健壮的文件读取函数,处理不同编码的文件 参数: file_path - 文件在电脑上的完整路径(字符串) 编程概念详解: 1. 函数定义:def关键字用于定义函数,函数是一段可重复使用的代码块 2. 参数传递:file_path是形式参数,调用时传入实际文件路径 3. 异常处理:try-except结构用于捕获和处理运行时错误 4. 上下文管理器:with语句用于资源管理,确保文件正确关闭 5. 编码处理:不同文件可能使用不同编码(UTF-8, Latin-1等) 6. 正则表达式:用于过滤控制字符 """ ########################################################## # 增强:支持多种编码格式,特别是Tera Term日志 ########################################################## # 尝试的编码列表(针对Windows环境) encodings = ['utf-8', 'latin-1', 'cp1252', 'gbk', 'big5', 'shift_jis', 'utf-16le'] for encoding in encodings: try: with open(file_path, 'rb') as f: raw_content = f.read() # 尝试解码 content = raw_content.decode(encoding, errors='replace') # 移除ANSI转义序列 (例如: \x1b[32m 颜色代码) ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') content = ansi_escape.sub('', content) # 移除其他控制字符 (保留换行符) content = re.sub(r'[\x00-\x09\x0B-\x1F\x7F]', '', content) # 修复常见的乱码模式(特定于Tera Term) content = re.sub(r'[^\x20-\x7E\r\n\u4e00-\u9FFF]', ' ', content) return content except UnicodeDecodeError: continue # 回退方案:使用二进制读取并过滤 try: with open(file_path, 'rb') as f: raw_content = f.read() # 使用错误替换并移除不可打印字符 return raw_content.decode('utf-8', errors='replace').encode('utf-8', errors='replace').decode('utf-8', errors='ignore') except Exception as e: print(f"严重错误: 无法读取文件 {file_path}: {e}") return None 日志解析函数 - 重点讲解正则表达式 def parse_log_file(log_content, normalization_point): “”" 解析DDR校准日志文件,提取关键数据 参数: log_content - 日志文件的内容(字符串) normalization_point - 归一化点(十六进制整数) 数据结构说明: data = { vref: { dq_index: { 'read': (min, max, window), 'write': (min, max, window) } } } raw_data = { vref: { dq_index: { 'read': {'min': min_val, 'max': max_val}, 'write': {'min': min_val, 'max': max_val} } } } """ # 检测日志格式并调用相应的解析函数 log_format = detect_log_format(log_content) if log_format == OLD_LOG_FORMAT: print("检测到旧日志格式") return parse_old_log(log_content, normalization_point) else: print("检测到新日志格式(Tera Term)") return parse_tera_term_log(log_content, normalization_point) def parse_old_log(log_content, normalization_point): “”" 解析旧日志格式(原始格式) 参数: log_content - 日志文件的内容(字符串) normalization_point - 归一化点(十六进制整数) “”" # 初始化数据结构 data = {} # 主数据结构,存储解析后的数据 current_vref = None # 当前处理的vref值 pending_data = {} # 临时存储待处理的数据(字典) current_offset = None # 当前偏移量 raw_data = {} # 存储原始数据(偏移前) # 按行处理日志内容 # 字符串方法:split('\n') 按换行符分割字符串 for line in log_content.split('\n'): # 字符串方法:strip() 移除首尾空白字符 line = line.strip() # 空行检查 if not line: continue # 跳过空行 ########################################################## # 正则表达式1:匹配VREF行 # 模式:r'.*vref:\s*0x([0-9a-fA-F]+)' # 目标示例: "Setting vref: 0x1A3" # # 详细分解: # .* - 匹配任意字符(除换行符外)0次或多次(贪婪匹配) # vref: - 匹配字面字符串 "vref:" # \s* - 匹配0个或多个空白字符(空格、制表符等) # 0x - 匹配字面字符串 "0x" # ( - 开始捕获组 # [0-9a-fA-F] - 字符类,匹配十六进制字符(0-9, a-f, A-F) # + - 匹配前面的元素1次或多次 # ) - 结束捕获组 # # 匹配过程: # "Setting vref: 0x1A3" -> 匹配整个字符串 # 捕获组1: "1A3" ########################################################## vref_match = re.match(r'.*vref:\s*0x([0-9a-fA-F]+)', line) if vref_match: # 获取捕获组内容 hex_str = vref_match.group(1) # int()函数:字符串转整数 # 参数1:字符串 # 参数2:基数(16表示十六进制) current_vref = int(hex_str, 16) # 字典初始化 data[current_vref] = {} # 嵌套字典初始化 raw_data[current_vref] = {} pending_data = {} # 重置临时数据 current_offset = None continue # 跳过后续处理 ########################################################## # 正则表达式2:匹配偏移量行 # 模式:r'.*0x38c:\s*(?:0x)?([0-9a-fA-F]+)' # 目标示例: "Offset 0x38c: 0x25" 或 "0x38c: 25" # # 详细分解: # .* - 匹配任意字符0次或多次 # 0x38c: - 匹配字面字符串 "0x38c:" # \s* - 匹配0个或多个空白字符 # (?: - 开始非捕获组 # 0x - 匹配字面字符串 "0x" # )? - 非捕获组出现0次或1次 # ( - 开始捕获组 # [0-9a-fA-F]+ - 匹配1个或多个十六进制字符 # ) - 结束捕获组 # # 特殊说明: # (?:...) 是非捕获组,匹配但不捕获内容 # 用于处理可选前缀而不创建额外捕获组 ########################################################## offset_match = re.match(r'.*0x38c\s*:\s*(?:0x)?([0-9a-fA-F]+)', line) if offset_match and current_vref is not None: try: hex_str = offset_match.group(1) offset_value = int(hex_str, 16) # 计算偏移量:归一化点 - 读取值 current_offset = normalization_point - offset_value except ValueError: # 异常处理:打印警告信息 print(f"警告: 无法解析偏移量: {offset_match.group(1)}") current_offset = None continue ########################################################## # 正则表达式3:匹配最大值点 # 模式:r'.*dq(\d+)\s+max_(\w+)_point\s*:\s*(-?\d+)' # 目标示例: "dq5 max_read_point: 120" # # 详细分解: # .* - 匹配任意字符0次或多次 # dq - 匹配字面字符串 "dq" # (\d+) - 捕获组1:匹配1个或多个数字(DQ索引) # \s+ - 匹配1个或多个空白字符 # max_ - 匹配字面字符串 "max_" # (\w+) - 捕获组2:匹配1个或多个单词字符(方向:read/write) # _point - 匹配字面字符串 "point" # \s*:\s* - 匹配冒号前后任意空白 # (-?\d+) - 捕获组3:匹配可选负号后跟1个或多个数字 # # 捕获组说明: # 组1: DQ索引 (如 "5") # 组2: 方向 (如 "read") # 组3: 最大值 (如 "120") ########################################################## max_match = re.match(r'.*dq(\d+)\s*max_(\w+)_point\s*:\s*(-?\d+)', line) if max_match and current_vref is not None: # 提取捕获组内容 dq_index = int(max_match.group(1)) # 转换为整数 direction = max_match.group(2) # 字符串 max_val = int(max_match.group(3)) # 转换为整数 # 字典操作:检查键是否存在并初始化 if current_vref not in raw_data: # 字典设置默认值 raw_data[current_vref] = {} if dq_index not in raw_data[current_vref]: raw_data[current_vref][dq_index] = {} if direction not in raw_data[current_vref][dq_index]: # 嵌套字典初始化 raw_data[current_vref][dq_index][direction] = {'min': None, 'max': None} # 存储原始值(不应用偏移) raw_data[current_vref][dq_index][direction]['max'] = max_val # 只有读方向应用偏移 if direction == 'read' and current_offset is not None: # 应用偏移 max_val += current_offset # 存储到临时数据字典 key = (dq_index, direction) # 元组作为字典键 if key not in pending_data: pending_data[key] = {} pending_data[key]['max'] = max_val # 字典值也是字典 continue ########################################################## # 正则表达式4:匹配最小值点(结构类似最大值匹配) # 模式:r'.*dq(\d+)\s+min_(\w+)_point\s*:\s*(-?\d+)' # 目标示例: "dq5 min_read_point: 32" ########################################################## min_match = re.match(r'.*dq(\d+)\s*min_(\w+)_point\s*:\s*(-?\d+)', line) if min_match and current_vref is not None: dq_index = int(min_match.group(1)) direction = min_match.group(2) min_val = int(min_match.group(3)) key = (dq_index, direction) # 存储原始值(类似最大值处理) if current_vref not in raw_data: raw_data[current_vref] = {} if dq_index not in raw_data[current_vref]: raw_data[current_vref][dq_index] = {} if direction not in raw_data[current_vref][dq_index]: raw_data[current_vref][dq_index][direction] = {'min': None, 'max': None} raw_data[current_vref][dq_index][direction]['min'] = min_val # 只有读方向应用偏移 if direction == 'read' and current_offset is not None: min_val += current_offset # 更新临时数据 if key in pending_data: # 字典更新操作 pending_data[key]['min'] = min_val else: pending_data[key] = {'min': min_val} continue ########################################################## # 正则表达式5:匹配窗口行 # 模式:r'.*dq(\d+)\s+(\w+)_windows\s*:\s*(-?\d+)' # 目标示例: "dq5 read_windows: 88" # # 详细分解: # .* - 匹配任意字符0次或多次 # dq - 匹配字面字符串 "dq" # (\d+) - 捕获组1:匹配1个或多个数字(DQ索引) # \s+ - 匹配1个或多个空白字符 # (\w+) - 捕获组2:匹配1个或多个单词字符(方向) # _windows - 匹配字面字符串 "_windows" # \s*:\s* - 匹配冒号前后任意空白 # (-?\d+) - 捕获组3:匹配可选负号后跟1个或多个数字 ########################################################## win_match = re.match(r'.*dq(\d+)\s*(\w+)_windows\s*:\s*(-?\d+)', line) if win_match and current_vref is not None: dq_index = int(win_match.group(1)) direction = win_match.group(2) windows = int(win_match.group(3)) key = (dq_index, direction) # 检查是否已收集最小值和最大值 if key in pending_data and 'min' in pending_data[key] and 'max' in pending_data[key]: min_val = pending_data[key]['min'] max_val = pending_data[key]['max'] # 确定最大延迟值(读0x7F=127,写0xFF=255) max_delay = 0x7F if direction == 'read' else 0xFF # 确保值在有效范围内 min_val = max(0, min_val) # 最小值不小于0 max_val = min(max_delay, max_val) # 最大值不超过最大延迟 # 检查数据有效性 if min_val > max_val or windows < 0: result = None # 无效数据 else: # 计算窗口大小 window_size = max_val - min_val + 1 result = (min_val, max_val, window_size) # 存储到最终数据结构 if dq_index not in data[current_vref]: # 初始化嵌套字典 data[current_vref][dq_index] = {} data[current_vref][dq_index][direction] = result # 从临时数据中移除 del pending_data[key] # 删除字典键 # 返回解析结果 return data, raw_data def parse_tera_term_log(log_content, normalization_point): “”" 解析Tera Term生成的新日志格式 参数: log_content - 日志文件的内容(字符串) normalization_point - 归一化点(十六进制整数) “”" # 初始化数据结构 data = {} current_vref = None pending_data = {} current_offset = None raw_data = {} # 预处理:移除行首时间戳 (例如: [2025-07-31 21:26:46.357]) log_content = re.sub(r'$$\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\.\d{3}$$\s*', '', log_content) for line in log_content.split('\n'): line = line.strip() if not line: continue # 增强的VREF匹配 - 容忍乱码前缀 vref_match = re.search(r'vref\D*0x([0-9a-fA-F]{1,3})', line, re.IGNORECASE) if vref_match: try: hex_str = vref_match.group(1) current_vref = int(hex_str, 16) data[current_vref] = {} raw_data[current_vref] = {} pending_data = {} current_offset = None except ValueError: print(f"警告: 无法解析VREF值: {hex_str}") continue # 增强的偏移量匹配 offset_match = re.search(r'0x38c\D*([0-9a-fA-F]{2})', line, re.IGNORECASE) if offset_match and current_vref is not None: try: hex_str = offset_match.group(1) offset_value = int(hex_str, 16) current_offset = normalization_point - offset_value except (ValueError, TypeError): print(f"警告: 无法解析偏移量: {offset_match.group(1)}") current_offset = None continue # 增强的数据点匹配 - 容忍乱码和格式变化 # 匹配模式: dqX max_Y_point: value point_pattern = r'dq(\d+)\s*(max|min)\s*_(\w+)_point\s*[:=]\s*(-?\d+)' point_match = re.search(point_pattern, line, re.IGNORECASE) if point_match and current_vref is not None: try: dq_index = int(point_match.group(1)) point_type = point_match.group(2).lower() # 'max' or 'min' direction = point_match.group(3).lower() # 'read' or 'write' value = int(point_match.group(4)) # 初始化数据结构 if current_vref not in raw_data: raw_data[current_vref] = {} if dq_index not in raw_data[current_vref]: raw_data[current_vref][dq_index] = {} if direction not in raw_data[current_vref][dq_index]: raw_data[current_vref][dq_index][direction] = {'min': None, 'max': None} # 存储原始值 if point_type == 'max': raw_data[current_vref][dq_index][direction]['max'] = value else: # 'min' raw_data[current_vref][dq_index][direction]['min'] = value # 应用偏移(仅读方向) if direction == 'read' and current_offset is not None: value += current_offset # 存储到临时数据 key = (dq_index, direction) if key not in pending_data: pending_data[key] = {} pending_data[key][point_type] = value except (ValueError, IndexError) as e: print(f"解析数据点时出错: {line} -> {e}") continue # 增强的窗口匹配 win_pattern = r'dq(\d+)\s*(\w+)\s*_windows\s*[:=]\s*(-?\d+)' win_match = re.search(win_pattern, line, re.IGNORECASE) if win_match and current_vref is not None: try: dq_index = int(win_match.group(1)) direction = win_match.group(2).lower() windows = int(win_match.group(3)) key = (dq_index, direction) if key in pending_data and 'min' in pending_data[key] and 'max' in pending_data[key]: min_val = pending_data[key]['min'] max_val = pending_data[key]['max'] # 确定最大延迟值 max_delay = 0x7F if direction == 'read' else 0xFF # 确保值在有效范围内 min_val = max(0, min_val) max_val = min(max_delay, max_val) if min_val > max_val or windows < 0: result = None else: window_size = max_val - min_val + 1 result = (min_val, max_val, window_size) # 存储到最终数据结构 if dq_index not in data[current_vref]: data[current_vref][dq_index] = {} data[current_vref][dq_index][direction] = result # 从临时数据中移除 del pending_data[key] except (ValueError, KeyError) as e: print(f"解析窗口时出错: {line} -> {e}") return data, raw_data 眼图指标计算函数 - 算法详解(修改后) def calculate_eye_metrics(data, avddq, dq_index, direction): “”" 计算眼图的最大宽度、最大高度以及中心点 参数: data - 解析后的日志数据(字典结构) avddq - AVDDQ电压值(浮点数) dq_index - DQ索引(0-15,整数) direction - 方向(‘read’或’write’,字符串) 算法说明: 1. 遍历所有VREF值 2. 计算实际电压 = (vref / 0x1FF) * avddq 3. 获取当前DQ和方向的数据 4. 计算窗口大小(UI单位) 5. 确定最大眼宽(所有窗口中的最大值) 6. 计算最大眼高(连续电压范围的最大高度) 7. 计算眼图中心点(最大眼高和最大眼宽的交点) """ # 初始化变量 max_eye_width = 0.0 max_eye_height = 0.0 # 存储每个电压对应的窗口大小(用于计算眼高) voltage_windows = defaultdict(float) # 存储每个电压对应的延迟范围(用于计算眼宽) voltage_delay_ranges = {} # 存储每个延迟位置对应的电压范围(用于计算眼高) delay_voltage_ranges = defaultdict(list) # 确定最大延迟值(读0x7F=127,写0xFF=255) max_delay = 0x7F if direction == 'read' else 0xFF # 确定UI范围(读2UI,写4UI) ui_range = 2 if direction == 'read' else 4 # 遍历所有VREF值 for vref, dq_data in data.items(): # 计算实际电压 # 0x1FF = 511(9位最大值) voltage = (vref / 0x1FF) * avddq # 字典安全访问:get()方法 # 避免KeyError异常 dq_info = dq_data.get(dq_index, {}).get(direction) if dq_info is None: continue # 跳过无数据项 # 解包元组 min_point, max_point, window_size = dq_info # 重新计算窗口大小(确保正确性) window_size = max_point - min_point + 1 # 计算窗口大小(UI单位) window_ui = (window_size / max_delay) * ui_range # 更新最大眼宽 if window_ui > max_eye_width: max_eye_width = window_ui # 存储电压-窗口映射 voltage_windows[voltage] = window_ui # 存储电压-延迟范围映射(用于计算眼宽) voltage_delay_ranges[voltage] = (min_point, max_point) # 存储延迟位置对应的电压范围(用于计算眼高) for delay in range(min_point, max_point + 1): delay_voltage_ranges[delay].append(voltage) # 计算最大眼高(连续电压范围) # 步骤: # 1. 对电压排序 # 2. 遍历排序后的电压 # 3. 计算连续有效窗口的电压范围 sorted_voltages = sorted(voltage_windows.keys()) # 排序电压值 current_height = 0 # 当前连续高度 max_height = 0 # 最大高度 # 遍历排序后的电压(从第二个元素开始) for i in range(1, len(sorted_voltages)): # 计算电压差 voltage_diff = sorted_voltages[i] - sorted_voltages[i-1] # 检查相邻电压点是否都有有效窗口 # 字典键存在性检查 if sorted_voltages[i] in voltage_windows and sorted_voltages[i-1] in voltage_windows: current_height += voltage_diff if current_height > max_height: max_height = current_height else: current_height = 0 # 重置高度计数器 max_eye_height = max_height # 计算最大眼宽对应的延迟位置(新增) # 找到具有最大窗口的电压点 best_voltage = None max_window_ui = 0 for voltage, window_ui in voltage_windows.items(): if window_ui > max_window_ui: max_window_ui = window_ui best_voltage = voltage # 计算最大眼高对应的延迟位置(新增) # 找到具有最宽电压范围的延迟位置 best_delay = None max_voltage_range = 0 for delay, voltages in delay_voltage_ranges.items(): if voltages: min_v = min(voltages) max_v = max(voltages) voltage_range = max_v - min_v if voltage_range > max_voltage_range: max_voltage_range = voltage_range best_delay = delay # 计算眼图中心点 center_ui = None center_voltage = None if best_delay is not None and best_voltage is not None: # 将延迟转换为UI单位 center_ui = (best_delay / max_delay) * ui_range center_voltage = best_voltage # 返回计算结果 return max_eye_width, max_eye_height, center_ui, center_voltage, best_delay, best_voltage 眼图数据生成函数 - 详细解释算法 def generate_eye_diagram(data, avddq, ui_ps, dq_index, direction): “”" 生成眼图数据点 参数: data - 解析后的日志数据(字典) avddq - AVDDQ电压值(浮点数) ui_ps - 每个UI的时间(皮秒) dq_index - DQ索引(0-15,整数) direction - 方向(‘read’或’write’,字符串) 算法说明: 1. 遍历所有VREF值 2. 计算实际电压 = (vref / 0x1FF) * avddq 3. 遍历所有可能的延迟值 4. 将延迟值转换为UI单位 5. 根据数据有效性标记为通过点或失败点 """ pass_points = [] # 存储通过点(绿色) fail_points = [] # 存储失败点(红色) # 确定最大延迟值(读0x7F=127,写0xFF=255) max_delay = 0x7F if direction == 'read' else 0xFF # 确定UI范围(读2UI,写4UI) ui_range = 2 if direction == 'read' else 4 # 遍历所有VREF值 for vref, dq_data in data.items(): # 计算实际电压 voltage = (vref / 0x1FF) * avddq # 获取当前DQ和方向的数据 dq_info = dq_data.get(dq_index, {}).get(direction) # 遍历所有可能的延迟值 for delay in range(0, max_delay + 1): # 将延迟值转换为UI单位 ui_value = (delay / max_delay) * ui_range # 如果没有有效数据,标记为失败点 if dq_info is None: fail_points.append((ui_value, voltage)) else: # 解包元组 min_point, max_point, _ = dq_info # 检查当前延迟是否在有效范围内 if min_point <= delay <= max_point: pass_points.append((ui_value, voltage)) else: fail_points.append((ui_value, voltage)) return pass_points, fail_points 眼图数据生成函数 - 增加边界计算功能 def generate_eye_diagram(data, raw_data, avddq, ui_ps, dq_index, direction): “”" 生成眼图数据点和边界 参数: data - 解析后的日志数据(字典) raw_data - 原始数据(偏移前) avddq - AVDDQ电压值(浮点数) ui_ps - 每个UI的时间(皮秒) dq_index - DQ索引(0-15,整数) direction - 方向(‘read’或’write’,字符串) 返回: pass_points - 通过点列表 fail_points - 失败点列表 adjusted_boundary - 调整后PASS区域的边界点 raw_boundary - 原始数据PASS区域的边界点 """ pass_points = [] # 存储通过点(绿色) fail_points = [] # 存储失败点(红色) # 存储调整后和原始数据的边界点 adjusted_boundary = [] raw_boundary = [] # 确定最大延迟值(读0x7F=127,写0xFF=255) max_delay = 0x7F if direction == 'read' else 0xFF # 确定UI范围(读2UI,写4UI) ui_range = 2 if direction == 'read' else 4 # 存储每个电压对应的延迟范围 adjusted_ranges = {} raw_ranges = {} # 遍历所有VREF值 for vref, dq_data in data.items(): # 计算实际电压 voltage = (vref / 0x1FF) * avddq # 获取当前DQ和方向的数据(调整后) dq_info = dq_data.get(dq_index, {}).get(direction) # 获取原始数据 raw_info = raw_data.get(vref, {}).get(dq_index, {}).get(direction, {}) # 遍历所有可能的延迟值 for delay in range(0, max_delay + 1): # 将延迟值转换为UI单位 ui_value = (delay / max_delay) * ui_range # 处理调整后数据 if dq_info is None: fail_points.append((ui_value, voltage)) else: min_point, max_point, _ = dq_info if min_point <= delay <= max_point: pass_points.append((ui_value, voltage)) else: fail_points.append((ui_value, voltage)) # 收集调整后数据的边界点 if dq_info is not None: min_point, max_point, _ = dq_info min_ui = (min_point / max_delay) * ui_range max_ui = (max_point / max_delay) * ui_range adjusted_ranges[voltage] = (min_ui, max_ui) # 收集原始数据的边界点 if 'min' in raw_info and 'max' in raw_info: min_val = raw_info['min'] max_val = raw_info['max'] min_ui_raw = (min_val / max_delay) * ui_range max_ui_raw = (max_val / max_delay) * ui_range raw_ranges[voltage] = (min_ui_raw, max_ui_raw) # 生成调整后PASS区域的边界点(实线) if adjusted_ranges: # 按电压排序 sorted_voltages = sorted(adjusted_ranges.keys()) # 上边界(最大延迟) for voltage in sorted_voltages: _, max_ui = adjusted_ranges[voltage] adjusted_boundary.append((max_ui, voltage)) # 下边界(最小延迟,反向排序) for voltage in reversed(sorted_voltages): min_ui, _ = adjusted_ranges[voltage] adjusted_boundary.append((min_ui, voltage)) # 闭合图形 if adjusted_boundary: adjusted_boundary.append(adjusted_boundary[0]) # 生成原始数据PASS区域的边界点(虚线) if raw_ranges: # 按电压排序 sorted_voltages = sorted(raw_ranges.keys()) # 上边界(最大延迟) for voltage in sorted_voltages: _, max_ui = raw_ranges[voltage] raw_boundary.append((max_ui, voltage)) # 下边界(最小延迟,反向排序) for voltage in reversed(sorted_voltages): min_ui, _ = raw_ranges[voltage] raw_boundary.append((min_ui, voltage)) # 闭合图形 if raw_boundary: raw_boundary.append(raw_boundary[0]) return pass_points, fail_points, adjusted_boundary, raw_boundary 输出原始数据到新日志 - 文件操作详解 def export_raw_data(raw_data, normalization_point, log_path): “”" 输出原始数据到新日志文件(按DQ划分) 参数: raw_data - 原始数据(偏移前) normalization_point - 归一化点 log_path - 原始日志文件路径 文件操作详解: 1. 创建输出目录:os.makedirs() 2. 构建文件路径:os.path.join() 3. 写入文件:open()配合write() 4. 格式化输出:f-string """ # 获取当前时间戳 timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") # 获取日志文件名(不含扩展名) log_filename = os.path.basename(log_path) if '.' in log_filename: # rsplit() 从右边分割字符串,maxsplit=1表示只分割一次 log_name = log_filename.rsplit('.', 1)[0] else: log_name = log_filename # 创建输出目录 log_dir = os.path.dirname(log_path) or os.getcwd() # 获取目录或当前工作目录 output_dir = os.path.join(log_dir, "raw_data_export") # 创建输出目录路径 ########################################################## # os.makedirs() 创建目录(如果不存在) # exist_ok=True 表示目录已存在时不报错 ########################################################## os.makedirs(output_dir, exist_ok=True) # 创建输出文件路径 output_file = os.path.join(output_dir, f"{log_name}_raw_data.txt") # 写入原始数据 with open(output_file, 'w', encoding='utf-8') as f: # 写入标题信息 f.write("=" * 80 + "\n") f.write(f"DDR校准原始数据报告 (归一化点: 0x{normalization_point:X})\n") f.write(f"生成时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"原始日志: {log_path}\n") f.write("=" * 80 + "\n\n") # 按vref排序 sorted_vrefs = sorted(raw_data.keys()) for vref in sorted_vrefs: # 写入vref标题 f.write(f"VREF: 0x{vref:03X}\n") # :03X表示3位十六进制大写,不足补0 f.write("-" * 60 + "\n") # 按DQ索引排序 sorted_dq = sorted(raw_data[vref].keys()) for dq_index in sorted_dq: # 写入DQ标题 f.write(f" DQ{dq_index}:\n") # 处理读方向数据 if 'read' in raw_data[vref][dq_index]: rd = raw_data[vref][dq_index]['read'] f.write(f" 读方向:\n") f.write(f" 原始最小值: {rd['min']}\n") f.write(f" 原始最大值: {rd['max']}\n") # 计算并写入窗口大小 window_size = rd['max'] - rd['min'] + 1 f.write(f" 窗口大小: {window_size}\n") # 处理写方向数据 if 'write' in raw_data[vref][dq_index]: wr = raw_data[vref][dq_index]['write'] f.write(f" 写方向:\n") f.write(f" 原始最小值: {wr['min']}\n") f.write(f" 原始最大值: {wr['max']}\n") # 计算并写入窗口大小 window_size = wr['max'] - wr['min'] + 1 f.write(f" 窗口大小: {window_size}\n") f.write("\n") # DQ间空行 f.write("\n") # VREF间空行 print(f"原始数据已导出至: {output_file}") return output_file 眼图绘制函数 - 数据可视化详解(修改后) 眼图绘制函数 - 数据可视化详解(修改后) def plot_eye_diagrams(log_content, data_rate, avddq, log_path, normalization_point): “”" 绘制DDR眼图 参数: log_content - 日志内容 data_rate - 数据速率(Mbps) avddq - AVDDQ电压(V) log_path - 日志文件路径 normalization_point - 归一化点 “”" # 设置中文字体支持 plt.rcParams[‘font.sans-serif’] = [‘SimHei’, ‘Arial Unicode MS’, ‘Microsoft YaHei’, ‘WenQuanYi Micro Hei’] plt.rcParams[‘axes.unicode_minus’] = False # 计算UI时间(皮秒) ui_ps = (1 / (data_rate * 1e6)) * 1e12 # 解析日志文件 data, raw_data = parse_log_file(log_content, normalization_point) # 导出原始数据到新日志 raw_data_file = export_raw_data(raw_data, normalization_point, log_path) # 检查数据有效性 if not data: print("错误: 无法从日志中解析出有效数据") return None, None, None # 创建图表对象 fig_write, axes_write = plt.subplots(4, 4, figsize=(20, 20)) fig_read, axes_read = plt.subplots(4, 4, figsize=(20, 20)) # 设置标题 norm_title = f" (Normalized to 0x{normalization_point:X}, Raw Data: {os.path.basename(raw_data_file)})" fig_write.suptitle(f'DDR Write Eye Diagram (Data Rate: {data_rate} Mbps, UI: {ui_ps:.2f} ps){norm_title}', fontsize=18) fig_read.suptitle(f'DDR Read Eye Diagram (Data Rate: {data_rate} Mbps, UI: {ui_ps:.2f} ps){norm_title}', fontsize=18) # 展平坐标轴数组 axes_write = axes_write.flatten() axes_read = axes_read.flatten() # 创建图例元素 legend_elements = [ Line2D([0], [0], marker='o', color='w', label='Pass', markerfacecolor='green', markersize=10), Line2D([0], [0], marker='o', color='w', label='Fail', markerfacecolor='red', markersize=10), Line2D([0], [0], color='orange', linestyle='-', label='Post-Norm Boundary'), Line2D([0], [0], color='orange', linestyle='--', label='Pre-Norm Boundary') ] # 存储分组中心点数据(读眼图) group1_center_vrefs = [] # DQ0-DQ7 group1_center_delays = [] # DQ0-DQ7 group2_center_vrefs = [] # DQ8-DQ15 group2_center_delays = [] # DQ8-DQ15 # 遍历16个DQ通道 for dq_index in range(16): # 计算写眼图指标(不计算中心点) write_width, write_height, _, _, _, _ = calculate_eye_metrics(data, avddq, dq_index, 'write') # 计算读眼图指标和中心点 read_width, read_height, read_center_ui, read_center_voltage, read_center_delay, read_center_vref = calculate_eye_metrics( data, avddq, dq_index, 'read' ) # ================= 写眼图处理 ================= # 生成写眼图数据点和边界 write_pass, write_fail, write_adjusted_boundary, write_raw_boundary = generate_eye_diagram( data, raw_data, avddq, ui_ps, dq_index, 'write' ) # 绘制写眼图 if write_fail: x_fail, y_fail = zip(*write_fail) axes_write[dq_index].scatter(x_fail, y_fail, s=1, c='red', alpha=0.1, zorder=1) if write_pass: x_pass, y_pass = zip(*write_pass) axes_write[dq_index].scatter(x_pass, y_pass, s=1, c='green', alpha=0.5, zorder=2) # 添加写眼图标注(仅显示最大眼宽和眼高) write_text = f"Max Eye Width: {write_width:.3f} UI\nMax Eye Height: {write_height:.3f} V" axes_write[dq_index].annotate( write_text, xy=(0.98, 0.02), xycoords='axes fraction', fontsize=9, ha='right', va='bottom', bbox=dict(boxstyle='round', facecolor='white', alpha=0.8) ) # 设置写眼图轴属性 axes_write[dq_index].set_title(f'DQ{dq_index} Write Eye', fontsize=12) axes_write[dq_index].set_xlabel('Delay (UI)', fontsize=10) axes_write[dq_index].set_ylabel('Voltage (V)', fontsize=10) axes_write[dq_index].set_xlim(0, 4) # 写眼图0-4UI axes_write[dq_index].set_ylim(0, avddq) axes_write[dq_index].grid(True, linestyle='--', alpha=0.6) axes_write[dq_index].legend(handles=legend_elements, loc='upper right', fontsize=9) axes_write[dq_index].tick_params(axis='both', which='major', labelsize=9) # ================= 读眼图处理 ================= # 生成读眼图数据点和边界 read_pass, read_fail, read_adjusted_boundary, read_raw_boundary = generate_eye_diagram( data, raw_data, avddq, ui_ps, dq_index, 'read' ) # 绘制读眼图 if read_fail: x_fail, y_fail = zip(*read_fail) axes_read[dq_index].scatter(x_fail, y_fail, s=1, c='red', alpha=0.1, zorder=1) if read_pass: x_pass, y_pass = zip(*read_pass) axes_read[dq_index].scatter(x_pass, y_pass, s=1, c='green', alpha=0.5, zorder=2) # 绘制调整后边界(实线) if read_adjusted_boundary: x_adj, y_adj = zip(*read_adjusted_boundary) axes_read[dq_index].plot(x_adj, y_adj, 'orange', linewidth=1.5, alpha=0.8, zorder=3) # 绘制原始数据边界(虚线) if read_raw_boundary: x_raw, y_raw = zip(*read_raw_boundary) axes_read[dq_index].plot(x_raw, y_raw, 'orange', linewidth=1.0, alpha=0.7, zorder=4) # 添加读眼图标注 read_text = f"Max Eye Width: {read_width:.3f} UI\nMax Eye Height: {read_height:.3f} V" axes_read[dq_index].annotate( read_text, xy=(0.98, 0.02), xycoords='axes fraction', fontsize=9, ha='right', va='bottom', bbox=dict(boxstyle='round', facecolor='white', alpha=0.8) ) # 设置读眼图轴属性 axes_read[dq_index].set_title(f'DQ{dq_index} Read Eye', fontsize=12) axes_read[dq_index].set_xlabel('Delay (UI)', fontsize=10) axes_read[dq_index].set_ylabel('Voltage (V)', fontsize=10) axes_read[dq_index].set_xlim(0, 2) # 读眼图0-2UI axes_read[dq_index].set_ylim(0, avddq) axes_read[dq_index].grid(True, linestyle='--', alpha=0.6) axes_read[dq_index].legend(handles=legend_elements, loc='upper right', fontsize=9) axes_read[dq_index].tick_params(axis='both', which='major', labelsize=9) # 绘制读眼图中心点和辅助线 if read_center_ui is not None and read_center_voltage is not None: # 绘制中心点 axes_read[dq_index].scatter( [read_center_ui], [read_center_voltage], s=100, marker='*', c='yellow', edgecolors='black', zorder=10 ) # 计算原始Vref值 original_vref = int(round((read_center_voltage * 0x1FF) / avddq)) # 添加中心点标注 center_text = f"Center: ({read_center_ui:.3f} UI, {read_center_voltage:.3f} V)\n" \ f"Raw: Vref=0x{original_vref:X}, Delay={read_center_delay}" axes_read[dq_index].annotate( center_text, xy=(read_center_ui, read_center_voltage), xytext=(read_center_ui + 0.1, read_center_voltage + 0.05), arrowprops=dict(facecolor='black', shrink=0.05), fontsize=8, ha='left' ) # 绘制辅助线:最大眼宽竖线(蓝色虚线) axes_read[dq_index].axvline( x=read_center_ui, color='blue', linestyle='--', alpha=0.7, label=f'Max Width Line' ) # 绘制辅助线:最大眼高横线(蓝色虚线) axes_read[dq_index].axhline( y=read_center_voltage, color='blue', linestyle='--', alpha=0.7, label=f'Max Height Line' ) # 添加辅助线图例 line_legend = [ Line2D([0], [0], color='blue', linestyle='--', label='Max Width Line'), Line2D([0], [0], color='blue', linestyle='--', label='Max Height Line') ] axes_read[dq_index].legend(handles=legend_elements + line_legend, loc='upper right', fontsize=9) # 根据DQ索引分组存储中心点数据 if dq_index < 8: group1_center_vrefs.append(original_vref) group1_center_delays.append(read_center_delay) else: group2_center_vrefs.append(original_vref) group2_center_delays.append(read_center_delay) # 计算分组统计值 def calculate_group_stats(vrefs, delays): """计算一组中心点的统计值""" if not vrefs: return None, None, None, None # 计算Vref平均值和中位数 avg_vref = sum(vrefs) / len(vrefs) sorted_vrefs = sorted(vrefs) mid = len(sorted_vrefs) // 2 if len(sorted_vrefs) % 2 == 0: median_vref = (sorted_vrefs[mid-1] + sorted_vrefs[mid]) / 2 else: median_vref = sorted_vrefs[mid] # 计算延迟平均值和中位数 avg_delay = sum(delays) / len(delays) sorted_delays = sorted(delays) mid = len(sorted_delays) // 2 if len(sorted_delays) % 2 == 0: median_delay = (sorted_delays[mid-1] + sorted_delays[mid]) / 2 else: median_delay = sorted_delays[mid] return avg_vref, median_vref, avg_delay, median_delay # 计算第一组(DQ0-DQ7)的统计值 stats1 = calculate_group_stats(group1_center_vrefs, group1_center_delays) # 计算第二组(DQ8-DQ15)的统计值 stats2 = calculate_group_stats(group2_center_vrefs, group2_center_delays) # 在图像顶部添加分组汇总信息(两行) if stats1[0] is not None: avg_vref1, median_vref1, avg_delay1, median_delay1 = stats1 # 第一组文本(DQ0-DQ7) summary_text1 = f"DQ0-DQ7 Center Points: " \ f"Avg Vref=0x{int(round(avg_vref1)):X}, " \ f"Median Vref=0x{int(median_vref1):X}, " \ f"Avg Delay={avg_delay1:.1f}, " \ f"Median Delay={median_delay1:.1f}" # 位置:0.95(顶部) fig_read.text(0.5, 0.95, summary_text1, ha='center', fontsize=12, bbox=dict(facecolor='white', alpha=0.8)) if stats2[0] is not None: avg_vref2, median_vref2, avg_delay2, median_delay2 = stats2 # 第二组文本(DQ8-DQ15) summary_text2 = f"DQ8-DQ15 Center Points: " \ f"Avg Vref=0x{int(round(avg_vref2)):X}, " \ f"Median Vref=0x{int(median_vref2):X}, " \ f"Avg Delay={avg_delay2:.1f}, " \ f"Median Delay={median_delay2:.1f}" # 位置:0.92(在上一行下方) fig_read.text(0.5, 0.92, summary_text2, ha='center', fontsize=12, bbox=dict(facecolor='white', alpha=0.8)) # 调整布局 fig_write.tight_layout(rect=[0, 0, 1, 0.96]) # 为读眼图顶部留出空间 fig_read.tight_layout(rect=[0, 0, 1, 0.90]) # 文件路径处理(添加时间戳) log_dir = os.path.dirname(log_path) or os.getcwd() timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") log_filename = os.path.basename(log_path) log_name = log_filename.rsplit('.', 1)[0] if '.' in log_filename else log_filename # 构建输出文件路径(写眼图只加时间戳) write_filename = os.path.join(log_dir, f"{log_name}_ddr_write_eye_{timestamp}.png") # 构建读眼图文件名(包含两组VREF平均值) group1_avg_vref = int(round(stats1[0])) if stats1[0] is not None else 0 group2_avg_vref = int(round(stats2[0])) if stats2[0] is not None else 0 read_filename = os.path.join( log_dir, f"{log_name}_ddr_read_eye_{timestamp}_G1Vref_0x{group1_avg_vref:X}_G2Vref_0x{group2_avg_vref:X}.png" ) # 保存图像 - 修复变量名拼写错误 fig_write.savefig(write_filename, dpi=300, bbox_inches='tight') fig_read.savefig(read_filename, dpi=300, bbox_inches='tight') # 关闭图像释放内存 plt.close(fig_write) plt.close(fig_read) # 打印结果 print(f"写眼图已保存至: {write_filename}") print(f"读眼图已保存至: {read_filename}") return write_filename, read_filename, raw_data_file 主函数 - 程序入口点详解 def main(): “”" 主函数,程序入口点 功能: - 获取用户输入 - 读取日志文件 - 解析数据 - 生成眼图 - 导出结果 用户交互详解: 1. 使用input()获取用户输入 2. 使用循环处理无效输入 3. 使用try-except捕获异常 """ # 打印欢迎信息 print("=" * 50) print("DDR眼图生成器(带原始数据导出)") print("=" * 50) # 用户输入DataRate(带异常处理) while True: try: data_rate = float(input("请输入DataRate (Mbps/Pin): ")) break except ValueError: print("错误: 请输入有效的数字") # 用户输入AVDDQ电压(带异常处理) while True: try: avddq = float(input("请输入AVDDQ电压值 (V): ")) break except ValueError: print("错误: 请输入有效的数字") # 归一化点输入处理(带错误检查) while True: norm_input = input("请输入归一化点(十六进制值,如0x40或40): ").strip() if not norm_input: print("错误: 输入不能为空,请重新输入") continue try: # 处理十六进制前缀 if norm_input.startswith(("0x", "0X")): hex_str = norm_input[2:] else: hex_str = norm_input # 字符串转整数(16进制) normalization_point = int(hex_str, 16) break except ValueError: print(f"错误: '{norm_input}' 不是有效的十六进制数,请重新输入") # 日志文件路径输入(带文件存在检查) while True: log_path = input("请输入日志文件路径: ").strip() # 检查文件是否存在 # os.path.exists() 判断路径是否存在 if not os.path.exists(log_path): print(f"错误: 文件 '{log_path}' 不存在,请重新输入") else: # 获取绝对路径 log_path = os.path.abspath(log_path) break # 读取文件内容 log_content = robust_read_file(log_path) if log_content is None: print("无法读取日志文件") return # 尝试生成眼图(带异常处理) try: # 调用眼图生成函数(返回三个值) write_file, read_file, raw_data_file = plot_eye_diagrams( log_content, data_rate, avddq, log_path, normalization_point ) print("\n眼图生成成功!") print(f"原始数据文件: {raw_data_file}") except Exception as e: # 捕获所有异常并打印错误信息 print(f"眼图生成失败: {e}") # 异常对象:e.args 获取异常参数 print(f"错误详情: {e.args}") Python特殊检查 - 模块执行控制 if name == “main”: “”" name 是Python的内置变量 当脚本直接运行时,name 等于 “main” 当脚本被导入时,name 等于模块名 这种结构允许: 1. 直接运行脚本时执行测试代码 2. 作为模块导入时不执行测试代码 """ main() # 调用主函数
最新发布
08-02
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

袁袁袁袁满

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值