# 振动数据接入(CMS / TCM)· v0.1 > 日期: 2026-09-12 · 适用: `<安装目录>`(安装目录) > 本文回答: ①振动侧的现场件长什么样、放哪;②谁把它变成系统能用的东西;③改了哪些代码、怎么验收; > ④哪些还做不到(如实列,不假装)。 > 相关: `docs\数据目录结构与落位约定_v0.2.md`(§2b 落位速查)· `docs\重算操作手册_v0.1.md`(一键重算) --- ## 1. 用户令与现场件盘点 **用户令原文**(路径按本包文档约定写成占位符): 「数据层里的「CMS 振动评估报告」应遵循 `<安装目录>\data\raw\如东\windcms`、「振动线 handoff」应遵循 `<安装目录>\data\raw\如东\m5_cms_tcm` 存放; 要求 1. 修改 `<安装目录>` 下的观澜系统, 支持振动数据参与系统运行、重算等; 2. 自 `<现场包目录>` 提取相应振动数据存放至上述目录」;随后用户把 `CMS_RuDong_CGN_202603-04.zip` 放进现场包 —— **那正是此前缺的原始件**。 现场包(`<现场包目录>`)里的振动相关件共三类: | # | 件 | 形态 | 能不能算 | |---|---|---|---| | ① | `CMS_RuDong_CGN_202603-04.zip`(38.8 GB 压缩 / 150 GB 解压 / 25,679 件) | Brande TCM Enterprise 导出: `measurement\<年>\<月>\\__decode.json`;每件是一次 API 响应 `{body:{body:{"<时间戳>":[{"Record":{…}}]}}}` | **能**(索引/谱/报告全部由它算) | | ② | 12 份月度「振动分析报告(用印版)」PDF(2025-06…2026-05) | **纯扫描件** | **不能** | | ③ | 上海电气 2026年07月报告 docx(4.87 MB)· 大生科技传动链报告 docx(20.81 MB) | Word, 有文本层 | 能(逐台判级转录) | **②为什么不能算(实测, 不是推测)**: 用 pypdf 打开 2026年5月那份 → `len(pages)=12`, 每页 `len(images)=1`, `page.extract_text()` 长度 **0**(12 页全 0)。字节面也印证: `/Font` 0 处、 `FlateDecode` 0 处、`/Image` 24 处、`DCTDecode` 12 处 = 每页一张 JPEG。 ⇒ 没有 OCR 就取不出任何数值。**处理方式: 只登记归档**(`_扫描件清单.json` 记 sha256/大小/无文本层), 不参与判级、不假装读过。要它们的数值只有两条路: 现场给电子件(docx/xlsx),或上 OCR(本包不装)。 **顺带核过的一条旧判断**: v0.2.0 初版把「`(8)…\振动分析报告\`」列在"故意不落"里,理由是 "成品牌报告而非可再加工的测量数据, 且 windcms 要的测点索引包里没有"。**该判断在 2026-09-12 被推翻** (①到货后索引源件具备),故改为落位。现场包里的**同件副本**(根目录 `…2026年5月…(2).pdf`、 嵌套包 `如东海上振动报告11份.zip`)不重复落 —— 落三遍会得到 3 份同名报告。 --- ## 2. 落位约定 ``` data\raw\如东\ ├─ windcms\ ← 数据层「CMS 振动评估报告」 │ ├─ CMS_RuDong_CGN_202603-04\ │ │ └─ measurement\2026\{03,04}\WTGxx\*_decode.json 25,679 件 / 150 GB │ └─ 厂家报告\上海电气_月度\ │ ├─ 中广核如东海上风电场2025年06月…2026年05月振动分析报告用印版.pdf 12 件(扫描件) │ ├─ 中广核如东海上风电场2026年07月振动分析报告_上海电气.docx │ └─ _扫描件清单.json └─ m5_cms_tcm\ ← 数据层「振动线 handoff」 └─ 厂家报告\ └─ 中广核如东海上风电场传动链振动分析报告_大生科技_20260311(1).docx ``` - 两个目录名已写进 `src\windscada\config.py` 的 `STATION_SUBDIRS`(扫描/落位/维护页从此认它们; 改目录名 = 换接口,要同步维护页与本文)。 - 原始导出**保留包内 `measurement\` 这一层**(来源可追溯);摄入按 `rglob` 找 `*_decode.json`, 套不套这层都能吃。 - 若现场给出 `handoff_vibration_v2.json` / `component_history.json` **正本**,放 `m5_cms_tcm\` 下即被优先采用 (包内那两份目前仍是随包快照 —— 振动线分支的产物没随包,见 §7)。 - 落位命令(映射表可复核、可重跑,同尺寸文件自动跳过): ``` .venv\Scripts\python.exe scripts\place_raw_data.py --src <现场包目录> --scope vib --dry-run .venv\Scripts\python.exe scripts\place_raw_data.py --src <现场包目录> --scope vib ``` --- ## 3. 摄入链(谁把原始件变成系统能用的东西) ``` data/raw/如东/windcms/…/*_decode.json │ scripts/rudong_tcm_index.py → outputs/rudong/m5_cms_tcm/windows/<窗>/index.parquet (54 列) │ scripts/rudong_tcm_spectra.py → outputs/rudong/m5_cms_tcm/windows/<窗>/spectra/*.npz │ + <窗>/spectra_meta.parquet(谱库目录内另存一份, 兼顾两种读取路径) │ [可选] scripts/windcms.py report / kb → outputs/rudong/windcms/*(★默认不跑, 见 §3b) ▼ data/raw/如东/{windcms,m5_cms_tcm}/…/*.docx │ scripts/vib_reports_build.py → outputs/rudong/windcms/厂家报告提取_<报告期>.json │ + 报告_CMS振动状态评估报告_<报告期>.md(与自产报告同构) │ + outputs/rudong/m5_cms_tcm/报告_TCM传动链振动分析_<报告期>.md ``` **一键**: `scripts/vib_raw_build.py`(索引→谱;`--skip-spectra` 可关;`--with-report` 才跑报告/知识库)。 它也写 `outputs/rudong/m5_cms_tcm/vib_raw_manifest.json`(源件路径、窗名、行数、谱数、时间窗、 各步耗时/rc,以及 `missing_chain`)。 ### 3b. ★ 为什么"重生成 CMS 报告"默认不跑(2026-09-12 实测) 跑一次 `scripts/windcms.py report` 在本包**会让产物变差**: | 件 | 随包快照 | 本包重生成后 | 变化 | |---|---|---|---| | `windcms/report.md` | 16,857 B(含逐台融合级表 + L4 过闸谱线) | **467 B**(融合级表空、L4 写"无") | **−97.2%** | | `windcms/overview.html` | 646,218 B | 548,466 B | −15.1% | | `windcms/index_eng.html` | 443,841 B | 408,112 B | −8.0% | | `windcms/index.html` | 389,997 B | 382,544 B | −1.9%(逐台页仍 38 个) | 根因不在数据,在**缺件**:`src/windcms/data.py::load_model()` 要读 `m5/model_run_l6.parquet` 与 `m5/fusion_38.csv`,而这两件属六层链的 `model_run` / `fusion` 两步 —— 那四步脚本没随包(§7),产物也不在。 于是"重生成"= 用残缺输入覆盖完整快照。 **处置**: 摄入默认只做索引/谱(加性、不覆盖任何随包件);报告/知识库改为 `--with-report` 显式开启, 且要求先备份 `outputs/<场>/windcms/`。本次实测后已把 `outputs/rudong/windcms` **逐字节还原**为随包快照 (56 件,哈希比对 0 差异),只保留厂家报告转录那两件。六层链补齐后这个默认值应当翻过来。 **来源登记**: 摄入与转录产出的每一件都由构建脚本自登记进 `outputs/<场>/_derived_manifest.json` (`src/derived_manifest.py`),`_provenance.json` 生成时据此把它们记成 `raw-derived` —— 避免"新造的件因不在随包快照里而整条不进台账"。 **窗名口径**: `wMMDD` = **数据起始日**(与既有 `w0127`/`w0707`/`w0811` 同口径)。 `src\windcms\pipeline.py::window_year_gate` 会拦"窗名无年份 + 数据过老"的误用(w1226 事故的护栏)。 重复摄入同一批数据时,已存在的窗自动改名为 `<窗>_reimport_<时分>`,而 `data.EXCLUDE_DEFAULT` 把带 `_reimport` 的窗排除在生产集外 —— **重复摄入不会污染分析集**。 **消费者(无需改一行代码,窗是自动发现的)**: | 消费者 | 拿什么 | |---|---| | `src\windcms\data.py::windows()` | 扫 `m5\windows\w????\index.parquet` → 新窗进分析集 | | `src\windcms\data.py::load_scalars()` | 各窗标量(`ds_size==1` 行)拼接 → CMS 报告/页面 | | `src\windcms\data.py::spectrum()` | `spectra_meta.parquet` + `npz['values'][shard_row]` → **谱图能取到** | | `src\windscada\taxonomy.py` / `subsys\fusion.py::windcms_grades()` | 最新 `报告_CMS振动状态评估报告_*.md` 的 `## 附录 A` 表 → 设备状态转录 | > ★ `spectra` 这一环此前是**死的**: 包内 `m5\spectra\`、`m5\windows\` 两个目录整个不在, > `data.spectrum()` 只能返回 None。本次摄入让谱图第一次有数据可画。 --- ## 4. 格式契约与对拍(改这块必看) 摄入产物与**包内既有产物**是同构关系,基准就是包内那份 `outputs\rudong\m5_cms_tcm\tcm_index.parquet` (330,308 行 × 54 列,2026-01-27~02-03 窗,同一摄取逻辑的产物): | 检查 | 结果 | |---|---| | 列名与列序 | **完全一致**(54 列,逐字抄自基准,含列序) | | dtype | 差异 3 列: `rec_i`(float64/int64)、`ds_dim`(float64/str)、`parse_error`(str/object)—— 消费者按列名取数,不受影响 | | 取值(FFT 行) | `x_offset=0.0` `x_delta=0.9375` `x_unit='Hz'` `y_unit='m/s²'` 与基准同行**逐格一致** | | 内部一致性 | `ds_size == lines+1` 比例 **1.000** | **TCM 导出里踩过的两个坑(写在这里防复发)**: 1. **X/Y 轴四件在 `Measurement.DataSets` 层,不在 `DataSet` 里**。`Size`/`Dimension`/`Values` 在 `DataSets.DataSet`,而 `X-axisOffset`/`X-axisDelta`/`X-axisUnit`/`Y-axisUnit` 在**上一层**。 第一版两层取错 → 四列整列为空,内部一致性检查算出 0.000(本该 1.000)才暴露。**先看一致性数再信列**。 2. `DataSets.DataSet.Values` 是**空格分隔的数值字符串**(不是数组)。谱: `Size=Lines+1`; `X-axisDelta = 带宽/Lines`(例: 6000/6400 = 0.9375)。标量: `Size==1` 时 `Values` 本身就是标量值。 另有少数测量(`FFT_250/2000/10000_Tr`, `Lines=400`)的 `x_delta = 带宽/800` = 半带宽口径 —— **源数据自己的约定**,摄入原样转录,不做"修正"。 --- ## 5. 厂商报告的转录纪律 `scripts\vib_reports_build.py` 只做**转录**,不做判级: - **合并单元格必须按 `tc` 恒等还原**:python-docx 对合并区会把同一个 `tc` 在相邻列/行重复给出, 照抄会把 `优秀 | 优秀 | 优秀` 压成一格(第一版手工 dump 就吃了这个亏: 表里 `WTG-01` 行显示成 `WTG-01 | 优秀 | 无`,实际是 主轴承=优秀 齿轮箱=优秀 发电机=优秀 结论=无)。规则: 同行 `tc` 与左邻 相同 = 横向合并(沿用左值);与上行同列相同 = 纵向合并(沿用上值)。 - **用厂家自己的总览句当校验和**:2026年07月报告原文"…优秀机组34台,良好3台,预警0台,无数据1台, 测点异常1台"。转录结果 `{优秀34, 良好3, 不可判1}` —— **逐项一致**("无数据"记 `不可判`: 没测到 ≠ 正常)。 - **厂家没给的不猜**:报告按「主轴承/齿轮箱/发电机」给级(**不分前后**),转录表里主轴承前/后填同一值并注明; 「综合」= 三项**取严**(危险>报警>良好>优秀>不可判)的聚合规则结果,必须写明"这是我们的规则不是厂家判级"; 「融合级/CMS 红黄/行动等级建议」厂家未给 → 写 `—`。 - **两条轴不混**:厂商报告转录的 md 与观澜自产报告同构(消费端不必改代码),报告期用**文件名月份取月末**, 于是它不会顶掉更新的自产报告;随包自产报告缺失时它就是最新版(从零重算的机器上正好用得上)。 大生科技那份只写 `报告_TCM传动链振动分析_*.md`,**不冒充 handoff 判级**。 - **口径不同别直接比**:厂家四级(优秀/良好/预警/报警)+「数据异常」≠ 观澜五级。实测两者对 38 台 有 33 台结论不同(厂家 2026-07 vs 观澜 2026-08)—— 来源、日期、判据都不同,属预期,不是谁错。 --- ## 6. 验收(命令 + 实测数字) ``` .venv\Scripts\python.exe scripts\place_raw_data.py --src <现场包目录> --scope vib --dry-run # 先看计划 .venv\Scripts\python.exe scripts\place_raw_data.py --src <现场包目录> --scope vib # 落位 (150 GB) .venv\Scripts\python.exe scripts\vib_raw_build.py --jobs 10 # 摄入 (索引+谱) .venv\Scripts\python.exe scripts\vib_reports_build.py --dump # 只打印转录, 人工核对 .venv\Scripts\python.exe scripts\vib_reports_build.py # 写产物 .venv\Scripts\python.exe scripts\scan_stations.py # 两个新目录是否被认到 ``` **落位**(实测): `windcms\CMS_RuDong_CGN_202603-04` 25,679 件 150.1 GB + `windcms\厂家报告\上海电气_月度` 13 件 51.4 MB + `m5_cms_tcm\厂家报告` 1 件 20.8 MB = **25,693 件 / 150.2 GB** (`rc=0`;余量要求: `F:` 需 ≥ 165 GB)。重跑幂等(同尺寸跳过;实测第二次跳过 2 件已存在的 docx)。 **摄入**(实测, 10 并行): 索引步 **392.8 s** → `windows\w0316\index.parquet` **2,066,686 行 × 54 列** (标量行 861,917 · 谱行 1,204,769;38 台;时间 2026-03-16 17:27:22 → 2026-04-21 10:18:33); 谱步 **477.2 s** → **420,742 条谱** / **1,712 个 npz 分片**(只转 `FFT_`,故小于"谱行"数; `Size>1` 行里另有 `Time_*` 波形 78 万行未转)。窗体合计 **3.27 GB**。全链 **875 s(15 分钟)**。 **消费端**(实测, 改代码前后都是同一套消费者): | 环节 | 实测结果 | |---|---| | `data.windows()` | 发现 `['w0127', 'w0316']` —— 新窗自动进分析集 | | `data.load_scalars()` | 193,902 行 × 11 列(w0316 贡献 168,880;7 个关键标量齐) | | `data.spectra_meta()` | 420,742 行 × 18 列,8 个测点全 | | `data.spectrum()` | **✅ 取到真实谱**: `WTG01 / Gear_IMS / FFT_6000_Tr` → 6,401 点,x 0→6000 Hz,y 单位 m/s²(此前 `m5/spectra` 缺失 ⇒ 只能返回 None) | | 索引列契约 | `data._read_index()` 要的 10 列齐;无缺列 | **厂商报告转录**: 上海电气 2026-07 → 38 台,取严 `{优秀34, 良好3, 不可判1}` 与报告原文总览句 `优秀34/良好3/预警0/无数据1/测点异常1` 逐项一致;大生科技 2026-03-11 → 38 台 `{优秀29, 良好7, 预警1, 数据异常1}`(与其正文"WTG02 未取到数据"一致); 消费端解析器(取 `## 附录 A` 后 `| WTG` 行第 6 列)读到 38 台不缺台。 **来源台账**(`outputs/rudong/_provenance.json`): 改造前 **17 raw-derived / 572 shipped**; 本次摄入+转录后 **1,740 / 571**(`m5_cms_tcm` 由 0/90 变 1,718/90、`windcms` 由 0/56 变 2/54)。 逐件登记由 `src/derived_manifest.py` 承载(谁算的谁登记),`products_restore_missing.py` 读它; 已存在的窗若漏登记,用 `python scripts/vib_raw_build.py --register-only` 幂等补登记。 **页面侧**(`http://127.0.0.1:28084/detail/v2#tab=system` 的数据层表,接口 `/detail/api/maint_survey`): 两行位置已改为 `<安装目录>\data\raw\如东\windcms\` 与 `<安装目录>\data\raw\如东\m5_cms_tcm\`。 ★ 组件服务**启动时会把这张表烘进缓存**,改完 `src/ontology/maintenance.py` 必须重启组件才生效 (本次用运营台同一套脚本: `_ops_stop_keep_gateway.py` + `_ops_start_and_open.py --no-open`)。 **其余闸门**: 全库 183 个 py 在 `-W error::SyntaxWarning` 下编译 0 失败;本体审计 `rc=0`; `check_transferable.py` 命中数 350 → **344**(`data\raw` 已排除出文本扫描;我的产出 0 处, manifest 里的源件路径已改为相对安装根)。 ## 7. 仍然做不到的(如实列) - **六层链四步未随包**: `rudong_tcm_oem_scan.py` / `rudong_line_energy_share.py` / `rudong_model_run.py` / `rudong_fusion_run.py`(原在振动线分支 `claude/vibration-data-diagnosis-32b69e`)。 因此扫描线/能量占比/模型层/融合层那几类 parquet(`oem_frequency_scan`、`gear_freq_scan`、 `blade_1p_*`、`model_run_l6`、`fusion_38.csv` 等)**仍走包内随包快照**,不由 `data\raw` 重算; 连带的后果是 **CMS 报告/页面不能重生成**(§3b 实测 −97%)。`vib_raw_manifest.json` 的 `missing_chain` 字段如实列着这四项;缺口台账里对应 **B5**。 - **扫描件 PDF 的数值**取不出(无文本层, 本包不带 OCR)。 - `handoff_vibration_v2.json` / `component_history.json` 曾是**随包快照**(含人工裁决、校准更新与开放项)。 **2026-09-19 已补生成端**(用户令「所有的计算均要形成观澜的源代码」): `scripts/rudong_fusion_handoff.py` (融合面,报告状态等级 + fusion_38 + L6 过闸线 → 同结构件)与 `scripts/component_history_build.py` (逐窗标量 x_self → 在升/换件闭环;本机只有 1 个导出色窗 ⇒ 在升/闭环**如实为空**并把数据边界写进 `meta.★数据边界`,绝不用"看起来像"的常数填表)。现场给正本时**正本优先**(生成端不覆盖正本)。 - 谱库的磁盘代价:整窗全转(`--meas ALL`)会显著吃盘(含 `Time_*` 波形, 单点 65,536/200,000); 默认只转 `FFT_` 谱(本轮 42 万条谱 = 3.2 GB)。 - 一个可选的后续:把纯 Python 的 `pypdf` 轮子并入 `wheels\`,让 `vib_reports_build.py` 能**离线** 逐件核验扫描件页数/有无文本层(本轮是靠临时装的 pypdf 抽检一件得出的结论,清单里记了这一点)。