手把手教你用C#做疫情传播仿真

原文:手把手教你用C#做疫情传播仿真

手把手教你用C#做疫情传播仿真

姐妹篇:手把手教你用C#做疫情传播仿真 产品经理版

在上篇文章中,我介绍了用C#做的疫情传播仿真程序的使用和配置,演示了其运行效果,但没有着重讲其中的代码。

今天我将抽丝剥茧,手把手分析程序的架构,以及妙趣横生的细节。

首先来回顾一下运行效果:

注意看,程序中的信息,包含信息统计、城市居民展示和医院展示三个部分,其中居民按状态的不同,显示为不同的颜色。

本文将先从程序员的角度,说说程序中的实现细节,细节中会聊一聊与与Java版的不同,最后进行总结。

细节介绍

细节介绍一 · 从“人”说起

居民类如下所示:

struct Person
{
    public PersonStatus Status;
    public Vector2 Position;
    public float EstimateDays;
    public float Direction;

    public static Person Create(float citySize)
    {
        // ...
    }

    public void Draw(DeviceContext ctx, XResource x)
    {
        // ...
    }

    public void MoveAroundInCity(float dt, float citySize)
    {
        // ...
    }
}

enum PersonStatus
{
    Healthy, // 健康
    InfectedInShadow, // 被感染,处于潜伏期
    Illness, // 发病
    InHospital, // 发病并进入医院
    Cured, // 治愈
    Dead, //死亡
}

一个城市将会模拟5000个居民,因此在设计这个类的时候,应该尽可能地考虑性能、节约内存。

所以,状态最好越少越好,在设计这个类的时候,我谨慎地保留了状态Status、当前位置Position、用于做状态机的EstimateDays和移动方向Direction这四个状态。

细节介绍二 - 居民的状态变更流

居民状态扭转过程如下所示:

 (有传染性,传染给健康人)
    ??   ?       ?
    ??   ?       ?
健康 ? 潜伏期 ? 发病 ? 入院隔离 ? 治愈

                     死亡

其中,健康被感染的验证除了状态检测外,还要由居民之间的距离决定。而是否戴口罩,又会影响其判断距离,这些逻辑用代码表示如下:

const float InffectRate = 0.8f; // 靠得够近时,被携带者感染的机率
static bool WearMask = false; // 是否戴口罩
// 要靠多近,才会触发感染验证
static float SafeDistance() => WearMask ? 1.5f : 3.5f;

void StepDay()
{
    // ...

    // healthy -> infected
    List<int> newlyInffectedIds = new List<int>();
    newlyInffectedIds = healthyIds
        .AsParallel()
        .Where(x =>
        {
            foreach (var infectorId in infectorIds)
            {
                if (Vector2.DistanceSquared(Persons[x].Position, Persons[infectorId].Position) <= SafeDistance() * SafeDistance())
                    return true;
            }
            return false;
        })
        .ToList();

    foreach (int personId in newlyInffectedIds)
    {
        Infect(personId);
    }
}

EstimateDays字段用于控制潜伏期发病到去医院的等待时间治愈时间,这个字段用得较为巧妙。正常可能需要三个字段,但这三种状态之间,不存在状态共享,因此可以使用一个共享的字段来代替。

比如,infected -> illness状态扭转的代码表述如下:

void StepDay()
{
    for (var i = 0; i < Persons.Length; ++i)
    {
        // ... 其它代码

        // infected -> illness
        if (Persons[i].Status == PersonStatus.InfectedInShadow)
        {
            --Persons[i].EstimateDays;
            if (Persons[i].EstimateDays <= 0)
            {
                Persons[i].Status = PersonStatus.Illness;
                Persons[i].EstimateDays = GenerateToHospitalDays();
            }
            continue;
        }
    }

    // ... 其它代码
}

注意,代码中总会使用EstimateDays,来判断是否要进入下一个状态,而进入下一个状态后,便会重新指定新的EstimateDays。通过这样的状态共享,便可为Person类节省许多状态。

细节介绍3 - 性能优化

注意上文中的代码,它原本可能会是一个5000x5000的大循环,而每帧的时间仅仅只有1/60=13.33ms

经过反复思考,我使用了三种方法来优化。

