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

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

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

https://developer.apple.com/documentation/charts

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

### Rastrigin関数
多変量の非線形関数であり、グローバル最適化問題のベンチマークとして広く使われています。この関数は、単純な構造の中に多数の局所的な極小値を持つため、最適化アルゴリズムの評価に適しています。
- 定義
```math
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次元の場合

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

https://en.wikipedia.org/wiki/Rastrigin_function

## ヒートマップを描画してみる
まずは、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関数をヒートマップで出力することができます。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/610116/d9d314a0-567a-d28a-7b53-c50c80eed875.jpeg">


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

```swift
    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) ```を適用し、正方形のヒートマップを維持します。
```swift
    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で並列処理を行うことによって高速に計算することができます。
```swift
    // グリッドの座標リスト
    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の更新を行うことによって並列処理を描画することができます。

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

```swift
    // 実行時間の測定
    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な任意の処理を渡すだけで実行時間を測定可能です。

## 参考記事
 https://qiita.com/yamakentoc/items/55a7d7264691b2caf592