プログラマとして、なにかできることはないのか、、、。
リアルなウイルスに関して、無力感を感じていたところに、以下のエントリに出会った。
なるほど、こうやって可視化することで、パニックにならず冷静な対応をすることができるわけか、、、
このようなコードなら、プログラマには朝飯前? (これは、普段からあまり表には出てこない、プログラマという職業が、みなさまのお役にたてるチャンスかもしれないじゃないですか。)
「感染を封じ込める」を可視化する
「START」ボタンを押すと、入力した条件で、どのくらい感染を封じ込められるのか(広がるのか)がシミュレートされます。
そして、いろいろ数値を変えて実行してみることで、どのパラメータが感染に効果があるのかもわかってきます。
See the Pen I tried to visualize the infection by yamazaki.3104 (@yamazaki3104) on CodePen.
初期条件では、おそらく収束するまでに 80% 近い感染者数になると思います。
トータルの感染者数を減らすには、
・移動速度を減らす → movespeed
・素早く治療する(もしくは閉じこもる) → cure_days
・接触しない(手を洗う、マスクをする?) → range
どれも効きますが、どれが効果があると思いますか?
ここに結論を書くよりも、みなさまで実際に試してみて、なにが効果的なのか各自で感じるとよいと思います。(わたしなりの結論は、このエントリの最後に書きます。)
また、コードもすべて載せておきますので、CodePenからでも、このページからでもご自由にご利用ください。ただし、このコードが正しく動作しているかどうかは、まったく保証できないし、計算された数値をどう解釈するかは各自の責任の範囲で使ってください
。
「こうしたほうがいいよ」とか「こういう条件も追加してシミュレートしてみたいのだけど、、、」といったコメントも歓迎しています。(ぶっちゃけ、プログラマは「すげー!」とか「ありがとう!」という言葉に飢えていると思います。)
全コード
<html>
<body>
<script>
// このコードが正しく動作しているかどうかは、まったく保証できないし、計算された数値をどう解釈するかは各自の責任の範囲で使ってください
// Dashboard.mini.js 2020-06-23
const Dashboard={build_timestamp:"2020-06-23",table:[],bind:t=>{for(let i in t.view){if("[object Function]"!=toString.call(t.view[i]))continue;const e=document.querySelector(i);null!=e&&(e.innerHTML="")}return t.backup=null,t.H={},t.simple_validator="SimpleValidator"in this&&"validation_rule"in t?new SimpleValidator({object:t.validation_rule}):null,Dashboard.table.push(t),t.data},D:()=>{class t{constructor(t,i,e=null){this.T=null,this.X=e,this.C=null,this.N=[],this.S=null,this.P=i,this.A={},this.E={},this.update(t)}I(){this.N=[],this.A={},this.T=null,this.C=null,this.E={},this.P.removeChild(this.S),this.S=null}check_E(){if(null!=this.S){for(const t in this.E){if("on_resize"!=t)continue;const i=`w: ${this.S.offsetWidth}, h: ${this.S.offsetHeight}`;this.E[t].size!=i&&this.E[t].ev_func(this.S),this.E[t].size=i}for(const t of this.N)t.check_E()}}update(i){if("[object Object]"==toString.call(i)){const e={};for(let t of i.atr)for(let i in t)"on_resize"==i?"[object Function]"==toString.call(t[i])&&(this.E[i]={ev_func:t[i],size:""}):e[i]=t[i];const s=i.tag;if(this.T!=s){for(const t of this.N)t.I();null!=this.S&&this.P.removeChild(this.S),""==s?this.S=this.P:("svg"==s&&(this.X="xmlns"in e?e.xmlns:"http://www.w3.org/2000/svg"),"div"==s&&(this.X=null),null==this.X?this.S=document.createElement(s):this.S=document.createElementNS(this.X,s),this.P.appendChild(this.S)),this.T=s,this.C=null,this.A={},this.N=[]}for(const t in this.A)t in e||this.S.removeAttribute(t);for(const t in e)if("[object Function]"==toString.call(e[t]))this.S[t]=(i=>e[t](i));else if(this.S.hasAttribute(t))"input"==s&&"value"==t?this.S.value=e[t]:this.S.setAttribute(t,e[t]);else{const i=document.createAttribute(t);i.value=e[t],this.S.setAttributeNode(i)}if(this.A=e,this.N.length>i.cts.length)for(let t=this.N.length-1;t>=i.cts.length;t--)this.N[t].I(),this.N.pop();for(const e in i.cts)e<this.N.length?this.N[e].update(i.cts[e]):this.N.push(new t(i.cts[e],this.S,this.X))}else null!=this.T&&this.I(),this.C!=i&&(null!=this.S?this.S.data=i:(this.S=document.createTextNode(i),this.P.appendChild(this.S),this.T=null),this.C=i)}}const i=t=>{if("[object Array]"!=toString.call(t))return[];if(t.length<1)return[];let e=[],s=[];for(const n of t)if("[object Object]"==toString.call(n))e.push(n);else if("[object Array]"==toString.call(n))for(const t of i(n))s.push(t);else s.push(n);return s.length<1?[]:"[object Object]"==toString.call(s[0])?s:[{tag:s[0],atr:e,cts:s.slice(1)}]};for(const e of Dashboard.table){for(let t in e.view)if(t in e.H)for(const i of e.H[t])i.check_E();let s="";const n=t=>{if("[object Object]"!=toString.call(t))s+=JSON.stringify(t);else for(const i in t)"ignore"!=i&&(s+=i,n(t[i]))};if(n(e.data),e.backup!=s){e.validation=null,null!==e.simple_validator&&(e.validation=e.simple_validator.validate(e.data),e.validation.validation_rule=e.validation_rule),e.backup=s;for(let s in e.view){if("[object Function]"!=toString.call(e.view[s]))continue;const n=document.querySelector(s);if(null==n)continue;const l=i(e.view[s](e.data,e.validation));s in e.H||(e.H[s]=[]);const o=e.H[s];if(o.length>l.length)for(let t=o.length-1;t>=l.length;t--)o[t].I(),o.pop();for(const i in l)i<o.length?o[i].update(l[i]):o.push(new t(l[i],n))}}}window.requestAnimationFrame(Dashboard.D)}};window.requestAnimationFrame(Dashboard.D),window.addEventListener("resize",Dashboard.D);
</script>
<div id='div_ui' class='Dashboard' ></div>
<div id='div_svg' class='Dashboard' ></div>
<div id='result' class='Dashboard' ></div>
<canvas id='chart' width='555px' height='100px'></canvas>
<script>window.onload = ()=>{
class Point {
constructor() {
this.x = Math.random() * AREA_W
this.y = Math.random() * AREA_H
this.movespeed_x = (Math.random() - 0.5)
this.movespeed_y = (Math.random() - 0.5)
this.stat = 'nomal'
this.elapsed_date = -1 // 経過日数
}
move() { // 移動
this.x += this.movespeed_x * ui.movespeed
this.y += this.movespeed_y * ui.movespeed
if ( this.x < 0 ) { this.x *= -1 ; this.movespeed_x *= -1 }
if ( this.y < 0 ) { this.y *= -1 ; this.movespeed_y *= -1 }
if ( this.x >= AREA_W ) { this.x = AREA_W - (AREA_W - this.x); this.movespeed_x *= -1 }
if ( this.y >= AREA_H ) { this.y = AREA_H - (AREA_H - this.y); this.movespeed_y *= -1 }
}
check( _a ) { // 確認
if ( this.stat != 'have' ) return
this.elapsed_date++ // 経過日数
if ( this.elapsed_date > ui.complete_cure_days ) {
this.stat = 'CompleteCure'
return
}
const r = ui.range
for ( const a of _a )
{
const rx = this.x - a.x
if ( rx > r ) continue
if ( rx < -r ) continue
const ry = this.y - a.y
if ( ry > r ) continue
if ( ry < -r ) continue
if ( rx * rx + ry * ry > r * r ) continue
if ( a.stat == 'nomal' ) {
a.stat = 'have'
a.elapsed_date = 0
}
}
}
}
const ui = Dashboard.bind(
{
data: { people: 1000, movespeed: 10, complete_cure_days: 14, range: 10 },
view: {
'div#div_ui.Dashboard': ( _d ) => [
[ 'div', 'people : ', [ 'input', { type:'number', value: _d.people, oninput: _el=>_d.people = _el.target.value } ] ],
[ 'div', 'movespeed: ', [ 'input', { type:'number', value: _d.movespeed, oninput: _el=>_d.movespeed = _el.target.value } ] ],
[ 'div', 'cure_days: ', [ 'input', { type:'number', value: _d.complete_cure_days, oninput: _el=>_d.complete_cure_days = _el.target.value } ] ],
[ 'div', 'range : ', [ 'input', { type:'number', value: _d.range, oninput: _el=> _d.range = _el.target.value } ] ],
[ 'button', 'START', { onclick: _el=>start_sim( _d.people ) } ],
],
}
} )
const percent = (_p) => { return parseInt( _p / ui.people * 1000 ) / 10 }
const result = Dashboard.bind(
{
data: { nomal_cnt: 0, have_cnt: 0, cure_cnt: 0, day: 0 },
view: {
'div#result.Dashboard': ( _d ) => [
[ 'div', `nomal: ${ _d.nomal_cnt}, ${ percent(_d.nomal_cnt) }%` ],
[ 'div', `have : ${ _d.have_cnt }, ${ percent(_d.have_cnt ) }%` ],
[ 'div', `cure : ${ _d.cure_cnt }, ${ percent(_d.cure_cnt ) }%` ],
[ 'div', `day : ${ _d.day }` ],
]
}
} )
const AREA_W = 555
const AREA_H = 333
const div_svg = Dashboard.bind(
{
data: { p: [] },
view: {
'div#div_svg.Dashboard': (_d) => [
[ 'svg', { width: AREA_W+5*2, height: AREA_H+5*2, stroke: "#111", fill: "#ddd" },
[ 'rect', { x: 0, y: 0, width: AREA_W+5*2, height: AREA_H+5*2, } ],
_d.p.map( (_it)=>
[ 'circle', { cx: _it.x+5, cy: _it.y+5, r: '5px',
fill: _it.stat=='nomal'?'hsl(95, 46%, 75%)':_it.stat=='have'?'red':'hsl(300, 41%, 59%)' } ]
)
]
]
}
} )
const animation = () => {
for ( let p of div_svg.p )
p.move()
for ( let p of div_svg.p )
p.check( div_svg.p )
let nomal_cnt = 0
let have_cnt = 0
let cure_cnt = 0
for ( let p of div_svg.p )
{
if ( p.stat == 'nomal' ) nomal_cnt ++
else if ( p.stat == 'have' ) have_cnt ++
else cure_cnt ++
}
result.nomal_cnt = nomal_cnt
result.have_cnt = have_cnt
result.cure_cnt = cure_cnt
// チャートに線を引く
const ctx = document.querySelector('canvas#chart').getContext('2d')
const draw_line = ( _x1, _y1, _x2, _y2, _color ) => {
ctx.beginPath()
ctx.moveTo( _x1, _y1 )
ctx.lineTo( _x2, _y2 )
ctx.moveTo( _x1-1, _y1 )
ctx.lineTo( _x2-1, _y2 )
ctx.strokeStyle = _color
ctx.stroke()
}
const n = percent( nomal_cnt )
const h = percent( have_cnt )
draw_line( result.day+1, 0, result.day+1, 100, '#fff' )
draw_line( result.day, 0, result.day, n, 'hsl(95, 46%, 75%)' )
draw_line( result.day, n, result.day, n+h, 'red' )
draw_line( result.day, n+h, result.day, 100, 'hsl(300, 41%, 59%)' )
result.day++
if ( result.day > 555 ) result.day = 0
if ( have_cnt > 0 )
window.setTimeout( animation, 1000/20 ) // 20fps
}
const start_sim = () => {
div_svg.p = []
for ( let i=0 ; i<ui.people ; ++i )
div_svg.p.push( new Point() )
div_svg.p[0].stat = 'have'
result.day = 0
window.setTimeout( animation, 1000/20 ) // 20fps
}
}</script>
</body></html>
細かい解説をしなくても、緑の丸がワシャワシャ動いているメインのところを SVG で描いていて、下のチャートの部分は canvas で描いています。どちらも一長一短がありますので、適材適所で使い分けています。
いまのところ、Chrome で動作の確認を行っています。
わたしの考察を書くと
「感染には移動が効く。とにかく一定時間じっとしているといい。ぶっちゃけ感染期間に何人と接触したのか?で決まる」
です。
このコードでは、movespeed が3以下になれば、ほどんと感染が広がらないことが確認できます。
もしくは、以下の2点で感染者数が劇的に変化することがわかると思います。お試しあれ。
movespeed: 5 ← 出歩かない(移動を半分にする)
cure_days: 14
range : 5 ← 感染距離を半分にする(手洗いをする)
みなさま、ふんばりどころです。がんばりましょう。
このコードが少しでもみなさまのお役にたてば幸いです。
Dashboard.js
コードの先頭に minify されているコードは、Dashboard と名付けた UI ライブラリですが、まだ、テストも不十分なので、しばらく minify のままとさせてください。みなさまからの声が集まったら、しっかりテストしたのちに公開、、、するかもしれないです。ってか、UIのテストって、どうやって書くんでしょう??
Dashboard.js に関してもコメント募集中です。よろしくお願いいたします。