自然は意外と単純なのか?
このワークシートはMath by Codeの一部です。
アート、背景、実装、バリエーションの順に見ていきましょう。
前回は花火のような粒子の動きを
運動法則のようなコードで
作ることができた。
今回は、生物の成長や動きのように、
一見複雑なアートが、
単純なルールに偶然がからむだけで疑似的に作られることを体験しよう。

今回のテーマは、
単純なルール + 偶然
この発想や現象は、
さまざまなところで使われている。
ゲーム、音楽、賭け事、
いろいろあるね。
背景にあるのは、
単純なオブジェクト生成の「ルール」と
偶然を生み出す「乱数」だ。
1つずつ見てみよう。
1.背景
ランダムウォーク、千鳥足というのがある。
酔っ払いがまっすぐ前に歩けず、ふらふらと右へ左へ向きが定まらずに進むようすをさす。
このルールは単純で、進行方向は右前か左前かのどちらか。
どっちに行くかは予測できない。
ここに乱数が使える。
<乱数の数学>
乱数は数列だ。
一次合同式を作ってみる。
初項(乱数の種)をr0、
rn=(a rn-1+b) mod M
としよう。
数列が整数としたら、法Mによる剰余は0からM-1のM通りだから、
いつかは剰余が同じになる。
だから、デタラメな出現に見えるようにするには、Mを巨大な数にすればよいね。
この発想のように、一次合同式を使った疑似乱数を一様乱数uniform randomという。
なぜ、一様というかの理由は簡単で、0からM-1までの数が計算で出現するので、
基本的には、ゾーンに区切れば、出現の確率はどれも同じくらいになるはずだから。
質問:初項seedから一様乱数を生成するコードはどう作ればよいでしょうか。
一次合同式をf(x)=ax+b(mod M) として、初項x=seedとするとき、
geogebraなら、理論上はList=Iteration( f, seed, M)とします。
現実的には、f(x)=ax + bとおき、g(x) =f- floor( f /M) Mとします。
なぜか、Mod(f, M)とすると文法エラーがおきるバグがあるみたいです。
l1=iteration(g, seed, M)とすると、初項seedの数列がM+1番目までのリストになります。
そこで、l3=take(l1, 2, M+1)とすることで、seedのあとのM個の数列ができますね。
l2=unique(l3)とすると、重複のないリストになるね。
このl2のサイズがMであるなら、M個の乱数が重複なく出そろうことになるね。
たとえば、a=2, b=3, M=7, seed=3とすると、集合{0,2,3}で、かたよる。
たとえば、a=5, b=3, M=8, seed=3とすると、集合{0,1,2,3,4,5,6,7}で、出そろう。
「aを8で割って5余る数、
bを奇数、
Mを2のべき乗の数
にすると、一様にM個の剰余が出そろう。」
と言われています。
下のアプレットで、数値を変えて実験してみよう。
一様乱数(実験用)
<乱数と言語>
乱数は偶然性を生み出すのにとても便利だ。
まず、おもな用途は
A. 基本は0と1の間の小数を生み出す。
B. それを拡大して、0とaの間の小数を生み出す。
C. aからb未満の小数を一様に出す。
D. さいころをふるように、整数のaからbまでが出る。b-a+1通りの整数を出す。
実際の言語では、どうなっているのだろうか?
・processingでは、pythonでもp5.jsでも同じ書き方。モジュールのインポートはなし。
A. 0以上1未満の小数を出す。random()
B. 0以上a未満の小数を出す。 random(a)
B. aからb未満の小数をだす。random(a,b)
C. aからbまでの整数をだす。int(random(a,b+1))
・ふつうのpythonではimport randomをして
A. 0以上1未満の小数を出す。random.random()
B. 0以上a未満の小数を出す。random.random(0, a)
C. aからb未満の小数をだす。random.uniform(a, b)
D. aからbまでの整数をだす。random.randint(a, b)
使う関数が一種類ならば、from random import randintのようにかけば、random.はつけなくてよい。
・ふつうのjavascriptでは、モジュールなしだが、Math.をつける。
A. 0以上1未満の小数を出す。Math.random()
B. 0以上a未満の小数を出す。Math.random()*a
C. aからb未満の小数をだす。Math.random()*(b-a)+a
D. aからbまでの整数をだす。Math.floor(Math.random()*(b-a+1))+a
それぞれの言語によって特徴があります。
processing関係の乱数はrandom関数1本で済みます。便利すぎですが、一般的ではありません。
ふつうのpythonは利用者も多いので、この関数を覚えるのがいいと思います。
一方でふつうのjavascriptも利用者が多いのですが、いちいち関数を加工する必要があります。
まあ、B,Cタイプなら、その場対応でなんとかなるでしょう。
しかし、Dタイプを使うときは、
アロー関数などで、次のように定義しておくと便利でしょう。
let randint = (a,b) => Math.floor(Math.random()*(b-a+1))+a;
こうするとrandint(1,6)と書くだけで、さいころの目関数として使えますね。
<正規乱数>
乱数はゲームなどでは一様乱数ですむことが多いかもしれません。
でも、現実の世界では、一様よりも、中心にかたまる、正規分布の方が自然だ。
だからシミュレーションなどでは、正規分布する乱数が使われることが多い。
質問:0以上1未満を返す一様乱数random()から平均m,標準偏差sdの正規乱数を作るには、どうしたらよいでしょうか。
一様乱数はxが0以上1未満のとき、確率密度関数f(x)=1/(1-0)=1となるね。
連続変数の分散と平均の出し方は、離散変数のΣを∫にしただけだから、Σで思い出して、積分に直そう。
平均はE(x)=Σxp(x)=m,分散、つまり、偏差2乗の平均 V(x)=1/nΣ(x-m)2=.......=E(x2)-m2
だから、
xの平均はE(X)=
x2乗の平均はE(X2)=
だから、xの分散は
ということは、
「中心極限定理」から、どんな分布でも、平均値の確率分布は正規分布に収束するので、
一様乱数の平均値を振り続けると、正規分布に収束するでしょう。
一様乱数randomを12回ふった値の合計resは、
平均が、分散が に近づき、その標準偏差がsd=1の平方根=1となる。
見かけは積分記号で一見すごそうにみえるけど、
ただのΣ計算、ただのたし算を連続変数化しただけで、もともと線形な処理だ。
だから、resに線形な処理をして、分散を変形できるはずだ。
たとえば、resから6を引いて平均を0にする。
さらに、標準偏差sd、平均mを指定した正規分布になるようにするには、
res*sd+mをすると、正規乱数として返してくれるね。
たとえば、pythonでやってみよう。
#[IN]Python
#====================
# norm from uniform
import matplotlib.pyplot as plt
import pandas as pd
from random import random
counter = 1000
def norm(ave, sd ):
res = 0
ans=0 ;
for i in range(12):
ans = random()
res +=ans
res -=6
return res *sd + ave
def docalc():
ave = 50
sd = 10
data=[]
for i in range(counter):
datai= norm(ave, sd)
data.append(datai)
#print(data)
df = pd.Series(data)
df.hist(bins=8, range=[20, 80])
plt.show()
docalc()
のような処理を、言語の文法に合わせて実行すればよいでしょう。
一様分布関数から、正規分布に近い出力ができるね。
[OUT]

