第二十三夜 电力民生写成的曲线
前言里说过,这条生命线的背后,站着另一群人。他们不爬铁塔,不下工地,守在屏幕前,手里的笔写下代码。我就是其中一个。
2021 年夏天,我进了电力这一行。进场第一天,我没有见到电网,没有见到电厂,也没有见到任何一台和电沾边的设备。我见到的是聊天窗口里甩过来的三个词,OpenADR,AC,RA。
后来我才知道,第一个是国际协议的名字,规范好几百页,查得到,看不懂;第二个,被广泛称作 VEN,AC 只是那间会议室里的叫法,我翻遍聊天记录也没找到全称;第三个,RA,是客户给下的定义,意思是资源聚合,我们要做的,不过是把这个定义变成能跑的程序。
三个词,没有一个能在搜索框里得到回答。这不是推理小说的伏笔,这是我入职的流程。
那天我把那本几百页的规范从头翻到尾,一个字也读不进去。不是英文的事,是每一句话下面都踩着我不认识的地。想读懂那些词,得先认识它们脚下的世界。所以这一夜不讲协议,不讲系统,讲一件最底下的事。电,是怎么走进你家的。
先从你家墙上那一排插座说起。
冰箱,洗衣机,热水器,空调,电视,路由器,还有不知道什么时候多出来的充电头。早上你出门,屋子空了,你以为它们全都睡了。其实没有。冰箱的压机隔一阵就醒,嗡一阵,再睡回去。路由器整夜亮着。热水器为了让你随时有热水,一直在保温。电视插着电,待机的小灯一粒一粒。一间空屋子,每一刻也有几十瓦在悄悄地流。
人在家的时候,这条流就粗了。傍晚回来,厨房先热闹,烧水,炒菜,电饭煲按下去,然后灯一盏一盏亮,电视开了,夏天的空调一开就是一整晚。夜里十一点以后灯灭了,空调还压着,再往后就只剩冰箱和路由器。到凌晨,一个家沉到几十瓦,像睡着的呼吸。
这些事,没有一件是商量好的。你今晚几点回家,炒几个菜,隔壁开不开空调,谁也不通知谁。
月底的电费单,把这些事折成几个数。居民用电按阶梯走,一个月用得越多,超出部分的单价越高,最底下那一档管着大多数人家,2012 年起全国都是这个章法。有的城市还能选分时,深夜的电便宜,白天的贵,价格替电网劝你一句,洗衣机可以挪到半夜去。你多半没理会过这句劝。没关系,记住它。价格开口说话这件事,后面有一整册的故事。
现在把镜头从家里挪开,挪到城边上的一家工厂。
厂区一角有间配电室,铁门半掩,一台变压器嗡嗡地响,柜子上挂着铭牌。全厂的电都从这台变压器过,高压进,低压出,再分到每条产线。它就是工厂版的插座,只不过按吨位算。
工厂的电费单,和家里长得不一样。除了用多少度电,还有一行钱,按一个奇怪的名目收,这个月最猛的那半小时。电网不计较你某一瞬间的峰值,它把每半小时的用量平均成一格,一个月里最高的那一格,就是这家厂的最猛用电时刻(最大需求量)。中国叫它最大需量,日本的电费单上写作デマンド,demand 的音译。基本电费按它算,契约报小了,超出要罚;报大了,一度电不用也照付。所以工厂的电工一年里最紧张的时刻,不是用电最多的时刻,是可能创纪录的那半小时。
在我后来的项目里,这样的工厂和大楼有一个统一的叫法,据点。一个据点,一块表,一簇设备。这个词从下一夜起每一夜都会遇见,先记在这里。
已经有公司在据点的配电室里装盒子,专门盯着最猛的那半小时,因为它值钱。谁在盯,为什么盯,值多少钱,那是后面几夜的事。种子埋下,接着走。
后来我在资料里第一次见到那条曲线。横轴,一天 24 小时。纵轴,功率。曲线从深夜的低谷爬上来,早上隆起一个峰,中午落下去一点,傍晚再隆起一个大得多的峰,夜里又落回去。
我脱口而出,这不就是流量监控图吗。
带我的前辈愣了一下,说,这么理解,对了一半。
对的那一半,它确实是一条千万个点汇成的曲线。错的那一半,流量图统计的是已经发生过的事,这条曲线此时此刻正在被写。你按下的电饭煲,楼下便利店的冰柜,你叫不出名字那家厂的产线,每合上一个开关,曲线就动一笔。它不是记录,它是全城正在做的事。
这样的曲线是怎么来的。
先看一户人家。我写了程序,让它把一家人的一天画出来。深夜是一条几十瓦的平线。七点前后,一个峰,一千瓦上下,烧水做饭,人一走就落回去。午后偶尔一个小峰,那是有人在家的日子。晚上七点,最高的峰,两千瓦往上,炊事,灯,电视,空调,挤在几个钟头里。十一以后落回平线。锯齿乱跳,毫无道理,这一家人的傍晚本来就没有道理,加班,聚餐,追剧到半夜,哪一天和哪一天都不一样。
再把一百户加起来。锯齿还在,可是讲道理了。晚上的峰变宽了,变圆了,凌晨的平线也平得更稳。一百个没商量过的傍晚,加出了一个大致说得出口的轮廓。
锯齿为什么越加越薄。因为每家的任性互不相干,你家电水壶响的时候别人家在睡觉,乱动相互错开,加在一起就互相抵消了一部分。可吃饭的点是共同的,千万个家往同一个钟头挤,这部分不但抵不掉,还越站越直。各自的越摊越薄,共同的越站越直,这就是加法自己的性质。
然后是 1000 万户。
程序老老实实地逐户去加,一户往 48 个格子里各添一个数,1000 万户添完,这条曲线自己站出来了。
凌晨四点,800 兆瓦上下,全城睡着的呼吸。
早上七点半,第一个肩膀,约 7200 兆瓦。
午后一点,一座小丘,约 3000 兆瓦,白天在家的人铺的。
晚上七点,18867 兆瓦,全天的峰顶。
夜里十点以后,一路落回去。
一千万件没有商量过的事,加出一个准时得像时刻表的轮廓。这句话不是修辞。程序再跑一遍,一个数都不会动,18867 就是 18867。明天千万个家庭照样任性,后天照样任性,可他们任性的总和,比列车时刻表还守时。
现在把那家工厂放回去。它最猛的那半小时两千多千瓦,折两兆瓦出头。放在 18867 兆瓦的城市曲线上,占万分之一,图上连一根毛刺都算不上。在电费单上它是巨兽,在这条曲线上它几乎不存在。
可是一家是毛刺,一万家就垒出轮廓。真实的城市曲线里,白天不会凹下去,全城的工厂,商场,写字楼把整个白天抬在高位。民生写着早晚那两个峰,工商业把白天整段抬高,谁也不是谁的地基,它们叠在一起,才是完整的一条。我这一夜的程序只画了民生的那一半,所以它白天凹下去,真城不凹。
一个家任性,一千万个家守时。这不是谁在管着他们,也不是谁在安排他们,这是加法自己的性质。一万个各自独立的任性,加出一个确定的轮廓,加的笔数越多,轮廓越稳。统计学给这件事起了名字,附注里交代,正文里你只需要看见它。
看见它之后,事情就来了。
轮廓稳,意味着明天的曲线今天就能画个八九不离十。既然画得出来,电厂就能排班,几点多烧,几点少烧,哪台检修,哪台待命。你以为电网在随机应变地伺候你,其实它在按一张时刻表迎你,而这张时刻表的底稿,是千万个家庭昨天,和昨天之前的无数个昨天,一起写的。
可预测的尽头是误差。时刻表再准,也有一口对不上的差。天热得比预报狠,球赛一开,千万台电视一起亮,该下的雨没下,空调又多压了一下午。差的那一口,谁来补。
这个问题问出口,第二十四夜就到了。
一个人用电是任性,一千万人用电是日程表。你关灯的那一秒,某台发电机少转了一口,而你永远不会知道是哪一台。
这一夜的三个镜头,一家,一百家,1000 万家加一家工厂,都写成了能跑的程序。种子锁死在那个夏天,谁来跑,都是同一条曲线。
// ============================================================// 第二十三夜《电力民生写成的曲线》演示件//// 一座城市的用电曲线,是千万个家加一簇工厂叠出来的。// 本程序打三个镜头:// 镜头一,一户人家,48 个半点钟,锯齿乱跳;// 镜头二,一百户,锯齿还在,但开始讲道理;// 镜头三,1000 万户加一家工厂,晚峰浮出,白天铺上细纹。//// 半小时一格是有意的:电费单上"最猛的那半小时"就是这一格,// 调度员看负荷曲线,也是这一格一格地看。//// 随机种子固定成进场那个夏天的日子,// 每个读者跑出来都是同一条曲线。// 正文里的每一个数字都必须和这里的输出对得上,// 这是本书的底线,所以种子不许漂。// ============================================================importjava.util.Random;publicclassLoadCurve{// 一天切成 48 格,每格半小时staticfinalintBINS=48;// 种子取 2021 年 8 月 1 日,进场那个夏天staticfinallongSEED=20210801L;publicstaticvoidmain(String[] args){Randomr=newRandom(SEED);// 镜头一:一户人家,单位千瓦double[]one=homeCurve(r); print("镜头一 一户人家(千瓦)", one);// 镜头二:一百户,单位千瓦// 为什么要一百户:一个家是任性,一百个家开始守规矩,// 这一步是给镜头三垫底的台阶double[]hundred=newdouble[BINS];for(inti=0; i <100; i++) { add(hundred, homeCurve(r)); } print("镜头二 一百户(千瓦)", hundred);// 镜头三:1000 万户加一家工厂// 逐户真加,不取巧:每户只往 48 个格子里各添一个数,// 内存只要 48 个 double,时间换半分钟,值得double[]cityKw=newdouble[BINS];for(inti=0; i <10_000_000; i++) { add(cityKw, homeCurve(r)); } add(cityKw, factoryCurve(r));// 千瓦换兆瓦,除以一千,城市尺度的数字才念得出口double[]cityMw=newdouble[BINS];for(inti=0; i < BINS; i++) { cityMw[i] = cityKw[i] /1000.0; } print("镜头三 1000 万户加一家工厂(兆瓦)", cityMw); }// 造一户人家的 48 格功率曲线,单位千瓦//// 这一家的用电没有一件是商量好的:// 起床有早有晚,回家有先有后,白天有没有人在家全看缘分。// 这条曲线,就是这一家作息的直接记录。staticdouble[]homeCurve(Random r){double[]w=newdouble[BINS];doublebase=0.05;// 待机的劲:冰箱、路由器、一粒粒小灯,半夜也不停doublemorningAmp=0.4+ r.nextDouble() *0.8;// 早高峰的劲:烧水做饭,0.4 到 1.2 千瓦doubleeveningAmp=1.2+ r.nextDouble() *1.8;// 晚高峰的劲:炊事照明电视空调,1.2 到 3.0 千瓦doublemorningAt=6.5+ r.nextDouble() *1.5;// 早晨最忙的时刻,落在 6 点半到 8 点之间doubleeveningAt=18.5+ r.nextDouble() *1.5;// 晚间最忙的时刻,落在 18 点半到 20 点之间booleansomeoneHome=r.nextDouble() <0.4;// 白天家里有没有人,四成人家有人doubledayAmp=someoneHome ?0.3+ r.nextDouble() *0.5:0.0;for(inti=0; i < BINS; i++) {doublet=i *0.5;// 这一小格是一天里的第几个半点钟doublev=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; }returnw; }// 钟形:中心在 c、宽度 s,离中心越近值越大,四周迅速落下去// 它不表达任何理论,就是"在那几个钟头附近隆起一个峰"的意思staticdoublebell(doublet,doublec,doubles){doubled=(t - c) / s;returnMath.exp(-d * d); }// 造一家工厂的 48 格功率曲线,单位千瓦//// 工厂和家相反:家的曲线是习惯写的,工厂的曲线是制度写的。// 班次几点开机几点停机写在排班里,不写在概率里。staticdouble[]factoryCurve(Random r){double[]w=newdouble[BINS];for(inti=0; i < BINS; i++) {doublet=i *0.5;doublev;if(t >=8&& t <18) { v =1800+ r.nextDouble() *400;// 生产时段:1800 到 2200 千瓦}elseif((t >=7&& t <8) || (t >=18&& t <19)) { v =600+ r.nextDouble() *300;// 开机与收工的坡}else{ v =60+ r.nextDouble() *40;// 夜里只剩值班照明和伺服的待机} w[i] = v; }returnw; }// 聚合的全部秘密就在这一步:把一条曲线挨格加进总量// 一个个体不重要,加到千万个,轮廓就自己站出来了staticvoidadd(double[] total,double[] part){for(inti=0; i < BINS; i++) { total[i] += part[i]; } }// 打印一条曲线:时刻、数值、字符柱// 柱长按这条曲线自己的最大值归一,只比轮廓不比大小staticvoidprint(String title,double[] w){doublemax=0;for(doublev : w) {if(v > max) { max = v; } } System.out.println("== "+ title +" ==");for(inti=0; i < BINS; i++) {inthh=i /2, mm = (i %2) *30;intbars=(int) Math.round(w[i] / max *40);StringBuilderb=newStringBuilder();for(intk=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 私信)。侵权必究。