算法一千零一夜 · 第二十三夜 电力民生写成的曲线

第二十三夜 电力民生写成的曲线

前言里说过,这条生命线的背后,站着另一群人。他们不爬铁塔,不下工地,守在屏幕前,手里的笔写下代码。我就是其中一个。

2021 年夏天,我进了电力这一行。进场第一天,我没有见到电网,没有见到电厂,也没有见到任何一台和电沾边的设备。我见到的是聊天窗口里甩过来的三个词,OpenADR,AC,RA。

后来我才知道,第一个是国际协议的名字,规范好几百页,查得到,看不懂;第二个,被广泛称作 VEN,AC 只是那间会议室里的叫法,我翻遍聊天记录也没找到全称;第三个,RA,是客户给下的定义,意思是资源聚合,我们要做的,不过是把这个定义变成能跑的程序。

三个词,没有一个能在搜索框里得到回答。这不是推理小说的伏笔,这是我入职的流程。

那天我把那本几百页的规范从头翻到尾,一个字也读不进去。不是英文的事,是每一句话下面都踩着我不认识的地。想读懂那些词,得先认识它们脚下的世界。所以这一夜不讲协议,不讲系统,讲一件最底下的事。电,是怎么走进你家的。

先从你家墙上那一排插座说起。

冰箱,洗衣机,热水器,空调,电视,路由器,还有不知道什么时候多出来的充电头。早上你出门,屋子空了,你以为它们全都睡了。其实没有。冰箱的压机隔一阵就醒,嗡一阵,再睡回去。路由器整夜亮着。热水器为了让你随时有热水,一直在保温。电视插着电,待机的小灯一粒一粒。一间空屋子,每一刻也有几十瓦在悄悄地流。

人在家的时候,这条流就粗了。傍晚回来,厨房先热闹,烧水,炒菜,电饭煲按下去,然后灯一盏一盏亮,电视开了,夏天的空调一开就是一整晚。夜里十一点以后灯灭了,空调还压着,再往后就只剩冰箱和路由器。到凌晨,一个家沉到几十瓦,像睡着的呼吸。

这些事,没有一件是商量好的。你今晚几点回家,炒几个菜,隔壁开不开空调,谁也不通知谁。

月底的电费单,把这些事折成几个数。居民用电按阶梯走,一个月用得越多,超出部分的单价越高,最底下那一档管着大多数人家,2012 年起全国都是这个章法。有的城市还能选分时,深夜的电便宜,白天的贵,价格替电网劝你一句,洗衣机可以挪到半夜去。你多半没理会过这句劝。没关系,记住它。价格开口说话这件事,后面有一整册的故事。

现在把镜头从家里挪开,挪到城边上的一家工厂。

厂区一角有间配电室,铁门半掩,一台变压器嗡嗡地响,柜子上挂着铭牌。全厂的电都从这台变压器过,高压进,低压出,再分到每条产线。它就是工厂版的插座,只不过按吨位算。

工厂的电费单,和家里长得不一样。除了用多少度电,还有一行钱,按一个奇怪的名目收,这个月最猛的那半小时。电网不计较你某一瞬间的峰值,它把每半小时的用量平均成一格,一个月里最高的那一格,就是这家厂的最猛用电时刻(最大需求量)。中国叫它最大需量,日本的电费单上写作デマンド,demand 的音译。基本电费按它算,契约报小了,超出要罚;报大了,一度电不用也照付。所以工厂的电工一年里最紧张的时刻,不是用电最多的时刻,是可能创纪录的那半小时。

在我后来的项目里,这样的工厂和大楼有一个统一的叫法,据点。一个据点,一块表,一簇设备。这个词从下一夜起每一夜都会遇见,先记在这里。

已经有公司在据点的配电室里装盒子,专门盯着最猛的那半小时,因为它值钱。谁在盯,为什么盯,值多少钱,那是后面几夜的事。种子埋下,接着走。

后来我在资料里第一次见到那条曲线。横轴,一天 24 小时。纵轴,功率。曲线从深夜的低谷爬上来,早上隆起一个峰,中午落下去一点,傍晚再隆起一个大得多的峰,夜里又落回去。

我脱口而出,这不就是流量监控图吗。

带我的前辈愣了一下,说,这么理解,对了一半。

对的那一半,它确实是一条千万个点汇成的曲线。错的那一半,流量图统计的是已经发生过的事,这条曲线此时此刻正在被写。你按下的电饭煲,楼下便利店的冰柜,你叫不出名字那家厂的产线,每合上一个开关,曲线就动一笔。它不是记录,它是全城正在做的事。

这样的曲线是怎么来的。

先看一户人家。我写了程序,让它把一家人的一天画出来。深夜是一条几十瓦的平线。七点前后,一个峰,一千瓦上下,烧水做饭,人一走就落回去。午后偶尔一个小峰,那是有人在家的日子。晚上七点,最高的峰,两千瓦往上,炊事,灯,电视,空调,挤在几个钟头里。十一以后落回平线。锯齿乱跳,毫无道理,这一家人的傍晚本来就没有道理,加班,聚餐,追剧到半夜,哪一天和哪一天都不一样。

