はじめに
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を使って固定されたデータをヒートマップとして描画します。
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な任意の処理を渡すだけで実行時間を測定可能です。
参考記事