1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ディップAdvent Calendar 2024

Day 4

Swift Chartsで数値計算を可視化しよう:並列処理によるヒートマップの描画

Posted at

はじめに

Swift Chartsは、Appleが提供するデータ可視化用のフレームワークです。WWDC22で発表され、WWDC24ではさらなる改良が加えられました。本記事では、Swift Chartsを使用して数値計算の結果をヒートマップで可視化する方法を紹介します。

Swift Chartsとは

Swift Chartsは、SwiftUIに統合されたグラフ描画フレームワークであり、シンプルなコードで高度なデータ可視化を実現できます。WWDC24では、ヒートマップやネットワークグラフなどが公式サポートになり、さらにグラ日の軸やスタイルの設定がより直感的になるアップデートが追加されました。

数値計算で使いたい

数値計算(特に多変量関数の評価結果)は、視覚化することでその性質やパターンを理解しやすくなります。本記事では、非線形関数の代表例である Rastrigin関数 を用いて、計算結果をヒートマップとして描画してみます。

Rastrigin関数

多変量の非線形関数であり、グローバル最適化問題のベンチマークとして広く使われています。この関数は、単純な構造の中に多数の局所的な極小値を持つため、最適化アルゴリズムの評価に適しています。

  • 定義
f(\mathbf{x}) = A n + \sum_{i=1}^{n} \left[ x_i^2 - A \cos(2\pi x_i) \right]
  • $f(\mathbf{x}) = (x_1, x_2, \ldots, x_n)$ は $n$ 次元の入力ベクトル
  • $A$ は定数(一般的に$A = 10$とされる)

2次元の場合

f(x, y) = A \cdot 2 + (x^2 - A \cos(2 \pi x)) + (y^2 - A \cos(2 \pi y))

ヒートマップを描画してみる

まずは、Swift Chartsを使って固定されたデータをヒートマップとして描画します。

HeatmapView.swift
import SwiftUI
import Charts

struct graphData: Identifiable {
    let id = UUID()
    let x: Double
    let y: Double
    let value: Double
}

struct GraphView: View {
    let A: Double = 10.0
    let gridSize = 50 // グリッド分割数(50×50マス)

    // Rastrigin関数の計算
    func rastrigin(x: Double, y: Double) -> Double {
        return A * 2 + (x * x - A * cos(2 * .pi * x)) + (y * y - A * cos(2 * .pi * y))
    }

    // データ生成
    var data: [graphData] {
        let range = stride(from: -5.0, to: 5.0, by: 10.0 / Double(gridSize))
        var points: [graphData] = []

        for x in range {
            for y in range {
                let value = rastrigin(x: x, y: y)
                points.append(graphData(x: x, y: y, value: value))
            }
        }
        return points
    }

    // グラデーションからーの設定
    func gradientColor(for value: Double, minValue: Double, maxValue: Double) -> Color {
        let normalizedValue: Double
        if maxValue != minValue {
            normalizedValue = (value - minValue) / (maxValue - minValue)
        } else {
            normalizedValue = 0.0 // 範囲がない場合
        }
        return Color(hue: 0.6 - (normalizedValue * 0.6), saturation: 1.0, brightness: 1.0)
    }

    var body: some View {
        GeometryReader { geometry in
            let size = min(geometry.size.width, geometry.size.height) * 0.9

            // 値の最小・最大を計算
            let minValue = data.map { $0.value }.min() ?? 0
            let maxValue = data.map { $0.value }.max() ?? 1

            Chart {
                ForEach(data) { point in
                    RectangleMark(
                        x: .value("X", point.x),
                        y: .value("Y", point.y),
                        width: .fixed(size / CGFloat(gridSize)),
                        height: .fixed(size / CGFloat(gridSize))
                    )
                    .foregroundStyle(gradientColor(for: point.value, minValue: minValue, maxValue: maxValue))
                }
            }
            .frame(width: size, height: size)
            .aspectRatio(1, contentMode: .fit)
            .position(x: geometry.size.width / 2, y: geometry.size.height / 2)
        }
        .padding()
    }
}

このようにRastrigin関数をヒートマップで出力することができます。

ヒートマップの色付け用の関数を設定します。valueを、全データの最小値から最大値の間で正規化し、値に応じた色(青から赤)を返します。

    func gradientColor(for value: Double, minValue: Double, maxValue: Double) -> Color {
        let normalizedValue: Double
        if maxValue != minValue {
            normalizedValue = (value - minValue) / (maxValue - minValue)
        } else {
            normalizedValue = 0.0 // 範囲がない場合
        }
        return Color(hue: 0.6 - (normalizedValue * 0.6), saturation: 1.0, brightness: 1.0)
    }

四角形のRectangleMarkでヒートマップを描画します。aspectRatio(1, contentMode: .fit) を適用し、正方形のヒートマップを維持します。

    Chart {
        ForEach(data) { point in
            RectangleMark(
                x: .value("X", point.x),
                y: .value("Y", point.y),
                width: .fixed(size / CGFloat(gridSize)),
                height: .fixed(size / CGFloat(gridSize))
            )
            .foregroundStyle(gradientColor(for: point.value, minValue: minValue, maxValue: maxValue))
        }
    }
    .frame(width: size, height: size)
    .aspectRatio(1, contentMode: .fit)
    .position(x: geometry.size.width / 2, y: geometry.size.height / 2)

ゆっくりとコマ送りで描画してみる

Swift Concurrencyで並列処理してみる

Swift Concurrencyで並列処理を行うことによって高速に計算することができます。

    // グリッドの座標リスト
    var gridPoints: [(x: Double, y: Double)] {
        let range = stride(from: -5.0, to: 5.0, by: 10.0 / Double(gridSize))
        return range.flatMap { x in
            range.map { y in
                (x: x, y: y)
            }
        }
    }

    // 並列計算を開始
    func calculateHeatMapData() async {
        isCalculating = true
        let points = gridPoints

        // TaskGroupを使用して並列計算
        await withTaskGroup(of: ConcurrentRastriginData?.self) { group in
            for point in points {
                group.addTask {
                    let value = await rastrigin(x: point.x, y: point.y)
                    return ConcurrentRastriginData(x: point.x, y: point.y, value: value)
                }
            }
            for await result in group {
                if let dataPoint = result {
                    await MainActor.run {
                        data.append(dataPoint)
                    }
                }
            }
        }

        isCalculating = false
    }

TaskGroupを使用して並列計算を行います。結果を収集しながらUIの更新を行うことによって並列処理を描画することができます。

直列と並列処理の比較

直列で計算した時との比較を行います。

    // 実行時間の測定
    func measureExecutionTime<T>(_ block: () async throws -> T) async -> Result<(result: T, time: Double), Error> {
        let start = DispatchTime.now()
        do {
            let result = try await block()
            let end = DispatchTime.now()
            let elapsed = Double(end.uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000_000
            return .success((result: result, time: elapsed))
        } catch {
            return .failure(error)
        }
    }

DispatchTimeを使うことによってナノ秒単位で計測することができます。async throwsな任意の処理を渡すだけで実行時間を測定可能です。

参考記事

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?