优化1 · 索引与缓存

首先是在城市类City中,我使用了一个索引:

class City
{
    public Person[] Persons;
    private SortedSet<int> infectorIds = new SortedSet<int>();
    private SortedSet<int> healthyIds = new SortedSet<int>();

    // ... 其它代码
}

该索引维护了两个索引infectorIdshealthyIds,保存好这两个索引后,这个双层循环检测性能可以从5000x5000降低到0-2000x2000,最优情况是初期和未期,数据规模趋近于0,最差情况在中期,数据规模趋近于2000x2000,总之会比简单的双层循环快很多。

注意:索引是有明显缺点的,索引的本质是缓存,缓存的本质是状态,状态的属性之一,就是bug,多一份索引,就需要多加一处维护索引的位置,就多加了一层“写bug”的风险。另外索引过多,可能会影响性能。

我会尽我一切努力,不给程序引入额外状态。除非我有一个无法拒绝的理由。

优化2 · 多线程

这算是.NET的福利吧。

如代码所示,我使用了PLINQ,这是从.NET 4.0推出的新玩意,只需一条简单的AsParallel(),就可以让代码几乎不变,就能享受多核CPU带来的性能红利,我完全不需要处理同步等机制。

优化3 · 使用值类型

也如代码所示,我特意为Person类选择了值类型(struct),它的优点在本程序中体现在两处:

一是在于创建时,无需分配堆内存,要知道内存分配需要请求操作系统(就像浏览器请求服务器那样)非常缓慢;

二是值类型数据的值,在内存中是连续的。这对CPU缓存是个天大的好消息。无论是否是现代CPU,对连续型的内存访问,性能总是最高的,在一性能测试中,连续内存与非连续内存的CPU访问速度差,高达50倍之大。

注意:Java中没提供类似于struct这样的关键字,无法自定义值类型。但通过一定技巧,如创建基元类型数组,也能实现高性能的连续内存访问。

我之前写过一篇文章《.NET中的值类型与引用类型》,包含了详情说明(包含缺点与优化、使用场景等)和性能测试。

细节介绍四 - 时间控制

我尝试写过很多游戏和动态模拟器,我认为时间控制的优劣,最能体现出一个模拟器/游戏制作者的用心。一般程序员都喜欢将垂直同步事件当作游戏的心脏,这样最简单,用代码表述如下(已简化):

void Render()
{
    float dt = RenderTimer.LastFrameTimeInSecond;

    Update(dt);
    Draw(ctx);

    SwapChain.Present(1, 0);
}

这样的好处是逻辑可能比较简单,可以在大脑中脑补每秒60帧,然后按60帧设置参数,想事情。

这样一来,更新逻辑Update(dt)可能就会和垂直同步事件强绑定。要知道有些投影仪可能只有50帧,而某些显示器,有144帧;然后就是它也和垂直同步选项强绑定,一旦关闭垂直同步,Update逻辑可能就会过快而导致程序运行不正常。

我的做法是将这些逻辑稍作封装,代码中的配置,只与真实世界中的时间相关,而与垂直同步选项无关:

const float SecondsPerDay = 0.3f; // 模拟器的秒数,对应真实一天

class City
{
    float dayAccumulate = 0;
    public void Update(float dt)
    {
        // step move
        for (var i = 0; i < Persons.Length; ++i)
        {
            Persons[i].MoveAroundInCity(dt, CitySize);
        }

        // step status
        dayAccumulate += dt;
        day += (dt / SecondsPerDay);

        while (dayAccumulate >= SecondsPerDay)
        {
            StepDay();
            dayAccumulate -= SecondsPerDay;
        }
    }
}

注意我使用了一个SecondsPerDay,来控制模拟器的运行速度,将这个值调大或调小,不影响运行的最终结果。

我还使用了一个dayAccumulate值,用于做按“”更新判断,这样的话,无论函数调用频率如何,调用StepDay()时都会确保相隔“一整天”。

细节介绍五 - 缩放管理

和时间管理一样,我认为窗口大小与缩放控制也很重要,否则程序只能以一种固定的分辨率、DPI来运行。我使用的是我自己写的“准”游戏引擎FlysEngine,它基于Direct2D,可以通过矩阵变换轻松地管理好程序缩放:

