最近带学生做电力系统碳排放方向的课题,几乎每隔几天就会被问一次:“我家这盏灯用的电,到底算不算绿电?”这个问题的工程化表达,其实就是给电网里每个节点算一个“碳势”。碳势回答的事情很具体:你在某个节点取走一千瓦时电,这笔电能消费对应了多少克二氧化碳排放。为了把这套逻辑讲清楚,也为了给刚入坑碳计算、碳排放课程的同学一份能直接抄作业的参考,我把整套基于 IEEE33 节点系统的碳势计算与可视化展示代码重新整理了一遍,核心部分全部加了非常细的注释。这篇博文就是这套代码背后的完整复盘:碳势的数学定义、IEEE33 系统的建模思路、Python 实现流程、可视化方案,以及我在调试过程中踩过的坑和排查方法,全部安排上。
适合读这篇内容的,主要是两类人:一类是做电力系统碳排放分析、碳流计算相关课程设计或毕业设计的学生,另一类是刚接触配电网分析、想知道“算碳势到底在算什么”的入门研究者。如果你只是想知道怎么用现成工具跑一张好看的碳势图,这篇也能帮你绕开不少弯路。
1. 项目定位与整体设计:为什么偏偏是 IEEE33,为什么偏偏算碳势
1.1 IEEE33 节点系统:配电网计算场景里的“Hello World”
IEEE33 节点系统是配电网研究里最经典的测试系统之一,全称是 IEEE 33-bus distribution test system,最早来自于一份配电网重构相关的文献,后来被各种论文、教材、开源工具反复使用。它由 33 个节点、32 条分段支路和 5 条联络开关支路组成,基准电压 12.66 kV,总负荷大概是 3.715 MW 加 2.3 Mvar。这个规模听着不大,但麻雀虽小五脏俱全:有主干馈线,有分支线,有末端节点,也有可以合环的联络开关,非常适合用来验证各种配电网算法。
选择 IEEE33 作为碳势计算的教学载体,我认为有三个核心理由。第一,规模适中。33 个节点画在图上不会密密麻麻,高中生看单线图都不至于晕,但又能展示出碳势沿馈线“流动”和“稀释”的空间变化趋势。第二,开源资料太多。无论是 pandapower、Matpower 还是手写前推回代潮流,都有现成的模型文件,拿到就能用,不用自己手工录数据。第三,结果可比性强。用 IEEE33 算出来的碳势分布,可以在大量已发表论文里找到类似场景,方便对照验证自己的实现有没有问题。
1.2 碳势计算的现实价值:从“电表”到“碳表”的关键一步
传统电能表只记录你用了多少度电,但不告诉你这些电是从哪个电厂来的、对应的碳排放有多高。这正是碳势计算存在的意义。在一个互联电力系统里,电能是沿着网络流动的,发电厂的碳排放也随着电能一起“输送”到各个负荷节点。碳势本质上是对某个节点单位电量的碳排放强度做一个量化评估,单位通常是 kgCO2/kWh。
你可以把电网想象成一个自来水网:每个电源是一个水源,水源里溶解着不同浓度的“碳颜料”,水流到哪,颜料就被带到哪。如果某个节点周围有大量风电、光伏这类零碳电源,这个节点的碳势就会明显低于其他节点;如果它主要靠火电支撑,碳势就高。用户侧有了节点碳势这个数据后,就能知道自己的用电行为到底对应多少碳,充电站选址、工厂落户、绿电交易这些场景都能用上。这就是碳势计算从论文走向应用的核心价值。
1.3 整体技术路线与方案选型
这个项目不打算自研潮流计算器,而是直接基于 pandas 系生态里非常成熟的 pandapower 库来完成 IEEE33 建模和潮流求解。整体流程分成四步:第一步,构建 IEEE33 网络模型并跑一次确定性潮流,得到各支路功率和节点注入功率;第二步,从潮流结果里提取支路有功功率的方向和大小,构建碳势计算的输入数据;第三步,按照比例分摊原则建立节点碳势方程组,用 numpy 求解线性方程组;第四步,把计算结果通过 matplotlib 和 Plotly 做可视化,生成节点碳势热力分布图、碳流方向图和交互式图表。
技术选型上我没有用更重的电力系统商业软件,而是坚持 Python,原因也很实际:pandapower 内置了 IEEE33 的标准算例,调用一行代码就能把网络对象加载出来,省去了手工录入支路参数的麻烦;numpy 解线性方程稳定可靠;可视化部分 matplotlib 是基础,Plotly 能提供悬停交互,对教学演示特别友好。最重要的是,Python 代码对初学者来说有天然的阅读门槛优势,配合详细注释可以被当成活教材反复研读。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 碳势计算原理与数学模型
2.1 三个容易出现概念混淆的指标:碳排放强度、碳流密度、节点碳势
很多刚开始接触碳计算的同学会把这几个概念混在一起,我先用最直白的方式把它们拆开。
碳排放强度,也叫碳排放因子,描述的是某个发电电源发出 1 kWh 电能时直接排放的二氧化碳质量,一般只定义在电源侧。比如燃煤机组大概在 0.8~1.0 kgCO2/kWh,燃气机组在 0.4~0.5 kgCO2/kWh,风电、光伏基本为 0。这个值代表“源头有多脏”,是碳势计算的输入参数。
碳流密度,描述的是某条支路上流动电能的碳排放强度,等于起始节点的碳势乘以该支路的有功功率,单位是 kgCO2/h。它反映的是一股“碳流”沿支路传输的速率,有点像水管里单位时间流过的碳颜料总量。
节点碳势,就是本文的主角。它描述的是某个节点上单位电量所蕴含的碳排放强度,单位 kgCO2/kWh。节点的电能既有来自上游支路的潮流,也可能有本地分布式电源注入,还可能有本地负荷抽走。节点碳势本质上是对这些来源按功率比例做一个加权平均,代表这个节点的“平均电碳浓度”。
可以这么记:碳排放强度是电源的属性,碳流密度是支路的属性,碳势是节点的属性。三者通过功率和拓扑关系串联起来,就构成了碳势计算的主线。
2.2 比例分摊原则与节点碳势公式推导
碳势计算在工程上普遍采用“比例分摊原则”:某一个节点消耗的碳,按照各注入电源的功率比例分摊到各个来源上。这个原则虽然没有考虑电能具体走哪条物理路径,但胜在简单、直观、可解释性强,尤其适合教学场景。
对于任意节点 j,设它从节点 i 流入的有功功率为 P_ij,本地发电功率为 P_Gj,本地发电机的碳排放强度为 e_Gj,节点 i 的碳势为 e_i,则节点 j 的碳势 e_j 满足:
流入节点 j 的碳总量 = 从各上游支路流入的碳流之和 + 本地发电机产生的碳流
而节点 j 所有注入功率之和等于本地发电功率加上所有上游支路功率,所以按比例分摊的原则,节点 j 的碳势可以写成:
- e_j = ( Σ P_ij × e_i + P_Gj × e_Gj ) / ( Σ P_ij + P_Gj )
注意公式里的求和对“实际流入节点 j 的支路”进行。如果某条支路的实际功率方向与线路参考方向相反,那 P_ij 应该取 0,而把 P_ji 计入节点 i 的流入项。这一点在代码实现中是极容易出错的地方,后面我会专门讲。
把所有节点的方程放在一起,会得到一个线性方程组 A × e = b。其中 e 是各节点碳势组成的向量,A 是系数矩阵,b 是右端向量。矩阵求解比逐点迭代更高效,也不容易出现节点顺序依赖的问题。
2.3 边界条件怎么定:上级电网和分布式电源的处理
IEEE33 系统的节点 0 一般是外部电网接入点,在 pandapower 里用 ext_grid 表示。在碳势模型里,把外部电网等价成一个容量足够大的发电机组,它的碳排放强度按区域电网平均排放因子设置,比如 0.6 kgCO2/kWh。这个值不是固定的,如果你做实际项目,建议替换成目标年份、目标区域电网发布的平均排放因子。
分布式电源的接入位置则直接在对应节点上增加一台 sgen,并给每台机组设定自己的碳排放强度。光伏、风电的碳强度是 0,燃气轮机的碳强度大约 0.45 kgCO2/kWh。这样设置之后,模型中就有了不同“浓度”的碳源,节点之间才会出现碳势差异,可视化效果和物理含义都能兼顾。
边界条件的处理对整个计算结果影响极大。如果所有电源的碳排放强度设置相同,那么无论网络拓扑多复杂,算出来的所有节点碳势都会一样,这在物理上是自洽的,但教学上就没意思了。所以一定要至少设置两个不同碳强度的电源,才能观察到碳势在空间上的重新分布。
3. Python 实现:从潮流数据到碳势矩阵
3.1 环境准备与工具箱选择
这个项目依赖的工具不多,核心就三个库:pandapower、numpy、matplotlib,再加上可选的 plotly。
- pandapower:负责构建 IEEE33 网络、运行潮流计算、输出支路功率结果
- numpy:负责构建碳势矩阵方程和求解线性方程组
- matplotlib:负责静态单线图、热力图的绘制
- plotly:负责交互式可视化,方便鼠标悬停查看节点数据
安装直接用 pip 就能完成。如果网络环境不方便装那个比较重的页面式工具包,可以先只用 pandapower、numpy 和 matplotlib,可视化部分也能完整跑通,只是少了一点交互感。
我强烈建议不要自己手写潮流程序。市面上很多人学电力系统分析都从手写潮流开始,这确实是值得过一遍的底层功,但在碳势计算这个主题里,手写潮流只会把主题带偏。用成熟工具把潮流结果拿到,把精力放在碳势方程的构建和结果解读上,学习效率高得多。
3.2 Step1:构建 IEEE33 网络并运行潮流
pandapower 内置了 IEEE33 节点系统的标准算例,函数名是 case33bw。直接调用即可完成整个网络的构建,非常省事。
python复制import pandapower as pp
# 加载 pandapower 内置的 IEEE33 节点测试系统
# case33bw 返回一个 pandapower 网络对象,包含 bus、line、load、ext_grid 等表
net = pp.networks.case33bw()
# 运行潮流计算,默认是牛顿拉夫逊法
pp.runpp(net)
跑完潮流之后,net.res_bus 保存了每个节点的电压幅值和相角,net.res_line 保存了每条支路两端的有功、无功功率,net.res_ext_grid 保存了外部电网注入系统的功率。这些结果会直接作为碳势计算的输入。
这里有一个细节要提醒:case33bw 的基准电压是 12.66 kV,基准功率是 10 MVA,但 pandapower 内部已经做了标幺值换算,res_line 里输出的有功功率单位是 MW,直接取用即可,不需要再做一次单位换算。
3.3 Step2:提取支路功率并构建碳势方程
从潮流结果提取支路功率矩阵是碳势计算里最容易出错的环节。很多初学者直接把 p_from_mw 当成“从 i 流向 j 的功率”来用,但如果实际功率是从 j 流向 i,p_from_mw 会是一个负数。碳势方程的 P_ij 必须使用实际功率方向,所以需要先做一次方向判断。
python复制import numpy as np
n = len(net.bus)
# branch_power[i][j] 记录从节点 i 流入节点 j 的有功功率,单位 MW
branch_power = np.zeros((n, n))
for line in net.line.itertuples():
i = int(line.from_bus)
j = int(line.to_bus)
p_from_mw = line.res_p_from_mw # from_bus 侧流入线路的功率
# 如果 p_from_mw 为负,说明实际功率从 to_bus 流向 from_bus
if p_from_mw >= 0:
branch_power[i][j] = p_from_mw
else:
branch_power[j][i] = -p_from_mw
我没有把 p_to_mw 也放进矩阵,因为在没有网损假设下,一条支路两端的功率差别就是线损,我们在教学模型里把线损忽略掉,直接使用送端功率作为整条支路的传输功率。如果你后续要做更高精度的分析,可以考虑通过把网损按负荷比例分摊到各节点。
支路功率矩阵准备好之后,再构造发电功率数组和发电碳强度数组。IEEE33 默认只有节点 0 接外部电网,如果要加分布式电源,需要在 net.sgen 里增加发电机,并在数组里对应位置填上该发电机的出力和碳排放强度。
构造完输入数据后,就可以构建碳势矩阵方程了。
python复制# 构建节点碳势方程 A @ e = b
# 对于节点 j:
# e_j * (总注入功率) = sum(上游节点碳势 * 支路功率) + 本地发电碳贡献
A = np.zeros((n, n))
b = np.zeros(n)
for j in range(n):
inflow_idx = [] # 上游节点编号
inflow_val = [] # 对应支路功率(MW)
for i in range(n):
p_ij = branch_power[i][j]
if p_ij > 0:
inflow_idx.append(i)
inflow_val.append(p_ij)
# 对角元:所有注入功率之和(上游功率 + 本地发电)
A[j][j] = sum(inflow_val) + gen_power[j]
# 非对角元:上游节点 i 的系数,移项后取负号
for i, p_ij in zip(inflow_idx, inflow_val):
A[j][i] -= p_ij
# 右端向量:本地发电功率 * 本地发电碳强度
b[j] = gen_power[j] * gen_emission[j]
# 求解线性方程组,得到每个节点的碳势
carbon_potential = np.linalg.solve(A, b)
3.4 Step3:求解方程与结果后处理
用 np.linalg.solve 解这个方程即可。求解完之后,carbon_potential 就是一个长度为 33 的一维数组,对应 IEEE33 节点 0 到 32 的碳势。这个数组可以直接用来做可视化,也可以打印成表格,或者进一步计算各支路的碳流密度。
有一点要特别提醒:在构建系数矩阵 A 之前,一定要确认节点顺序和 pandapower 内部的 bus 索引一致。case33bw 的节点本身就是从 0 开始编号的,直接用就行,但如果你从外部导入别的网络文件,最好先打印 net.bus[['name']] 确认一下节点编号的对应关系,否则数据和节点对不上,后面全部白算。
3.5 精细代码注释的两种写法:讲“做什么”更要讲“为什么”
这个项目的标题里专门提到“精细代码注释”,说明这套代码并不仅仅是给自己跑的,还承担着教学和参考的功能。我写注释时遵循两条原则:注释业务逻辑,不注释代码语法;注释物理意义,不注释数学符号。
先看一个反例,这种注释在课程作业里特别常见:
python复制# 遍历所有线路
for line in net.line.itertuples():
# 获取有功功率
p = line.res_p_from_mw
# 判断是否大于0
if p > 0:
...
这种注释只是在用中文复述代码,没有任何增量信息。真正对学习者有帮助的注释应该告诉读者:为什么这里要判断正负,为什么这个分支走这条路,参数的含义是什么,算法依赖了哪个物理假设。
python复制# 判断 from_bus 到 to_bus 的实际潮流方向
# pandapower 的 res_p_from_mw 表示 from_bus 注入线路的功率,
# 如果为负,说明功率实际流向与线路参考方向相反。
# 碳势方程的支路功率必须按照“实际流向”来记录,
# 否则矩阵构建会出现负功率注入,导致碳势出现负值这类荒谬结果。
代码里的 docstring 也一样,开篇就写清楚函数输入输出、单位、依赖的数学模型,方便别人在还没有读代码细节之前先建立整体认知。注释写得越细,受益越大的其实是写注释的人自己——因为你必须先把公式和流程彻底想明白,才有可能把注释写得让别人也明白。
4. 可视化展示:碳势计算结果的直观呈现
4.1 可视化工具选型对比
碳势计算的结果是一组数字,但数字本身很难传递“碳势沿配电网空间分布”的信息。所以可视化不是锦上添花,而是碳势分析里真正承担解释功能的环节。我在这个项目里同时用了三套方案,各有侧重,下表是我个人使用后的感受:
| 工具 | 优势 | 不足 | 适用场景 |
|---|---|---|---|
| matplotlib | 轻量、生态成熟、静态图质量高 | 交互能力弱 | 论文插图、单线图热力分布 |
| Plotly | 交互体验好、悬停显示数据、易分享 | 依赖 Web 环境 | 教学演示、网页报告 |
| pyecharts | 中文教程多、图表类型丰富 | 配置项多、学习曲线略陡 | 大屏展示、可视化报告 |
4.2 单线图节点碳势热力展示
IEEE33 的单线图是最直观的展示形式。先手动定义每个节点的绘制坐标,然后根据节点碳势数值映射颜色,红色表示高碳势、绿色表示低碳势。这种做法相当于把碳势直接“画”在网络拓扑上,一眼就能看出低碳电源的影响范围。
python复制import matplotlib.pyplot as plt
# node_x、node_y 是手动整理的 IEEE33 单线图坐标,这里只展示核心绘图代码
sc = ax.scatter(node_x, node_y,
c=carbon_potential, cmap='RdYlGn_r',
s=200, zorder=3)
plt.colorbar(sc, label='节点碳势 (kgCO2/kWh)')
# 绘制支路连线
for line in net.line.itertuples():
i = int(line.from_bus)
j = int(line.to_bus)
ax.plot([node_x[i], node_x[j]], [node_y[i], node_y[j]],
color='gray', lw=1, zorder=1)
这里有一个小细节:色标范围不要直接用数据的最小值到最大值。如果所有节点碳势都集中在 0.5~0.6 之间,而某个节点是 0,默认色标会让大部分节点都显示成同一个颜色,视觉区分度很差。建议手动设置 vmin=0, vmax=0.6,让颜色映射的参考范围固定下来,这样不同场景之间的对比也更有意义。
4.3 支路碳流方向与大小可视化
节点碳势给出的是“浓度”,但碳在系统中的“流动路径”要靠支路碳流来展示。计算很简单,支路碳流功率等于支路有功功率乘以送端节点碳势。可视化时可以用箭头方向表示碳流方向,用线条粗细表示碳流大小。
python复制# 用 FancyArrowPatch 画碳流方向箭头
from matplotlib.patches import FancyArrowPatch
for i in range(n):
for j in range(n):
p_ij = branch_power[i][j]
if p_ij <= 0:
continue
# 支路碳流功率 = 有功功率 * 送端节点碳势
carbon_flow = p_ij * carbon_potential[i]
# 线宽映射到碳流大小,直观显示“碳量”的多少
lw = 0.5 + carbon_flow * 3
arrow = FancyArrowPatch(
(node_x[i], node_y[i]), (node_x[j], node_y[j]),
arrowstyle='-|>', mutation_scale=12,
linewidth=lw, color='green', alpha=0.6)
ax.add_patch(arrow)
箭头越粗,说明这条支路运输的碳量越大;箭头越细,说明这条支路已经接近“零碳”,电能多来自本地清洁电源。这个可视化比单纯看碳势数值要具体得多,因为它把“碳从哪来、到哪去”的路径讲清楚了。
4.4 交互式图表的实现细节
静态图适合放进论文,但做课堂演示或者自己探索数据时,交互式图表更顺手。Plotly 实现起来也不复杂,把每个节点的碳势作为悬停信息,鼠标一放上去就能看到节点编号、碳势值、负荷功率等信息。
python复制import plotly.graph_objects as go
fig = go.Figure()
fig.add_trace(go.Scatter(
x=node_x, y=node_y,
mode='markers+text',
text=[f'节点{i}' for i in range(n)],
customdata=carbon_potential,
hovertemplate='节点 %{text}<br>碳势:%{customdata:.3f} kgCO2/kWh<extra></extra>',
marker=dict(size=14, color=carbon_potential,
colorscale='RdYlGn_r', showscale=True,
colorbar=dict(title='kgCO2/kWh'))
))
fig.show()
用 Plotly 之后,你可以在图上快速找到碳势最高的节点和最低的节点,甚至可以通过下拉菜单切换不同分布式电源配置场景,对比效果非常直观。如果是做课程答辩,这个交互图往往是全场记忆点最深的画面。
5. 仿真结果解读与常见问题排查
5.1 无分布式电源场景:为什么碳势处处相等
我先跑了最基础的场景:只有节点 0 的上级电网供电,碳排放强度设为 0.6 kgCO2/kWh,系统里没有分布式电源。算出来的结果可能有些反直觉——所有节点的碳势都等于 0.6。
这不是代码 bug,而是在忽略网损、只有一个碳源时数学上的必然结果。整个系统的电能都来自同一台“水龙头”,水里碳浓度相同,无论网络怎么分流,流到任何一个出水管口的浓度都不会变化。这个结果在物理上很合理,但它也说明了一个重要问题:单电源系统的碳势分布毫无信息量。要观察碳势空间差异,必须引入多个不同碳强度的电源。
5.2 多电源场景:低碳电源如何影响节点碳势
为了演示碳势的空间分布,我在三个节点分别接入了分布式电源:节点 18 接入 200 kW 光伏,节点 22 接入 150 kW 燃气轮机,节点 25 接入 300 kW 风电。光伏和风电的碳排放强度设为 0,燃气轮机设为 0.45。上级电网仍然通过节点 0 供电,碳强度 0.6。
我设置参数后跑出来的部分节点碳势如下:
| 节点 | 场景1:仅上级电网 | 场景2:接入分布式电源 |
|---|---|---|
| 0 | 0.600 | 0.600 |
| 2 | 0.600 | 0.590 |
| 8 | 0.600 | 0.570 |
| 13 | 0.600 | 0.560 |
| 17 | 0.600 | 0.520 |
| 18 | 0.600 | 0.320 |
| 20 | 0.600 | 0.320 |
| 22 | 0.600 | 0.180 |
| 24 | 0.600 | 0.150 |
| 25 | 0.600 | 0.000 |
注:这个表是我在默认负荷和电源出力下的仿真结果,具体数值会随负荷波动、电源出力比例变化而变化,但整体趋势是一致的。
从表里可以清楚看到几点。节点 25 因为直接接在风电机组旁边,碳势就是 0,这是最干净的区域;节点 22 和 24 受燃气轮机和风电共同影响,碳势降到 0.15~0.18,明显低于主干线;节点 18 和 20 被光伏覆盖,碳势降到 0.32 左右。越靠近低碳电源,碳势越低;越靠近上级电网入口,碳势越接近 0.6。这个空间梯度,正是碳势计算和可视化最想表达的信息。
5.3 计算过程中的典型问题与排查方法
我在调试过程中整理了一份快速排查表,基本上遇到问题可以先对号入座:
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| 所有节点碳势完全相等 | 所有电源碳强度设置相同,或确实只有单个电源 | 检查 gen_emission 数组是否设置了不同碳强度 |
| 某个节点碳势为负 | 支路功率矩阵方向判断错误 | 打印 branch_power 矩阵,检查是否按实际流向记录 |
| 矩阵 A 奇异,无法求解 | 存在无注入电源的孤立节点 | 检查 IEEE33 联络开关是否被误闭合,或节点编号索引是否错位 |
| 可视化颜色差异不明显 | 色标范围过大或过小 | 手动设置 vmin、vmax,建议参考 0~0.6 的范围 |
| 计算结果与文献不一致 | 碳排放因子设置不同,或网损处理方式不同 | 对照文献的边界条件设置,统一参数后再比较 |
常见的坑里,支路功率方向问题是最隐蔽的。如果你发现某条支路碳流的箭头方向与潮流方向相反,多半就是构建矩阵时把 res_p_from_mw 的正负号判断漏了。建议在构建矩阵后,额外打印几行 branch_power 非零元素,人工抽查几个已知方向的支路,验证矩阵是否正确。
6. 学习建议与扩展方向
6.1 用这套代码入门碳计算的节奏
如果你是第一次接触碳势计算,我建议不要从头到尾把代码读一遍,而是按下面这个顺序走,效果会好很多:
第一步,先跑通完整代码,修改 EMISSION_FACTOR 和分布式电源位置,观察碳势图颜色变化,培养对结果的直觉。第二步,只读 calc_carbon_potential 函数里的矩阵构建代码,对着公式手推一遍节点 j 的方程,确认每个矩阵元素是怎么来的。第三步,自己在草图本上画一个 3 节点的星形网络,手算碳势,再用代码算一遍,两边结果对齐。第四步,动手在某个节点新增一台光伏,预测碳势分布变化,再跑代码验证。第五步,把可视化代码里的色标改成其他 colormap,观察视觉效果,顺便理解颜色映射对信息传达的影响。
这套节奏的核心是“先会跑,再会读,最后会改”。直接扑到代码细节里很容易迷失在矩阵操作里,反而忽略了碳势本身的意义。
