第二十三夜 电力民生写成的曲线
前言里说过,这条生命线的背后,站着另一群人。他们不爬铁塔,不下工地,守在屏幕前,手里的笔写下代码。我就是其中一个。
2021 年夏天,我进了电力这一行。进场第一天,我没有见到电网,没有见到电厂,也没有见到任何一台和电沾边的设备。我见到的是聊天窗口里甩过来的三个词,OpenADR,AC,RA。
后来我才知道,第一个是国际协议的名字,规范好几百页,查得到,看不懂;第二个,被广泛称作 VEN,AC 只是那间会议室里的叫法,我翻遍聊天记录也没找到全称;第三个,RA,是客户给下的定义,意思是资源聚合,我们要做的,不过是把这个定义变成能跑的程序。
三个词,没有一个能在搜索框里得到回答。这不是推理小说的伏笔,这是我入职的流程。
那天我把那本几百页的规范从头翻到尾,一个字也读不进去。不是英文的事,是每一句话下面都踩着我不认识的地。想读懂那些词,得先认识它们脚下的世界。所以这一夜不讲协议,不讲系统,讲一件最底下的事。电,是怎么走进你家的。
先从你家墙上那一排插座说起。
冰箱,洗衣机,热水器,空调,电视,路由器,还有不知道什么时候多出来的充电头。早上你出门,屋子空了,你以为它们全都睡了。其实没有。冰箱的压机隔一阵就醒,嗡一阵,再睡回去。路由器整夜亮着。热水器为了让你随时有热水,一直在保温。电视插着电,待机的小灯一粒一粒。一间空屋子,每一刻也有几十瓦在悄悄地流。
人在家的时候,这条流就粗了。傍晚回来,厨房先热闹,烧水,炒菜,电饭煲按下去,然后灯一盏一盏亮,电视开了,夏天的空调一开就是一整晚。夜里十一点以后灯灭了,空调还压着,再往后就只剩冰箱和路由器。到凌晨,一个家沉到几十瓦,像睡着的呼吸。
这些事,没有一件是商量好的。你今晚几点回家,炒几个菜,隔壁开不开空调,谁也不通知谁。
月底的电费单,把这些事折成几个数。居民用电按阶梯走,一个月用得越多,超出部分的单价越高,最底下那一档管着大多数人家,2012 年起全国都是这个章法。有的城市还能选分时,深夜的电便宜,白天的贵,价格替电网劝你一句,洗衣机可以挪到半夜去。你多半没理会过这句劝。没关系,记住它。价格开口说话这件事,后面有一整册的故事。
现在把镜头从家里挪开,挪到城边上的一家工厂。
厂区一角有间配电室,铁门半掩,一台变压器嗡嗡地响,柜子上挂着铭牌。全厂的电都从这台变压器过,高压进,低压出,再分到每条产线。它就是工厂版的插座,只不过按吨位算。
工厂的电费单,和家里长得不一样。除了用多少度电,还有一行钱,按一个奇怪的名目收,这个月最猛的那半小时。电网不计较你某一瞬间的峰值,它把每半小时的用量平均成一格,一个月里最高的那一格,就是这家厂的最猛用电时刻(最大需求量)。中国叫它最大需量,日本的电费单上写作デマンド,demand 的音译。基本电费按它算,契约报小了,超出要罚;报大了,一度电不用也照付。所以工厂的电工一年里最紧张的时刻,不是用电最多的时刻,是可能创纪录的那半小时。
在我后来的项目里,这样的工厂和大楼有一个统一的叫法,据点。一个据点,一块表,一簇设备。这个词从下一夜起每一夜都会遇见,先记在这里。
已经有公司在据点的配电室里装盒子,专门盯着最猛的那半小时,因为它值钱。谁在盯,为什么盯,值多少钱,那是后面几夜的事。种子埋下,接着走。
后来我在资料里第一次见到那条曲线。横轴,一天 24 小时。纵轴,功率。曲线从深夜的低谷爬上来,早上隆起一个峰,中午落下去一点,傍晚再隆起一个大得多的峰,夜里又落回去。
我脱口而出,这不就是流量监控图吗。
带我的前辈愣了一下,说,这么理解,对了一半。
对的那一半,它确实是一条千万个点汇成的曲线。错的那一半,流量图统计的是已经发生过的事,这条曲线此时此刻正在被写。你按下的电饭煲,楼下便利店的冰柜,你叫不出名字那家厂的产线,每合上一个开关,曲线就动一笔。它不是记录,它是全城正在做的事。
这样的曲线是怎么来的。
先看一户人家。我写了程序,让它把一家人的一天画出来。深夜是一条几十瓦的平线。七点前后,一个峰,一千瓦上下,烧水做饭,人一走就落回去。午后偶尔一个小峰,那是有人在家的日子。晚上七点,最高的峰,两千瓦往上,炊事,灯,电视,空调,挤在几个钟头里。十一以后落回平线。锯齿乱跳,毫无道理,这一家人的傍晚本来就没有道理,加班,聚餐,追剧到半夜,哪一天和哪一天都不一样。
再把一百户加起来。锯齿还在,可是讲道理了。晚上的峰变宽了,变圆了,凌晨的平线也平得更稳。一百个没商量过的傍晚,加出了一个大致说得出口的轮廓。
锯齿为什么越加越薄。因为每家的任性互不相干,你家电水壶响的时候别人家在睡觉,乱动相互错开,加在一起就互相抵消了一部分。可吃饭的点是共同的,千万个家往同一个钟头挤,这部分不但抵不掉,还越站越直。各自的越摊越薄,共同的越站越直,这就是加法自己的性质。
然后是 1000 万户。
程序老老实实地逐户去加,一户往 48 个格子里各添一个数,1000 万户添完,这条曲线自己站出来了。
凌晨四点,800 兆瓦上下,全城睡着的呼吸。
早上七点半,第一个肩膀,约 7200 兆瓦。
午后一点,一座小丘,约 3000 兆瓦,白天在家的人铺的。
晚上七点,18867 兆瓦,全天的峰顶。
夜里十点以后,一路落回去。