protected override void OnDraw(DeviceContext ctx)
{
    ctx.Clear(Color.DarkGray);

    float minEdge = Math.Min(ClientSize.Width / 2, ClientSize.Height / 2);
    float scale = minEdge / 540; // relative coordinate
    ctx.Transform =
        Matrix3x2.Scaling(scale) *
        Matrix3x2.Translation(ClientSize.Width / 2, ClientSize.Height / 2);
    City.Draw(ctx, XResource);
}

注意我定义了一个“魔法值”——540,它是FHD 1920x1080中,短边1080的一半。

这样一来,有两个好处。

首先,我程序后面所有代码,都可以按照1920x1080的“相对值”进行设计。无论客户的桌面分辨率是4k UHD还是1366x768,都会以相同的比例做缩放。

其次我还将坐标原点设为屏幕的正中心,这样也更加简化了我的后续代码,比如在控制Person的出生点时,我可以通过极坐标系直接生成:

struct Person
{
    public static Person Create(float citySize)
    {
        float phi = random.NextFloat(0, MathUtil.TwoPi);
        float r = random.NextFloat(0, citySize);
        var p = new Person { Status = PersonStatus.Healthy };
        p.Position.X = MathF.Sin(phi) * r;
        p.Position.Y = -MathF.Cos(phi) * r;
        p.Direction = random.NextFloat(0, MathF.PI * 2);
        return p;
    }

    // 其它代码
}

总结

本文从五个细节聊了我的【.NET疫情传播程序】的代码,其实这些代码不光应用在这个程序中,也应用到了我写过的许多小游戏和模拟器,都非常重要。

所有这些代码都已经上传到我的Githubhttps://github.com/sdcb/2019-ncp-simulation,各位可以自由star/fork/提issue/PR

喜欢的朋友请关注我的微信公众号【.NET骚操作】:

原文地址:https://www.cnblogs.com/lonelyxmas/p/12289619.html

时间: 2024-11-09 03:19:34

手把手教你用C#做疫情传播仿真的相关文章

【电源模块知识】 手把手教你DC-DC转换器做抗干扰

直流转换器在电路设计当中的重要性不用我们多做介绍,它的主要功能是将电压转换为能够固定并且有效的电压,根据不同的功能,DC-DC转换器还有各种各样的分类.其主要应用领域分布在数码相机.手机登便携式产品当中.较大的使用量就使得DC-DC转换器当中一些常见问题逐渐暴露出来,本篇文章我们就来主要探讨一下DC-DC转换器当中的干扰问题,有的高手说,DC-DC转换器当中的问题很大程度都出在DC本身上,为什么这么说呢?下面我们一起来看一下吧. 实际上,在一套完整的电路系统当中,电流在各种元器件和导体间流通的能

手把手教你用FineBI做数据可视化

前些日子公司引进了帆软商业智能FineBI,在接受了简单的培训后,发现这款商业智能软件用作可视分析只用一个词形容的话,那就是"轻盈灵动"!界面简洁.操作流畅,几个步骤就可以创建分析,获得想要的效果.此番学习也算让我入了数据可视化的门,今天就在这里分享我做数据可视化的心得. 先来说说Dashboard,商业智能仪表盘,是可视化分析的重点.它可以组合多个不同的表格,图表控件,所有指标和维度一键生成.很多BI工具在建立全局分析时,组件都是单独设立然后拼接而成,这里FineBI创造性地将分析容

微信测试工程师手把手教你做弱网络模拟测试

微信测试工程师手把手教你做弱网络模拟测试 Posted by 腾讯优测 | 3,152 views 小优有话说: app研发不同于实验室里做研究,哪里有"理想环境". 理想里,用户用着性能卓越的手机,连着畅通无阻的wifi网络. "哇塞!这个app好用到飞起!" 现实是,他们可能正用着你闻所未闻的机型,穿梭于地铁.公交.火车.乡间.大山-.. 信号"若隐若现,扑朔迷离" "我去!又crash了!" "唉,怎么又连不上

手把手教你做关键词匹配项目(搜索引擎)---- 第三天