再把一百户加起来。锯齿还在,可是讲道理了。晚上的峰变宽了,变圆了,凌晨的平线也平得更稳。一百个没商量过的傍晚,加出了一个大致说得出口的轮廓。

锯齿为什么越加越薄。因为每家的任性互不相干,你家电水壶响的时候别人家在睡觉,乱动相互错开,加在一起就互相抵消了一部分。可吃饭的点是共同的,千万个家往同一个钟头挤,这部分不但抵不掉,还越站越直。各自的越摊越薄,共同的越站越直,这就是加法自己的性质。

然后是 1000 万户。

程序老老实实地逐户去加,一户往 48 个格子里各添一个数,1000 万户添完,这条曲线自己站出来了。

凌晨四点,800 兆瓦上下,全城睡着的呼吸。

早上七点半,第一个肩膀,约 7200 兆瓦。

午后一点,一座小丘,约 3000 兆瓦,白天在家的人铺的。

晚上七点,18867 兆瓦,全天的峰顶。

夜里十点以后,一路落回去。

一千万件没有商量过的事,加出一个准时得像时刻表的轮廓。这句话不是修辞。程序再跑一遍,一个数都不会动,18867 就是 18867。明天千万个家庭照样任性,后天照样任性,可他们任性的总和,比列车时刻表还守时。

现在把那家工厂放回去。它最猛的那半小时两千多千瓦,折两兆瓦出头。放在 18867 兆瓦的城市曲线上,占万分之一,图上连一根毛刺都算不上。在电费单上它是巨兽,在这条曲线上它几乎不存在。

可是一家是毛刺,一万家就垒出轮廓。真实的城市曲线里,白天不会凹下去,全城的工厂,商场,写字楼把整个白天抬在高位。民生写着早晚那两个峰,工商业把白天整段抬高,谁也不是谁的地基,它们叠在一起,才是完整的一条。我这一夜的程序只画了民生的那一半,所以它白天凹下去,真城不凹。

一个家任性,一千万个家守时。这不是谁在管着他们,也不是谁在安排他们,这是加法自己的性质。一万个各自独立的任性,加出一个确定的轮廓,加的笔数越多,轮廓越稳。统计学给这件事起了名字,附注里交代,正文里你只需要看见它。

看见它之后,事情就来了。

轮廓稳,意味着明天的曲线今天就能画个八九不离十。既然画得出来,电厂就能排班,几点多烧,几点少烧,哪台检修,哪台待命。你以为电网在随机应变地伺候你,其实它在按一张时刻表迎你,而这张时刻表的底稿,是千万个家庭昨天,和昨天之前的无数个昨天,一起写的。

可预测的尽头是误差。时刻表再准,也有一口对不上的差。天热得比预报狠,球赛一开,千万台电视一起亮,该下的雨没下,空调又多压了一下午。差的那一口,谁来补。

这个问题问出口,第二十四夜就到了。

一个人用电是任性,一千万人用电是日程表。你关灯的那一秒,某台发电机少转了一口,而你永远不会知道是哪一台。

这一夜的三个镜头,一家,一百家,1000 万家加一家工厂,都写成了能跑的程序。种子锁死在那个夏天,谁来跑,都是同一条曲线。

java 复制代码
// ============================================================
// 第二十三夜《电力民生写成的曲线》演示件
//
// 一座城市的用电曲线,是千万个家加一簇工厂叠出来的。
// 本程序打三个镜头:
//   镜头一,一户人家,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();
    }
}
text 复制代码
== 镜头一  一户人家(千瓦) ==
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 ##
text 复制代码
== 镜头二  一百户(千瓦) ==
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 ##
text 复制代码
== 镜头三  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 私信)。侵权必究。

相关推荐
不一样的故事1261 小时前
NB/T 47013.8-2015 附录D 吸枪技术
人工智能·经验分享·算法
通问AI2 小时前
用 200 行 Python 给文档库做「上下文完整度」自检:四个可复算的检查项
算法
吴名氏.2 小时前
Java转AI Agent教程(第五周)Prompt工程
java·人工智能·prompt·ai agent
8Qi82 小时前
DDD架构演进
java·分布式·mvc·ddd
揽星河..2 小时前
【C语言】扫雷游戏
c语言·算法·游戏
paopaokaka_luck2 小时前
灾害应急物资管理系统(AI应急助手、Echarts可视化数据看板、捐赠入库联动、 物资全流程管理、库存变动追溯、灾情资讯与互动反馈、物资发放公示)
java·前端·javascript·spring boot·mysql·tomcat·echarts
丘山录2 小时前
本地部署 LongCat-2.5,要 7 台机器我只有 4 台
人工智能·算法
Allen_LVyingbo2 小时前
NVIDIA CUDA Tile 在医疗计算中的算子形式化、算法重构与内核实现
大数据·python·算法·重构·知识图谱·健康医疗·数据库架构
SL_staff2 小时前
HR系统对接钉钉考勤总失败?根源在于缺乏业务语义级逻辑中枢
java·开源·全栈