一千万件没有商量过的事,加出一个准时得像时刻表的轮廓。这句话不是修辞。程序再跑一遍,一个数都不会动,18867 就是 18867。明天千万个家庭照样任性,后天照样任性,可他们任性的总和,比列车时刻表还守时。
现在把那家工厂放回去。它最猛的那半小时两千多千瓦,折两兆瓦出头。放在 18867 兆瓦的城市曲线上,占万分之一,图上连一根毛刺都算不上。在电费单上它是巨兽,在这条曲线上它几乎不存在。
可是一家是毛刺,一万家就垒出轮廓。真实的城市曲线里,白天不会凹下去,全城的工厂,商场,写字楼把整个白天抬在高位。民生写着早晚那两个峰,工商业把白天整段抬高,谁也不是谁的地基,它们叠在一起,才是完整的一条。我这一夜的程序只画了民生的那一半,所以它白天凹下去,真城不凹。
一个家任性,一千万个家守时。这不是谁在管着他们,也不是谁在安排他们,这是加法自己的性质。一万个各自独立的任性,加出一个确定的轮廓,加的笔数越多,轮廓越稳。统计学给这件事起了名字,附注里交代,正文里你只需要看见它。
看见它之后,事情就来了。
轮廓稳,意味着明天的曲线今天就能画个八九不离十。既然画得出来,电厂就能排班,几点多烧,几点少烧,哪台检修,哪台待命。你以为电网在随机应变地伺候你,其实它在按一张时刻表迎你,而这张时刻表的底稿,是千万个家庭昨天,和昨天之前的无数个昨天,一起写的。
可预测的尽头是误差。时刻表再准,也有一口对不上的差。天热得比预报狠,球赛一开,千万台电视一起亮,该下的雨没下,空调又多压了一下午。差的那一口,谁来补。
这个问题问出口,第二十四夜就到了。
一个人用电是任性,一千万人用电是日程表。你关灯的那一秒,某台发电机少转了一口,而你永远不会知道是哪一台。
这一夜的三个镜头,一家,一百家,1000 万家加一家工厂,都写成了能跑的程序。种子锁死在那个夏天,谁来跑,都是同一条曲线。
// ============================================================
// 第二十三夜《电力民生写成的曲线》演示件
//
// 一座城市的用电曲线,是千万个家加一簇工厂叠出来的。
// 本程序打三个镜头:
// 镜头一,一户人家,48 个半点钟,锯齿乱跳;
// 镜头二,一百户,锯齿还在,但开始讲道理;
// 镜头三,1000 万户加一家工厂,晚峰浮出,白天铺上细纹。
//
// 半小时一格是有意的:电费单上"最猛的那半小时"就是这一格,
// 调度员看负荷曲线,也是这一格一格地看。
//
// 随机种子固定成进场那个夏天的日子,
// 每个读者跑出来都是同一条曲线。
// 正文里的每一个数字都必须和这里的输出对得上,
// 这是本书的底线,所以种子不许漂。
// ============================================================
import java.util.Random;
public class LoadCurve {
// 一天切成 48 格,每格半小时
static final int BINS = 48;
// 种子取 2021 年 8 月 1 日,进场那个夏天
static final long SEED = 20210801L;
public static void main(String[] args) {
Random r = new Random(SEED);
// 镜头一:一户人家,单位千瓦
double[] one = homeCurve(r);
print("镜头一 一户人家(千瓦)", one);
// 镜头二:一百户,单位千瓦
// 为什么要一百户:一个家是任性,一百个家开始守规矩,
// 这一步是给镜头三垫底的台阶
double[] hundred = new double[BINS];
for (int i = 0; i < 100; i++) {
add(hundred, homeCurve(r));
}
print("镜头二 一百户(千瓦)", hundred);
// 镜头三:1000 万户加一家工厂
// 逐户真加,不取巧:每户只往 48 个格子里各添一个数,
// 内存只要 48 个 double,时间换半分钟,值得
double[] cityKw = new double[BINS];
for (int i = 0; i < 10_000_000; i++) {
add(cityKw, homeCurve(r));
}
add(cityKw, factoryCurve(r));
// 千瓦换兆瓦,除以一千,城市尺度的数字才念得出口
double[] cityMw = new double[BINS];
for (int i = 0; i < BINS; i++) {
cityMw[i] = cityKw[i] / 1000.0;
}
print("镜头三 1000 万户加一家工厂(兆瓦)", cityMw);
}
// 造一户人家的 48 格功率曲线,单位千瓦
//
// 这一家的用电没有一件是商量好的:
// 起床有早有晚,回家有先有后,白天有没有人在家全看缘分。
// 这条曲线,就是这一家作息的直接记录。
static double[] homeCurve(Random r) {
double[] w = new double[BINS];
double base = 0.05; // 待机的劲:冰箱、路由器、一粒粒小灯,半夜也不停
double morningAmp = 0.4 + r.nextDouble() * 0.8; // 早高峰的劲:烧水做饭,0.4 到 1.2 千瓦
double eveningAmp = 1.2 + r.nextDouble() * 1.8; // 晚高峰的劲:炊事照明电视空调,1.2 到 3.0 千瓦
double morningAt = 6.5 + r.nextDouble() * 1.5; // 早晨最忙的时刻,落在 6 点半到 8 点之间
double eveningAt = 18.5 + r.nextDouble() * 1.5; // 晚间最忙的时刻,落在 18 点半到 20 点之间
boolean someoneHome = r.nextDouble() < 0.4; // 白天家里有没有人,四成人家有人
double dayAmp = someoneHome ? 0.3 + r.nextDouble() * 0.5 : 0.0;
for (int i = 0; i < BINS; i++) {
double t = i * 0.5; // 这一小格是一天里的第几个半点钟
double v = base;
v += morningAmp * bell(t, morningAt, 0.9); // 早晨那一口:出门前隆起一个峰
v += eveningAmp * bell(t, eveningAt, 1.2); // 晚间那一口:比早晨更高更宽
v += dayAmp * bell(t, 13.0, 3.5); // 白天在家:矮矮地铺在午后
v += r.nextDouble() * 0.06; // 随手开关的小噪声,让曲线毛糙
w[i] = v;
}
return w;
}
// 钟形:中心在 c、宽度 s,离中心越近值越大,四周迅速落下去
// 它不表达任何理论,就是"在那几个钟头附近隆起一个峰"的意思
static double bell(double t, double c, double s) {
double d = (t – c) / s;
return Math.exp(-d * d);
}
// 造一家工厂的 48 格功率曲线,单位千瓦
//
// 工厂和家相反:家的曲线是习惯写的,工厂的曲线是制度写的。
// 班次几点开机几点停机写在排班里,不写在概率里。
static double[] factoryCurve(Random r) {
double[] w = new double[BINS];
for (int i = 0; i < BINS; i++) {
double t = i * 0.5;
double v;
if (t >= 8 && t < 18) {
v = 1800 + r.nextDouble() * 400; // 生产时段:1800 到 2200 千瓦
} else if ((t >= 7 && t < 8) || (t >= 18 && t < 19)) {
v = 600 + r.nextDouble() * 300; // 开机与收工的坡
} else {
v = 60 + r.nextDouble() * 40; // 夜里只剩值班照明和伺服的待机
}
w[i] = v;
}
return w;
}
// 聚合的全部秘密就在这一步:把一条曲线挨格加进总量
// 一个个体不重要,加到千万个,轮廓就自己站出来了
static void add(double[] total, double[] part) {
for (int i = 0; i < BINS; i++) {
total[i] += part[i];
}
}
// 打印一条曲线:时刻、数值、字符柱
// 柱长按这条曲线自己的最大值归一,只比轮廓不比大小
static void print(String title, double[] w) {
double max = 0;
for (double v : w) {
if (v > max) {
max = v;
}
}
System.out.println("== " + title + " ==");
for (int i = 0; i < BINS; i++) {
int hh = i / 2, mm = (i % 2) * 30;
int bars = (int) Math.round(w[i] / max * 40);
StringBuilder b = new StringBuilder();
for (int k = 0; k < bars; k++) {
b.append('#');
}
System.out.printf("%02d:%02d %9.2f %s%n", hh, mm, w[i], b);
}
System.out.println();
}
}
== 镜头一 一户人家(千瓦) ==
00:00 0.09 ##
00:30 0.09 ##
01:00 0.07 #
01:30 0.05 #
02:00 0.07 #
02:30 0.06 #
03:00 0.07 #
03:30 0.10 ##
04:00 0.10 ##
04:30 0.08 ##
05:00 0.05 #
05:30 0.07 #
06:00 0.15 ###
06:30 0.45 ########
07:00 0.94 #################
07:30 1.26 ######################
08:00 0.91 ################
08:30 0.39 #######
09:00 0.17 ###
09:30 0.07 #
10:00 0.06 #
10:30 0.06 #
11:00 0.07 #
11:30 0.06 #
12:00 0.11 ##
12:30 0.11 ##
13:00 0.06 #
13:30 0.07 #
14:00 0.08 #
14:30 0.09 ##
15:00 0.06 #
15:30 0.08 #
16:00 0.09 ##
16:30 0.15 ###
17:00 0.30 #####
17:30 0.76 #############
18:00 1.52 ###########################
18:30 2.21 #######################################
19:00 2.25 ########################################
19:30 1.66 ##############################
20:00 0.91 ################
20:30 0.36 ######
21:00 0.18 ###
21:30 0.08 #
22:00 0.10 ##
22:30 0.06 #
23:00 0.09 ##
23:30 0.10 ##
== 镜头二 一百户(千瓦) ==
00:00 8.28 ##
00:30 8.34 ##
01:00 8.06 ##
01:30 7.96 ##
02:00 7.80 ##
02:30 7.68 ##
03:00 7.74 ##
03:30 8.04 ##
04:00 8.04 ##
04:30 8.18 ##
05:00 9.12 ##
05:30 13.45 ###
06:00 27.52 ######
06:30 50.56 ###########
07:00 69.80 ###############
07:30 69.20 ###############
08:00 51.14 ###########
08:30 29.62 ######
09:00 18.91 ####
09:30 17.36 ####
10:00 19.07 ####
10:30 21.77 #####
11:00 24.32 #####
11:30 26.77 ######
12:00 28.61 ######
12:30 30.17 ######
13:00 30.49 ######
13:30 30.07 ######
14:00 28.87 ######
14:30 26.89 ######
15:00 24.21 #####
15:30 21.97 #####
16:00 19.40 ####
16:30 18.97 ####
17:00 25.15 #####
17:30 47.67 ##########
18:00 91.71 ###################
18:30 148.57 ###############################
19:00 189.87 ########################################
19:30 188.47 ########################################
20:00 145.60 ###############################
20:30 87.96 ###################
21:00 43.02 #########
21:30 19.64 ####
22:00 10.55 ##
22:30 8.39 ##
23:00 7.91 ##
23:30 7.99 ##
== 镜头三 1000 万户加一家工厂(兆瓦) ==
00:00 800.19 ##
00:30 800.21 ##
01:00 800.17 ##
01:30 800.09 ##
02:00 800.21 ##
02:30 800.36 ##
03:00 800.81 ##
03:30 801.31 ##
04:00 803.39 ##
04:30 813.23 ##
05:00 890.19 ##
05:30 1315.86 ###
06:00 2670.64 ######
06:30 5043.70 ###########
07:00 7090.64 ###############
07:30 7162.31 ###############
08:00 5264.55 ###########
08:30 3055.18 ######
09:00 1891.83 ####
09:30 1689.63 ####
10:00 1864.15 ####
10:30 2122.77 #####
11:00 2388.86 #####
11:30 2632.36 ######
12:00 2829.13 ######
12:30 2956.92 ######
13:00 3001.35 ######
13:30 2957.06 ######
14:00 2829.09 ######
14:30 2632.20 ######
15:00 2389.45 #####
15:30 2128.54 #####
16:00 1904.83 ####
16:30 1884.74 ####
17:00 2539.32 #####
17:30 4726.97 ##########
18:00 9084.76 ###################
18:30 14726.84 ###############################
19:00 18867.08 ########################################
19:30 18820.32 ########################################
20:00 14579.62 ###############################
20:30 8820.38 ###################
21:00 4315.72 #########
21:30 1947.76 ####
22:00 1076.77 ##
22:30 849.29 ##
23:00 806.71 ##
23:30 800.81 ##
① 附录与真伪核对
Ⅰ 进场三词 OpenADR 与 RA 为真,几百页规范为真(本机存有正本);AC 只在聊天记录与设计书里以代号出现,它的谜底要等到册五才揭晓,本夜不揭(真)。
Ⅱ 电价章法 居民阶梯电价 2012 年 7 月起全国施行(真);分时电价自愿选择(真);两部制用户的基本电费按变压器容量或最大需量计费,由用户自愿选择,2018 年发改价格 500 号明确(真);日本高压电力基本料金按契约电力算定,以最大需要电力为基准(真);正文取半小时一格,是日本口径(真)。
Ⅲ 一户乱而万户稳 统计学称大数定律(真);本夜所有数值来自演示件一次实跑,随机种子固定为 20210801,重跑一个数都不会变(真,实跑)。
Ⅳ 前辈其人与全部对话(编);工厂与配电室场景(编);18867 兆瓦等城市数字是程序输出,属于模型口径,不是某座真实城市的记录(真,模型)。
② 术语对照
Ⅰ 负荷 被广泛称作 load,某一时刻全城正在取用的电功率总和;本夜的现象名叫用电的劲。
Ⅱ 需量 本夜不发证,现象名叫最猛的那半小时;它的正式证留到讲能源管理系统的那一夜。
Ⅲ 大数定律 各自独立的小事相加,加得越多,和的轮廓越稳;随机部分按平方根长,共同部分按倍数长,占比越摊越薄;本夜只借它一眼,不展开。
Ⅳ 据点 本书对装了表的工厂与大楼的统一叫法,一个据点,一块表,一簇设备。
③ 今夜习题 两道,评论区见。
Ⅰ 抄表员收上来 1000 个家庭的电器开关时刻表,每个电器一对时刻,几点开,几点关。哪一刻全城用电最重。替他把这一刻找出来,再数一数你的算法走了多少步。
Ⅱ 把演示件的种子改成你的生日,重跑一遍,看晚峰几点、多少兆瓦,再和邻座的数字对一下。
本书目录导航 上一篇:工业算法篇前言 从电流到电流比特 下一篇(载锅中) 起点:卷首语·算法童话
版权声明:本文为作者原创,受著作权法保护。未经授权禁止转载、搬运、摘编、改编及任何形式的二次创作;个人学习引用请注明作者与原文出处。转载授权请联系作者(CSDN 私信)。侵权必究。
网硕互联帮助中心







评论前必须登录!
注册