第三天 小王(运营总监)看到小丁丁整天都在淘宝.百度.魔方.拍拍上面淘关键词,每天花费的时间好长,工作效率又低,拿着这个借口来找到我. 说到:小帅帅,你看小丁丁每天都在淘宝.百度.魔方.拍拍上面淘关键词花费的时间好长,你能不能帮帮忙,看看能不能让系统自己做啦,这样可以节省好多人力,带来的效益多高.(0 其实就是为了掩饰他们懒惰 0) 小帅帅一听到可以带来的效益好高,王总还求着我呢 ,马上 两眼冒着星光,是该好好体现, 解决这个问题就可以体现出我的价值. 小帅帅拍着胸膛保证到:王总,这个小KS啦,

最准确的单点登录SSO图示和讲解(有代码范例)|手把手教做单点登录(SSO)系列之二

写第一篇博客<手把手教做单点登录(SSO)系列之一:概述与示例>,就获得了园子里朋友们热情的评论和推荐,感谢各位. 我那篇文章同时发了CSDN和博客园.对比一下,更感受到博客园童鞋们的技术交流热情:这篇文章在CSDN也有几百阅读量了,但评论区还静悄悄的.博客园才几天就有四十多个回复.二十多位童鞋推荐了. 深受鼓舞,周末没出门,熬了两个夜打磨图示.整理代码,给大家奉上本文. 完整的代码范例已完成,因和本文时序图严格对照,注释整理还需要一些工作,完成后将在下一篇放出.大家下载配置后,本地跑起来会是

UWP Jenkins + NuGet + MSBuild 手把手教你做自动UWP Build 和 App store包

背景 项目上需要做UWP的自动安装包,在以前的公司接触的是TFS来做自动build. 公司要求用Jenkins来做,别笑话我,之前还真不晓得这个东西. 会的同学请看一下支持错误,不会的同学请先自行脑补,我们一步一步的来. 首先我们准备2个安装包,Jenkins,NuGet 都下载最新的好了. 1. 安装Jenkins,下一步下一步.安装好了会自动浏览器跳转到http://localhost:8080/ 如下图 按照提示去C:\Program Files (x86)\Jenkins\secrets

手把手教你做关键词匹配项目(搜索引擎)---- 第六天

第六天 小帅帅周五休息后,精神估计太旺盛了,周末两天就狂欢去了,酒喝高了,把一件重要的事儿给忘记了. 周一重新整装 刺骨上战场. 一来公司,小帅帅终于记得他要做的事情,就迫不及待的整理会议报告(工作总结). 1.上周工作任务: 1) 页面提交关键词到关键词词库 2) 文件导入到关键词词库 3) 自动抓取关键此到关键词词库 2.能力的提升 1) 学会了如何读csv文件 2)  学会了curl 3)  学会了Html Dom parse 3.下周工作任务: 1) 了解下关键词词库的应用 刚写到这儿,

手把手教你做关键词匹配项目(搜索引擎)---- 第七天

第七天 小帅帅拿回去仔细研究了一个晚上. 发现代码其实都是自己写的,就多了一些类,于老大还不是抄的我的代码,心里又鄙视了于老大一番. 其实每个人都有通病,写过程的总是会鄙视写面向对象的,因为他们没体会到面向对象是啥玩意,要让他们写好可得花上好几年的工夫. 小帅帅学编程的时候,明明知道有函数这一概念,知道函数的写法,但是实际上就算一个函数里面几百行代码,也不知道去提前多个函数出来,美其名约:你看我多厉害,几百行代码耶. 小帅帅心里虽然鄙视于老大,但是看到于老大的代码怎么感觉很清爽,一切都那么自然.

手把手教你做关键词匹配项目(搜索引擎)---- 第一天

第一天 收到需求,需求如下: 1. 收集关键词,构建关键词词库. 收到这个任务,第一想法,这还不简单吗? 马上动手创建一个关键词录入界面,保存到数据库. 第一步完成了,哈哈大笑了一天,没想到事情原来如此的简单. $keywords = $_POST["keywords"]; foreach($keywords as $keyword) { #save $keyword to database .............. } 手把手教你做关键词匹配项目(搜索引擎)---- 第一天