File size: 7,505 Bytes
5c61046
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# tda_features.jl β€” Vietoris-Rips β†’ Persistence Barcodes β†’ Feature Vectors

module TDAFeatures

using LinearAlgebra
using Statistics

export VietorisRipsComplex, PersistenceDiagram, Barcode, barcode_to_feature_vector
export compute_persistence, wasserstein_distance, bottleneck_distance
export PersistenceInterval

# ═══════════════════════════════════════════════════════════════════════
# Vietoris-Rips Complex
# ═══════════════════════════════════════════════════════════════════════

struct VietorisRipsComplex
    points::Matrix{Float64}
    max_dim::Int
    epsilon::Float64
    simplices::Vector{Vector{Int}}
    filtration_values::Vector{Float64}
end

function VietorisRipsComplex(points::Matrix{Float64}, epsilon::Float64; max_dim::Int=2)
    n = size(points, 1)
    simplices = Vector{Int}[]
    filt_vals = Float64[]

    for i in 1:n
        push!(simplices, [i])
        push!(filt_vals, 0.0)
    end

    for i in 1:n, j in i+1:n
        d = norm(points[i,:] - points[j,:])
        if d <= epsilon
            push!(simplices, [i, j])
            push!(filt_vals, d)
        end
    end

    if max_dim >= 2
        for i in 1:n, j in i+1:n, k in j+1:n
            d_ij = norm(points[i,:] - points[j,:])
            d_jk = norm(points[j,:] - points[k,:])
            d_ik = norm(points[i,:] - points[k,:])
            if d_ij <= epsilon && d_jk <= epsilon && d_ik <= epsilon
                push!(simplices, [i, j, k])
                push!(filt_vals, max(d_ij, d_jk, d_ik))
            end
        end
    end

    VietorisRipsComplex(points, max_dim, epsilon, simplices, filt_vals)
end

# ═══════════════════════════════════════════════════════════════════════
# Persistent Homology (H0 and H1)
# ═══════════════════════════════════════════════════════════════════════

struct PersistenceInterval
    dim::Int
    birth::Float64
    death::Float64
end

struct PersistenceDiagram
    intervals::Vector{PersistenceInterval}
end

struct Barcode
    H0::Vector{PersistenceInterval}
    H1::Vector{PersistenceInterval}
end

function compute_persistence(vr::VietorisRipsComplex)::Barcode
    n = size(vr.points, 1)
    order = sortperm(vr.filtration_values)

    # H0: Connected components (union-find)
    parent = collect(1:n)
    rank = zeros(Int, n)

    function find(x)
        while parent[x] != x
            parent[x] = parent[parent[x]]
            x = parent[x]
        end
        return x
    end

    function union!(x, y)
        rx, ry = find(x), find(y)
        if rx != ry
            if rank[rx] < rank[ry]
                parent[rx] = ry
            elseif rank[rx] > rank[ry]
                parent[ry] = rx
            else
                parent[ry] = rx
                rank[rx] += 1
            end
            return true
        end
        return false
    end

    H0_intervals = PersistenceInterval[]
    for idx in order
        simp = vr.simplices[idx]
        val = vr.filtration_values[idx]
        if length(simp) == 2
            if union!(simp[1], simp[2])
                push!(H0_intervals, PersistenceInterval(0, 0.0, val))
            end
        end
    end

    max_filt = maximum(vr.filtration_values)
    for i in 1:n
        if find(i) == i
            push!(H0_intervals, PersistenceInterval(0, 0.0, max_filt))
        end
    end

    # H1: Cycles
    H1_intervals = PersistenceInterval[]
    parent_h1 = collect(1:n)
    function find_h1(x)
        while parent_h1[x] != x
            parent_h1[x] = parent_h1[parent_h1[x]]
            x = parent_h1[x]
        end
        return x
    end
    function union_h1!(x, y)
        rx, ry = find_h1(x), find_h1(y)
        if rx != ry
            parent_h1[rx] = ry
            return false
        end
        return true
    end

    for idx in order
        simp = vr.simplices[idx]
        val = vr.filtration_values[idx]
        if length(simp) == 2
            if union_h1!(simp[1], simp[2])
                push!(H1_intervals, PersistenceInterval(1, val, max_filt))
            end
        end
    end

    Barcode(H0_intervals, H1_intervals)
end

# ═══════════════════════════════════════════════════════════════════════
# Barcode β†’ Feature Vector
# ═══════════════════════════════════════════════════════════════════════

function barcode_to_feature_vector(bc::Barcode; n_bins::Int=50, max_filt::Float64=1.0)::Vector{Float64}
    features = Float64[]

    for intervals in [bc.H0, bc.H1]
        if isempty(intervals)
            append!(features, zeros(n_bins))
            continue
        end

        landscape = zeros(n_bins)
        for intv in intervals
            mid = (intv.birth + intv.death) / 2
            half_pers = (intv.death - intv.birth) / 2
            for (i, t) in enumerate(range(0, max_filt, length=n_bins))
                val = max(0.0, half_pers - abs(t - mid))
                landscape[i] = max(landscape[i], val)
            end
        end
        append!(features, landscape)
    end

    push!(features, Float64(length(bc.H0)))
    push!(features, Float64(length(bc.H1)))
    push!(features, sum(i.death - i.birth for i in bc.H0))
    push!(features, sum(i.death - i.birth for i in bc.H1))
    push!(features, maximum([i.death - i.birth for i in bc.H1]; init=0.0))

    return features
end

# ═══════════════════════════════════════════════════════════════════════
# Distances Between Barcodes
# ═══════════════════════════════════════════════════════════════════════

function wasserstein_distance(bc1::Barcode, bc2::Barcode; p::Int=2)::Float64
    dist = 0.0
    for (intervals1, intervals2) in [(bc1.H0, bc2.H0), (bc1.H1, bc2.H1)]
        n1, n2 = length(intervals1), length(intervals2)
        if n1 == 0 && n2 == 0
            continue
        elseif n1 == 0
            dist += sum((i.death - i.birth)^p for i in intervals2)
        elseif n2 == 0
            dist += sum((i.death - i.birth)^p for i in intervals1)
        else
            sorted1 = sort(intervals1, by=i -> i.death - i.birth, rev=true)
            sorted2 = sort(intervals2, by=i -> i.death - i.birth, rev=true)
            for (i1, i2) in zip(sorted1, sorted2)
                dist += abs((i1.death - i1.birth) - (i2.death - i2.birth))^p
            end
        end
    end
    return dist^(1/p)
end

function bottleneck_distance(bc1::Barcode, bc2::Barcode)::Float64
    wasserstein_distance(bc1, bc2; p=100)
end

end # module TDAFeatures