<ルール>
絵の部品になる図形に単純なルールを決めよう。
(ルール0)
・1個の点を画面の下(南)におく。
・点は1回で北東か北西に進む。どちらにするかはランダム。
・進んだ跡を画面にかく。
このルールでかくと、ランダムウォークに見えるアートができるでしょう。
(ルール1)
・円の1個目は中央におく。
・2個目からの円は平面にデタラメにばらまく。
・追加の円は、一番近い円に移動してから円を画面にかく。
このルールでかくと、冒頭の「植物の枝の成長」のように見えるアートができます。
(ルール2)
・円の1個目は中央におく。
・2個目からの円は平面にデタラメにばらまく。
・追加の円は、他の円に重ならない大きさの円を画面にかく。
このルールでかくと、円の隙間がどんどん埋められていくアートができるでしょう。
(ルール3)
・円は8方位に毎回ランダムに移動できる。
・移動したら、移動したあとを画面にかく。
このルールでかくと、ランダムウォークの8方位バージョンのアートができるでしょう。
あとは、図形や線にランダムまたは、位置に応じた色をつけることで、
さらにアートに変化がでるでしょう。
2.実装
ルール1を実装することで、
冒頭の図をかいてみよう。
<pyhon in processing>
'use strict';
maxCount = 5000 # max count of the cirlces
currentCount = 1
x = []
y = []
r = []
def setup():
size(800, 800)
strokeWeight(0.5)
# first circle
x.append( width/ 2)
y.append( height/2)
r.append(10)
def draw():
global currentCount, x,y,r
background(255)
newR = random(1, 7)
newX = random(newR, width - newR)
newY = random(newR, height - newR)
closestDist = 99999999999999999999 #max of distance
closestIndex = 0
for i in range(currentCount):
newDist = dist(newX, newY, x[i], y[i])
if newDist < closestDist:
closestDist = newDist
closestIndex = i
angle = atan2(newY - y[closestIndex],newX - x[closestIndex])
x.append( x[closestIndex] + cos(angle)*(r[closestIndex] + newR))
y.append( y[closestIndex] + sin(angle)*(r[closestIndex] + newR))
r.append( newR )
currentCount +=1
for i in range(currentCount):
fill(255*x[i]/width,255*y[i]/height,255*r[i]/7) #rule for coloring
ellipse(x[i], y[i], r[i]*2, r[i]*2) #draw circle
if currentCount >= maxCount:
noLoop()
単純で行数も少なく冒頭のキレイなアートができる。
しかし、意味が分かりにくいので、javascriptでクラスを使ってかき、
geogebraで動くようにしよう。
前回と同様に、javascriptで円のクラスを定義する。クラス利用(生成と描画)を分離してかく。
(円の定義)
コンストラクター(生成関数)で決めよう。
半径Rは1から7、X座標はー(画面横幅-円の半径)から(画面横幅-円の半径)とするこで、
円が画面から切れないようにする。Y座標も同様にしよう。
円の色、R,G,Bは255を最大に対して、200,100,50にして、茶色っぽくする。
円の連番は0番にかりに入れる。
・setId関数で円の連番をあとで設定できるようにする。
・setRXY関数で円のサイズR,座標X,Yをあとで変更できるようにする。
・neaness(other)関数で、他の円otherとの距離の2乗を求める。一番近い円を探すときに使おう。
・approach(other)関数で、他の円otherとの中心を結ぶ線にそって、中心どうしの距離が、
半径の和(this.R+other.R)になるまでくっつくときに、自分のX,Y座標を計算して更新する。つまり、くっつける。このスライドをするための角度angleを中心を結ぶ線を斜辺とする直角三角形のx方向の長さとy方向の長さから、そのatanから求めます。Math.atan2関数が使えます。
下の図のように、
this.Xは、半径の和のcos(angle)だけother.Xたし、
this.Yは、半径和のsin(angle)だけother.Yにたします。
・draw(app)関数で、1個の円をかきます。
画面のappletのオブジェクトを受け取り、アプレット画面に対する描画関係のAPI関数を使う。
円を1個かき、fillを1にして中もそめる。ラベルなし、軸なし、格子線なしにする。
(円の利用)
リセットボタンを押す(OnClicked)と、setup関数を実行します。
画面には隠れてますがスラーダーtがアニメーションで動き、最新情報(OnUpdate)でdraw関数を実行します。このdrawは円の1こずつのdrawではなく、画面全体のdrawにかかわります。
・setup関数で画面サイズをwidth,heigtともに600になるようにしつつ、原点が中央にくるようにします。
1個目の円を描く前に、書いてある円をぜんぶ削除します。1個目の円は半径10で原点を中心にかく。
・draw関数で、追加の円をかきます。追加円と既存円との近さを求めて、一番近い円の番号が
closestIIndexです。この番号の円に追加した円をapproachします。
それから、追加円のdrawをしましょう。
・円の集まりリストcirclesに円のインスタンスを追加したり、円の番号をセットするsetIDなどの
タイミングがずれると、ちがう絵になったりするので、このあたりの検証は
前回紹介した、VSコードでgeogebra のwebへの埋め込みなどでデバックするといいね。
冗長にはなるけれども、手続き処理をする書き方よりも、改造がしやすい利点があるね。
approach法
<geogebra by javascript >
let windowggbApplet = ggbApplet;
'use strict';
let circles = []; // 円たち
const maxCount = 5000; // max count of the cirlces
let currentCount = 1;
let width = 600;
let height = 600;
let randint = (a,b) => Math.floor(Math.random()*(b-a+1))+a;
function ggbOnInit() {
setup();
}
class Circle {
constructor() {
this.R = randint(1,7);
this.X = randint(this.R - width , width - this.R);
this.Y = randint(this.R - height, height - this.R);
this.colR= 200;
this.colG= 100;
this.colB= 50;
this.num = 0;
}
setId(num){
this.num =num;
}
setRXY(r,x,y) {
this.R = r;
this.X = x;
this.Y = y;
}
nearness(other){
return Math.pow((this.X- other.X),2) + Math.pow((this.Y- other.Y),2);
}
aproach(other){
let angle = Math.atan2(this.Y - other.Y, this.X - other.X);
this.X= other.X + Math.cos(angle) * (other.R + this.R);
this.Y= other.Y + Math.sin(angle) * (other.R + this.R);
this.colR= this.X *255/width+122;
this.colG= this.Y *255/height+122;
this.colB= this.R *255/7+122;
}
draw(app){
let posinfo = `c${this.num} = Circle(Point({${this.X},${this.Y}}),${this.R})`;
app.evalCommand(posinfo);
app.setColor("c"+this.num, this.colR, this.colG, this.colB);
app.setFilling("c"+this.num,1);//fillingout
app.setLabelVisible("c"+this.num,false);//NO label
app.setAxesVisible(false,false);//No Axes
app.setGridVisible(false);//No Grid
}
}
function setup() {
// 既存の点cオブジェクトあれば削除
if (circles.length > 0) {
for (let i = 0; i < circles.length; i++) {
windowggbApplet.deleteObject("c" + i);
}
}
currentCount = 1;
circles = [ ];
windowggbApplet.setCoordSystem(- width/2, width/2, - height/2, height/2);
//1個目の円をかく。
let firstC = new Circle();
firstC.setId(0);
firstC.setRXY(10,0,0);
circles[0] = firstC;
firstC.draw(windowggbApplet);
}
function draw() {
//円の追加
let closestDist = Number.MAX_VALUE;
let closestIndex = 0;
let newC = new Circle();
// 追加円に、もっとも近い円を探す。
for (let i = 0; i < currentCount; i++) {
let target = circles[i];
let newDist = newC.nearness(target);
if (newDist < closestDist) {
closestDist = newDist;
closestIndex = i;
}
}
//追加円をかく。
newC.setId(currentCount);
newC.aproach(circles[closestIndex]);
circles[currentCount] = newC;
newC.draw(windowggbApplet);
currentCount++;
}
3.バリエーション
質問:ルール2のように、円が重ならないように増えていくようにするにはどのようなコードにしたらよいでしょうか。
円をばらまくところはルール1と同じです。
ただ、円に重ならないようにするしくみを作る必要がありますね。
まず、円のコンストラクタで半径を50にしますが、最低半径は2にします。
そうすると、追加したい円thisと既存の円otherとの隙間がないと追加したものを無効にしましょう。
centerDist(other){ return Math.sqrt(Math.pow((this.X- other.X),2) + Math.pow((this.Y- other.Y),2));}で中心距離を求めます。
circumDist(other){return this.centerDist(other) - other.R -2;}周囲と周囲との最大のすきまを求めます。
noRest(other){return this.circumDist(other)<0 ? true: false;}は隙間がないときにtrueです。
もし、既存の円全部を調べて、追加するすきまがあれば、
一番隙間の狭い円に対して、標準半径50と最低半径2の間でくっつけらたらくっつける調整をします。
fit(other){
let maxR = this.centerDist(other) - other.R;
let res= (minR<=maxR)? true: false;
if(res){
if (maxR < this.R) {
this.R = maxR;
}
}
return res;
}
可能な最大半径をmaxRとして、minR=2以下ならfitは成功とします。さらに、maxRが50を切っているなら
半径を50からmaxRに更新して、隙間にぴったり詰め込めばよいですね。
理論上はこれを、drawで実行すると、毎回円を追加するたびに、隙間に入ります。
実際、VSコードとjsでwebにgeogebraアプレットを埋め込みができます。
しかし、これを素のgeogebraアプレットのスクリプト記述に貼り付けても
必ず動く保証はありません。
ここでは、上のアプレットをコピーして、グローバルjavaスクリプトに貼り付けただけですが、
最初は動きませんでした。しかし、スライダーの最新情報のjavascriptにalert("draw ");を入れたら動きました。
意味がわかりませんね。
同一シートにjavascriptのアプレットが2つ以上あると内部動作が不安定になるとか、裏で動いているgeogebraの謎の機構によるものと思われます。
参考までに、下に貼り付けたコードをのせておきますね。
重ならない円が増殖する
let Applet = ggbApplet;
'use strict';
let circles = []; //
const maxCount = 5000; // max count of the cirlces
let currentCount = 1;
let width = 600;
let height = 600;
let randint = (a,b) => Math.floor(Math.random()*(b-a+1))+a;
let minR = 3;
function ggbOnInit() {
setup();
}
class Circle {
constructor() {
this.R = 50;
this.X = randint(this.R - width , width - this.R);
this.Y = randint(this.R - height, height - this.R);
this.colR= 200;
this.colG= 100;
this.colB= 50;
this.num = 0;
}
setId(num){
this.num =num;
}
setRXY(r,x,y) {
this.R = r;
this.X = x;
this.Y = y;
}
centerDist(other){
//中心間の距離
return Math.sqrt(Math.pow((this.X- other.X),2) + Math.pow((this.Y- other.Y),2));
}
circumDist(other){
//周囲と周囲との最大のすきま
return this.centerDist(other) - other.R -2;
}
noRest(other){
//重なるときtrue
return this.circumDist(other)<0 ? true: false;
}
fit(other){
//一番周囲が近い他円に対して、半径の最適化が成功するとtrue
let maxR = this.centerDist(other) - other.R;
//console.log("this.R,maxR=",this.R,maxR);
let res= (minR<=maxR)? true: false;
if(res){
if (maxR < this.R) {
this.R = maxR;
}
console.log("this.R=",this.R);
}
this.colR= this.X *255/width+122;
this.colG= this.Y *255/height+122;
this.colB= this.R *255/7+122;
return res;
}
draw(app){
let posinfo = `c${this.num} = Circle(Point({${this.X},${this.Y}}),${this.R})`;
app.evalCommand(posinfo);
app.setColor("c"+this.num, this.colR, this.colG, this.colB);
app.setFilling("c"+this.num, 1);//filling
app.setLabelVisible("c"+this.num,false);//NO label
app.setAxesVisible(false,false);//No Axes
app.setGridVisible(false);//No Grid
}
}
function setup() {
//
if (circles.length > 0) {
for (let i = 0; i < circles.length; i++) {
Applet.deleteObject("c" + i);
}
}
currentCount = 1;
circles = []
Applet.setCoordSystem(- width/2, width/2, - height/2, height/2);
//1個目の円をかく。
let firstC = new Circle();
firstC.setId(0);
firstC.setRXY(50,0,0);
circles[0] = firstC;
firstC.draw(Applet);
}
function draw() {
//円の追加
let closestRest = Number.MAX_VALUE;
let closestIndex = 0;
let newC = new Circle();
let IntersectOthers = false;
// 他円との重なりチェック
for (let i = 0; i < currentCount; i++) {
let target = circles[i];
IntersectOthers ||= newC.noRest(target);
}
//console.log("intersect is ",IntersectOthers)
if (IntersectOthers==false){
// 追加円に、もっとも近い円を探す。
for (let i = 0; i < currentCount; i++) {
let target = circles[i];
let newRest = newC.circumDist(target);
if (newRest < closestRest && newRest >= 0) {
closestRest = newRest;
closestIndex = i;
}
}
//半径が範囲内であれば、追加円をかく。
if (newC.fit(circles[closestIndex])){
newC.setId(currentCount);
//console.log("(this , near) =",newC.num, closestIndex);
circles[currentCount] = newC;
newC.draw(Applet);
currentCount++;
}
}
}