私は以前に関数型言語で少量のプログラミングを行ったことがありますが、Clojureで遊び始めたばかりです。同じ種類の「HelloWorld」プログラムを実行すると、新しい言語を学習するときに古くなるので、Cinderの「Hello、Cinder」チュートリアルを実行して、途中でClojureとQuilに翻訳することにしました。チュートリアルの第5章では、粒子のリストの加速度を計算するために、このC++スニペットに出くわします。
void ParticleController::repulseParticles() {
for( list<Particle>::iterator p1 = mParticles.begin(); p1 != mParticles.end(); ++p1 ) {
list<Particle>::iterator p2 = p1;
for( ++p2; p2 != mParticles.end(); ++p2 ) {
Vec2f dir = p1->mLoc - p2->mLoc;
float distSqrd = dir.lengthSquared();
if( distSqrd > 0.0f ){
dir.normalize();
float F = 1.0f/distSqrd;
p1->mAcc += dir * ( F / p1->mMass );
p2->mAcc -= dir * ( F / p2->mMass );
}
}
}
}
私の目には、このコードには非常に重要な特徴が1つあります。それは、パーティクルのペアを比較し、両方のパーティクルを更新してから、将来同じ組み合わせをスキップすることです。これはパフォーマンス上の理由から非常に重要です。このコードはフレームごとに1回実行され、常に何千ものパーティクルが画面に表示される可能性があるためです(私よりも大きなOをよく理解している人は、おそらくこの方法の違いを教えてくれるでしょう。そして、すべての組み合わせを複数回繰り返します)。
参考までに、私が思いついたものを示します。以下のコードは一度に1つのパーティクルのみを更新するため、同じパーティクルを2回比較する多くの「余分な」作業を行っていることに注意してください。(注:「正規化」など、簡潔にするために省略されたメソッドもあります):
(defn calculate-acceleration [particle1 particle2]
(let [x-distance-between (- (:x particle1) (:x particle2))
y-distance-between (- (:y particle1) (:y particle2))
distance-squared (+ (* x-distance-between x-distance-between) (* y-distance-between y-distance-between))
normalized-direction (normalize x-distance-between y-distance-between)
force (if (> distance-squared 0) (/ (/ 1.0 distance-squared) (:mass particle1)) 0)]
{:x (+ (:x (:accel particle1)) (* (first normalized-direction) force)) :y (+ (:y (:accel particle1)) (* (second normalized-direction) force))}))
(defn update-acceleration [particle particles]
(assoc particle :accel (reduce #(do {:x (+ (:x %) (:x %2)) :y (+ (:y %) (:y %2))}) {:x 0 :y 0} (for [p particles :when (not= particle p)] (calculate-acceleration particle p)))))
(def particles (map #(update-acceleration % particles) particles))
更新:誰かが興味を持った場合に備えて、私が最終的に思いついたのは次のとおりです。
(defn get-new-accelerations [particles]
(let [particle-combinations (combinations particles 2)
new-accelerations (map #(calculate-acceleration (first %) (second %)) particle-combinations)
new-accelerations-grouped (for [p particles]
(filter #(not (nil? %))
(map
#(cond (= (first %) p) %2
(= (second %) p) (vec-scale %2 -1))
particle-combinations new-accelerations)))]
(map #(reduce (fn [accum accel] (if (not (nil? accel)) (vec-add accel accum))) {:x 0 :y 0} %)
new-accelerations-grouped)))
基本的に、プロセスは次のようになります。
- 粒子の組み合わせ:組み合わせ論の「組み合わせ」関数を使用して、粒子のすべての組み合わせを計算します
- new-accelerations:組み合わせのリストに基づいて加速のリストを計算します
- new-accelerations-grouped:すべてのパーティクルをループして組み合わせのリストをチェックし、各サブリストがすべての個別の加速度であるリストのリストを作成することにより、各パーティクルの加速度を(順番に)グループ化します。パーティクルが組み合わせリストの最初のエントリである場合は元の加速度を取得し、2番目の場合は反対の加速度を取得するという微妙な点もあります。次に、nilsを除外します
- 加速度の各サブリストをそれらの加速度の合計に減らします
今の問題は、これは私が以前行っていたよりも速いのでしょうか?(私はまだそれをテストしていませんが、私の最初の推測は方法ではありません)。
アップデート2: これが私が思いついた別のバージョンです。このバージョンは、上記で投稿したバージョンよりもすべての点ではるかに優れていると思います。新しいリストのパフォーマンス/簡単な可変性のために一時的なデータ構造を使用し、ループ/繰り返しを使用します。上記の例よりもはるかに高速であるはずですが、まだ検証していません。
(defn transient-particle-accelerations [particles]
(let [num-of-particles (count particles)]
(loop [i 0 new-particles (transient particles)]
(if (< i (- num-of-particles 1))
(do
(loop [j (inc i)]
(if (< j num-of-particles)
(let [p1 (nth particles i)
p2 (nth particles j)
new-p1 (nth new-particles i)
new-p2 (nth new-particles j)
new-acceleration (calculate-acceleration p1 p2)]
(assoc! new-particles i (assoc new-p1 :accel (vec-add (:accel new-p1) new-acceleration)))
(assoc! new-particles j (assoc new-p2 :accel (vec-add (:accel new-p2) (vec-scale new-acceleration -1))))
(recur (inc j)))))
(recur (inc i) new-particles))
(persistent! new-particles)))))