简介:这份PPT资料聚焦基于IEC 61970标准的新一代调度自动化系统设计,面向电力系统自动化方向的技术人员、系统集成工程师及自动化相关专业师生,帮助理解异构系统集成、标准化建模与调度信息共享的整体思路。压缩包共1个pptx文件,约3.87MB,以图文并茂的幻灯片形式层层展开,便于课堂讲解、技术汇报与自学梳理。当前已有80人学习浏览。内容围绕组件模型、公用信息模型CIM与组件接口规范CIS三条主线,覆盖SCADA、AGC/AVC、网络分析、DTS等应用软件与支撑平台的协作关系,并对比内标准方式与外标准方式的优缺点;同时以OPEN-2000E SCADA/EMS为例,介绍CORBA集成总线、分布式面向对象架构、跨平台部署,以及紧密耦合、中等耦合、松散耦合三类集成方案,还涉及基于CIM的数据库定义原则与组件化按需裁剪思路,适合用于搭建知识框架、撰写方案或作为技术培训参考。
1. 一次模型对不上的联调,暴露了 IEC 61970 的真正价值
做过主站联调的人大多碰过这样的场面:两套系统各自跑得好好的,模型一导过去,开关挂到了错误的电压等级上,量测点全成了孤岛。问题不在代码,在于两边对「设备」这件事的定义根本不一样。这份以 PPT 形式流传的《新一代调度自动化系统的设计》,讲的正是这件事的解法——用组件模型约束体系结构,用公用信息模型 CIM 统一数据语义,用组件接口规范 CIS 统一访问方式。它把原本各自封装、各自私有格式的 SCADA、AGC/AVC、网络分析、DTS 拆成组件,挂到 CORBA 集成总线上,再由 OPEN-2000E 这类支撑平台按需裁剪组合。做调度自动化系统集成、EMS 二次开发、电力信息模型的人,值得把这套思路从建模一直捋到接口压测。
2. CIM 建模落地:从一次接线图到 RDF/XML 模型文件
调度自动化的所有集成问题,追到根上都是模型问题。CIM 不是一张设备清单,而是一套 UML 类模型加上它的 RDF/XML 序列化规则。真正决定联调成败的细节有三个:设备挂在哪个容器下、导电关系靠什么串起来、对象的 mRID 怎么生成。这三件事在私有模型里往往拍脑袋定,到了跨系统交换时才暴露出无法对齐。
2.1 CIM 的包划分决定了模型文件的命名空间
CIM 把电力系统对象切成若干包,每个包对应一块业务域。包划分的意义不只是分类,更是命名空间隔离——标准包的类语义不能改,自定义信息必须另找地方放。
| CIM 包 | 承载的典型类 | 对应业务域 |
|---|---|---|
| Core | PowerSystemResource、Equipment、Substation | 公共基类与厂站容器 |
| Wires | ACLineSegment、Breaker、PowerTransformer | 一次设备与电气连接 |
| Topology | TopologicalNode、ConnectivityNode | 节点-开关模型 |
| Meas | Measurement、Analog、Discrete | 量测与限值 |
| SCADA | RemoteUnit、CommunicationLink | 远动通道与终端 |
| Generation | GeneratingUnit、HydroUnit | 机组与发电 |
| Protection | ProtectedSwitch、ProtectionEquipment | 继电保护 |
| LoadModel | ConformLoad、NonConformLoad | 负荷建模 |
拿到一份厂家模型文件,先看根元素的 xmlns 指到哪个版本,再看设备类是否都落在 Wires 和 Core 里。如果发现自定义字段被直接塞进标准类属性,基本可以判断这份模型跨系统交换时会出问题。
2.2 双绕组变压器的 CIM/XML 片段怎么写
最容易被写错的不是变压器,而是绕组和端子。变压器本体只表达「这里有一台变压器」,电气特性落在 TransformerWinding 上,导电关系落在 Terminal 上。
<!-- 命名空间决定类定义来自哪个 CIM 版本,跨系统交换前必须对齐 --> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cim="http://iec.ch/TC57/2009/CIM-schema-cim14#"> <!-- 厂站是设备容器,变压器通过 EquipmentContainer 挂上去 --> <cim:Substation rdf:ID="_SUB_2201"> <cim:IdentifiedObject.name>城东220kV变电站</cim:IdentifiedObject.name> </cim:Substation> <!-- 基准电压独立于设备存在,绕组用 ratedU 与之呼应但不相等 --> <cim:BaseVoltage rdf:ID="_BV_220"> <cim:BaseVoltage.nominalVoltage>220</cim:BaseVoltage.nominalVoltage> </cim:BaseVoltage> <cim:PowerTransformer rdf:ID="_TR_0001"> <cim:Equipment.MemberOf_EquipmentContainer rdf:resource="#_SUB_2201"/> <cim:IdentifiedObject.name>1号主变</cim:IdentifiedObject.name> </cim:PowerTransformer> <!-- 绕组承载额定容量与额定电压,双绕组就是两条记录 --> <cim:TransformerWinding rdf:ID="_TRW_0001_H"> <cim:TransformerWinding.MemberOf_PowerTransformer rdf:resource="#_TR_0001"/> <cim:TransformerWinding.ratedU>230</cim:TransformerWinding.ratedU> <cim:TransformerWinding.ratedS>180</cim:TransformerWinding.ratedS> <cim:TransformerWinding.windingType>primary</cim:TransformerWinding.windingType> </cim:TransformerWinding> <!-- 端子是唯一导电的实体,设备本身不导电 --> <cim:Terminal rdf:ID="_T_0001_H"> <cim:Terminal.ConductingEquipment rdf:resource="#_TR_0001"/> <cim:Terminal.ConnectivityNode rdf:resource="#_CN_2201"/> <cim:Terminal.sequenceNumber>1</cim:Terminal.sequenceNumber> </cim:Terminal> </rdf:RDF>几个参数需要特别说明。ratedU 填的是绕组额定电压 230kV,而 BaseVoltage.nominalVoltage 填的是系统标称 220kV,这两者不相等是正常的,很多导入工具报「电压等级不匹配」的告警就来自这里,实际是提示而非错误。ratedS 单位是 MVA,交换前务必确认对方是否用了 MW 或 kVA,单位错位会让潮流计算直接发散。sequenceNumber 决定绕组在拓扑中的顺序,高低压侧写反会导致变压器变比取倒数。Terminal 的 ConductingEquipment 指向变压器而不是绕组,这是很多自研工具最容易搞错的一处。
2.3 mRID 生成:随机 UUID 还是确定性 UUID
mRID 是 CIM 对象在全模型范围内的唯一标识,也是跨系统交换时唯一的对齐依据。用随机 UUID 看着省事,但每次重建模型都会换一批 ID,历史数据、告警记录、报表统计全部对不上。
import uuid def new_mrid() -> str: """随机 UUID:新建对象时方便,但重建模型后与历史数据断链""" return str(uuid.uuid4()) def stable_mrid(domain: str, business_key: str) -> str: """确定性 UUID:同一业务主键在任何一次重建中生成同一个 mRID""" raw = f"{domain}:{business_key}" return str(uuid.uuid5(uuid.NAMESPACE_DNS, raw)) if __name__ == "__main__": # domain 用来隔离不同调度中心的编号空间,避免同名厂站撞号 print(stable_mrid("SUB_2201", "TR_0001"))uuid5 是命名空间加名字的哈希派生,输入不变输出就不变,这正是模型反复重建时需要的性质。domain 参数建议用厂站编码或调度机构编码,不要用中文名,避免编码差异导致哈希漂移。存量系统迁移时,常见做法是先跑一遍业务主键到 mRID 的映射生成,把结果落成一张对照表存进数据库,后续所有适配器都查这张表。
2.4 遵循 base CIM 不等于照抄 CIM
实施原则通常写成两句话:建模严格按 base CIM 走,不擅自给标准类加属性;CIM 覆盖不到的现场信息,用独立命名空间扩展。扩展类挂在自定义命名空间下,只通过关联指向标准类,不改标准类的继承链。
这里有个容易被忽略的边界:CIM 不同版本的类层级有过调整,Equipment 与 PowerSystemResource 的继承关系、Terminal 的关联命名都变过。拿到别家模型先别急着导,把两边的 xmlns 版本拉出来对一遍,必要时写一个 XSLT 或 Python 转换脚本做类名映射。转换脚本本身要纳入版本管理,因为它是后续所有联调的基线。
3. CIS 接口与 CORBA 总线:组件之间的数据怎么走
模型统一了语义,还得统一访问方式。CIS 定义的是一组抽象服务接口,CORBA 提供的是把这些接口落地到异构进程之间的中间件。两者配合的方式是:接口用 IDL 描述,桩代码由编译器生成,运行时通过命名服务找到对方。理解了这条链路,调度自动化系统里「组件化」才算真正落地。
3.1 CIS 的四个服务接口怎么分工
CIS 不是一个接口,而是一族按数据特征切分的服务。选错接口是性能问题的一大来源,比如拿 GDA 去刷秒级实时断面。
| 接口 | 定位 | 数据特征 | 典型调用方 |
|---|---|---|---|
| GDA | 通用数据访问 | 按 CIM 对象读写,请求-应答 | 网络分析、DTS |
| HSDA | 高速数据访问 | 实时断面,亚秒级刷新 | 画面刷新、AGC |
| TSDA | 时间序列数据访问 | 历史曲线,支持批量 | 报表、离线计算 |
| GES | 事件与订阅 | 变化推送、告警联动 | 告警窗、联动应用 |
边界判断很简单:数据源是实时库且刷新周期在秒以内,走 HSDA;数据要落历史库、按时间段取,走 TSDA;数据是按 CIM 对象组织的静态或准静态属性,走 GDA。三者背后的数据存储往往不是同一个库,混用会带来额外转换开销。
3.2 用 IDL 把接口约定固化成桩代码
接口约定写在文档里没人看,写成 IDL 才有约束力。下面是一段裁剪过的 GDA 风格接口,保留了最核心的查询与读写。
// 仅保留资源查询与属性读写,用于说明接口约定的写法 module IEC61970 { module GDA { typedef string ResourceID; // 即 CIM 对象的 mRID typedef sequence<ResourceID> ResourceIDList; struct PropertyValue { string name; // CIM 属性名,如 TransformerWinding.ratedS any value; // 类型由属性元数据决定,用 any 承载异构值 }; typedef sequence<PropertyValue> PropertyValueList; interface ResourceQuery { ResourceIDList query(in string filter); PropertyValueList get(in ResourceID id, in string propertyName); void set(in ResourceID id, in PropertyValueList values); }; }; };typedef string ResourceID 这一行看着不起眼,却是整套互操作的基础——它把 mRID 固化成跨系统的公共主键类型。any 用来承载 float、long、string 等异构值,代价是反序列化时要做类型判别,实现侧必须为每个属性准备类型元数据,否则取值时只能靠猜。
3.3 组件适配器:把私有点号翻译成 mRID
存量应用不会说 CIM,它们的接口是私有协议、点号寻址。适配器的职责就三件:查映射表、做单位归一、转换异常语义。
class LegacyScadaAdapter: """把私有协议接口包装成 CIS 资源查询接口""" def __init__(self, endpoint: str, mrid_map: dict): self.endpoint = endpoint # 遗留系统私有接口地址 self.mrid_map = mrid_map # {CIM mRID: 私有点号} self.unit_table = {"P": ("kW", 1000.0), "U": ("V", 1000.0)} def get(self, mrid: str, prop: str): tag = self.mrid_map.get(mrid) if tag is None: # 映射缺失是最常见的联调失败原因,直接抛错而不是返回空值 raise KeyError(f"{mrid} 未建立映射,需先做模型比对") raw = self._call_legacy(tag, prop) return self._normalize(prop, raw) def _normalize(self, prop: str, raw): # 单位归一到 CIM 约定:功率用 MW,电压用 kV suffix = prop[-1] if suffix in self.unit_table: _, divisor = self.unit_table[suffix] return raw / divisor return rawmrid_map 这张表通常由模型比对工具生成,导入前先跑一次比对,把差异清单人工确认。单位归一放在适配器内部而不是调用方,是因为调用方拿到的值必须符合 CIM 语义,否则网络分析算出来的潮流永远偏三个数量级。异常处理上,映射缺失要抛错而非返回空值——返回空值会让上层计算悄悄用零值参与,最终表现成错误的计算结果,排查成本极高。
3.4 CORBA 与 REST 的实时性取舍
新做系统的人常问为什么不直接用 HTTP 接口。答案在时延和推送模型上。
| 维度 | CORBA 集成总线 | HTTP/JSON 服务 |
|---|---|---|
| 单次调用时延 | 同机百微秒级 | 毫秒级起 |
| 跨语言支持 | IDL 生成多语言桩 | 需自行约定 schema |
| 跨平台屏蔽 | ORB 层屏蔽系统差异 | 天然跨平台 |
| 变化推送 | 事件通道与回调原生支持 | 需 WebSocket 等补充 |
| 运维复杂度 | 高,依赖命名服务 | 低 |
提示:CORBA 部署的坑集中在 IOR 与命名服务。把 NameService 地址写进配置文件和启动参数,别硬编码进二进制,换机器时只改配置不改代码,能省掉一次重新编译和回归测试。
4. OPEN-2000E 的层次+插件架构与组件裁剪
组件化架构能不能站住,取决于它能不能按站点规模缩放。一个大区主站要跑全量 PAS 和 DTS,一个县级站可能只需要 SCADA 加基本告警。同一套代码库要覆盖这两种形态,靠的就是层次划分加组件裁剪。
4.1 硬件到应用的四层划分与替换粒度
这套体系结构从下到上分成四层,每层的可替换程度不一样,替换成本也差得很远。
| 层次 | 内容 | 替换粒度与代价 |
|---|---|---|
| 硬件 | 各类服务器与工作站 | 整机替换,需重编 ORB 本地库 |
| 操作系统 | UNIX 与 Windows | 平台层屏蔽,应用组件基本不感知 |
| 支撑平台 | 图形工具、报表工具、系统管理、服务工具 | 可裁剪,缺哪块补哪块 |
| 应用软件 | SCADA、AGC、PAS、DTS、网络分析 | 按需装配,可整组件摘除 |
支撑平台这一层是最容易被低估的。图形工具、报表工具、系统管理看着像附属品,实际上它们决定了应用组件能不能复用同一套人机界面和数据服务。裁剪时如果只装 SCADA 组件而不装服务工具,会出现在线组件无法热替换的问题。
4.2 组件装配清单与功能子集裁剪
组件装配建议用声明式清单描述,不要写死在启动脚本里。下面是一套「SCADA + AGC + PAS」的最小装配清单,DTS 和 AVC 被裁掉。
system: name: open2000e-edge bus: # 命名服务地址走配置,方便整站迁移 orbs: ["corbaloc::10.10.1.21:2809/NameService"] components: - id: scada.core impl: scada_core.so deps: [cim.repository, cis.gda] - id: agc.main impl: agc_main.so # AGC 依赖实时断面,必须能拿到 HSDA deps: [scada.core, cis.hsda] - id: pas.network impl: pas_network.so deps: [cim.repository, cis.gda] optional: true # 小规模站点可不装 trim: disable: [dts, avc] # 按需裁剪掉的功能子集deps 字段决定启动顺序,缺失依赖时平台应拒绝加载而不是静默跳过,否则会出现在线组件运行时找不到服务的情况。optional 标记的组件允许加载失败,适合容量受限的站点。trim.disable 是显式下线,和「没装」在状态管理上要区分开,前者会释放资源并注销命名服务条目,后者只是没启动。
4.3 内标准与外标准的成本对照
新建主站和存量并网是两类完全不同的场景,选的路径也不一样。
| 维度 | 内标准方式 | 外标准方式 |
|---|---|---|
| 体系结构 | 组件模型内建 | 保持原有私有结构 |
| 数据模型 | CIM 原生 | 私有格式加边界转换 |
| 接口效率 | 无转换开销 | 转换带来损耗 |
| 故障点 | 少 | 转换模块成为新增单点 |
| 信息损失 | 无 | 转换中语义可能被丢弃 |
| 开发投入 | 大 | 小 |
| 适用场景 | 新建主站、长期演进 | 存量系统短期并网 |
外标准方式真正的问题不是效率,而是信息损失和故障点叠加。转换环节一旦丢失字段,上层拿到的是「看起来正常」的残缺模型,这类问题在联调阶段极难定位。判断规则可以简化成一句:这套系统未来三年还要接第三方应用,就走内标准;只是把老系统接进来跑一两年,外标准性价比更高。
4.4 新组件接入总线的三步
新应用接入不需要改动总线本身,但有三步必须走完,缺一步都会在运行期出问题。
# 1. 用 IDL 编译生成目标语言的桩代码,接口有变更时必须重新生成 idl -bcxx -Wbh=.h -Wbs=.cpp IEC61970_GDA.idl # 2. 注册组件到命名服务,路径按 应用/模块 分层,便于按前缀批量查询 nsadmin -h 10.10.1.21:2809 bind \ "IEC61970/GDA/NetworkAnalysis" IOR:010000002b00000049444c3a... # 3. 上线前确认命名服务里能看到自己,避免注册到错误的域 nsadmin -h 10.10.1.21:2809 list "IEC61970/GDA/*"命名服务的路径分层不是装饰。按应用名前缀做批量查询,能在不停机的情况下巡检所有已注册组件是否在线,这比逐个重启验证快得多。第三步的 list 命令要写进上线检查单,很多「组件加载成功但调不通」的问题,根源就是注册到了另一个 NameService 实例上。
5. CIM 模型校核与接口验证:上线前的两道关
联调通过不等于可以上线,模型和接口各需要一轮独立验证。模型侧关注语义完整性,接口侧关注时序稳定性,两者的检查手段完全不同。
5.1 导入前的 CIM 一致性校核脚本
模型导入前的自动校核能挡掉大部分低级错误。用 rdflib 解析模型文件,重点检查端子的连接关系和 mRID 唯一性。
from rdflib import Graph, Namespace, RDF CIM = Namespace("http://iec.ch/TC57/2009/CIM-schema-cim14#") def check(path: str) -> list: g = Graph().parse(path, format="xml") problems = [] # 检查一:Terminal 必须挂在且只挂在一个 ConnectivityNode 上 for t in g.subjects(RDF.type, CIM.Terminal): nodes = list(g.objects(t, CIM.Terminal_ConnectivityNode)) if len(nodes) != 1: problems.append(f"{t} 连接节点数为 {len(nodes)},应为 1") # 检查二:mRID 在全模型内唯一,重复会导致引用错乱 seen = {} for s in g.subjects(): key = str(s).lstrip("_") seen[key] = seen.get(key, 0) + 1 problems += [f"mRID 重复: {k}" for k, v in seen.items() if v > 1] return problems第一项检查针对悬空端子,这类错误在图形工具里看不见,但拓扑着色时会直接算错。第二项针对 mRID 重复,多份模型合并时尤其常见,表现是某些设备的量测被挂到了另一台设备上。脚本返回空列表才算通过,任何非空结果都要人工确认后再导入。
5.2 接口压测指标与故障对照
接口验证不只看通不通,还要看稳态指标。常用三个口径:单次调用时延(p95)、订阅吞吐(每秒变化点数量)、长连接稳定性(连续运行小时数)。
# HSDA 订阅压测:连续 60 秒、按 1000 点/秒注入,统计实际到达速率 ./hsda_probe --ior file://./HSDA.ior \ --duration 60 --rate 1000 --report pps注入速率和实际到达速率之间的差值反映的是背压,差值持续扩大说明消费者处理不过来,需要调 ORB 线程池或降低订阅范围。三类高频故障的定位顺序也值得固化下来:命名服务查不到组件,先看 IOR 是否过期;调用返回类型错误,先看 IDL 是否重新编译;数据值明显偏离,先查适配器的单位归一表。
把--rate往上提到 5000 再跑一遍,如果 pps 抖动超过 10%,先查 ORB 线程池配置而不是网络带宽,这是同机部署下最常见的误判方向。
本文还有配套的精品资源,点击获取