Skip to content

Commit 849779f

Browse files
committed
update
1 parent 3ffe066 commit 849779f

File tree

5 files changed

+106
-157
lines changed

5 files changed

+106
-157
lines changed

docs/src/man/rhythmic_analysis.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Rhythmic Analysis
22
=============
3-
The prevalence of poetry in Arabic literature necessitates scientific tool to study the rhythmic signatures. Unfortunately, apart from the fact that there are no tools to do this yet, at least to the best knowledge of the author, there are no resources as well for the statistical methodologies of studying rhythm as well until recently. The recent work of [asaadthesis](@citet) provided initial statistical tools that are now available in Yunir.jl as well. This section will demonstrate the APIs for doing rhythmic analysis based on the methodologies proposed by [asaadthesis](@citet). To do this, there are two types of text that will be studied, one for Arabic poetry and the Holy Qur'an. For the Holy Qur'an, a comprehensive analysis was done by [asaadthesis](@citet), readers are encouraged to read it. As to how to do apply it though using Yunir.jl, this section will cover the details.
3+
The prevalence of poetry in Arabic literature necessitates scientific tool to study the rhythmic signatures. Unfortunately, there are no resources for such methodology until the recent work of [asaadthesis](@citet). This section will demonstrate the APIs for doing rhythmic analysis based on the methodologies proposed by [asaadthesis](@citet). To do this, there are two types of text that will be studied, and these are pre-Islamic poetry and the Holy Qur'an.
44

55
## Arabic Poetry
66
The first data is from a well known author, [Al-Mutanabbi المتنبّي](https://en.wikipedia.org/wiki/Al-Mutanabbi), who authored several poetry including the titled [*'Indeed, every woman with a swaying walk'*](https://www.youtube.com/watch?v=9c1IrQwfYFM), which will be the basis for this section.
@@ -127,6 +127,7 @@ line_syllables[end-1]
127127
```
128128
!!! info "Note"
129129
The indexing is set to `end-1` because the last line of the `texts` variable is a blank line space as seen in the results of the `texts` variable assigned to the mapping function above.
130+
130131
## References
131132
```@bibliography
132133
Pages = ["rhythmic_analysis.md"]

src/Yunir.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ include("utils/decode.jl")
1212
include("utils/normalize.jl")
1313
include("utils/dediac.jl")
1414
include("utils/clean.jl")
15+
include("utils/texts_wrapper.jl")
1516
include("alignment/align.jl")
1617
include("utils/encode.jl")
1718
include("utils/parse.jl")
@@ -30,6 +31,9 @@ export align, score, count_matches, count_aligned, count_mismatches, count_inser
3031
export Alignment, AbstractCAMeLDB, AbstractEncoder, SimpleEncoding
3132
export @transliterator, genproperties
3233

34+
# text wrapper
35+
export Arabic, Buckwalter
36+
3337
# Orthography
3438
export AbstractCharacter, AbstractCharacter, AbstractConsonant, AbstractSolar, AbstractLunar,
3539
AbstractVowel, AbstractTanween, AbstractQuranPauseMark

src/analysis/rhythmic/utils.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ julia> tajweed_timings = Dict{String,Int64}(
349349
"^" => 4
350350
)
351351
julia> syllabic_consistency(segments, tajweed_timings)
352+
```
352353
"""
353354
function syllabic_consistency(segments::Vector{Segment}, syllable_timings::Dict{String,Int64})
354355
segment_scores = Int64[]

src/analysis/rhythmic/vis.jl

Lines changed: 92 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,51 @@
1-
using Colors
2-
using CairoMakie
3-
using GraphMakie
4-
using Graphs
5-
using Makie
1+
@enum VisType last_recited schimiller
62

7-
"""
8-
join(harakaat::Array{Harakaat})
9-
10-
Join function for handling `Harakaat` object. It joins the harakaat together with `?` separator.
11-
12-
```julia-repl
13-
julia> ar_raheem = "ٱلرَّحِيمِ"
14-
"ٱلرَّحِيمِ"
15-
16-
julia> r = Rhyme(true, Syllable(1, 2, 2))
17-
Rhyme(true, Syllable{Int64}(1, 2, 2))
3+
abstract type AbstractRhythmicVisArgs end;
4+
abstract type AbstractSyllable end;
5+
struct RhythmicVis{T} where T <: AbstractRhythmicVisArgs
6+
type::VisType
7+
args::T
8+
end
189

19-
julia> output = r(ar_raheem, true)
20-
Segment("َّحِ?حِيم", Harakaat[Harakaat("َ", false), Harakaat("ِ", false)])
10+
struct LastRecitedVisArgs <: AbstractRhythmicVisArgs
11+
fig_args::Figure
12+
title::String
13+
end
14+
LastRecitedVisArgs() = LastRecitedVisArgs(Figure(resolution=(800, 800)), "")
2115

22-
julia> join(encode(output).harakaat)
23-
"a?i"
24-
```
25-
"""
26-
Base.join(harakaat::Array{Harakaat}, delim::String="?") = join([h.char for h in harakaat], delim)
16+
struct LastRecitedSyllable <: AbstractSyllable
17+
syllable::Buckwalter
18+
end
2719

20+
function (r::RhythmicVis)(texts::Array{Buckwalter})
21+
if r.type == last_recited
22+
y1_chars, y2_chars, y3_chars = last_syllable.(texts)
23+
y1, y1_dict = encode_to_number(y1_chars)
24+
y2, y2_dict = encode_to_number(y2_chars)
25+
y3, y3_dict = encode_to_number(y3_chars)
26+
end
27+
end
2828

2929
"""
30-
Sequence(sequence::Vector{String}, y_axis::Vector{Int64})
30+
last_syllable(text::Buckwalter)
3131
32-
Create a `Sequence` object for the rhythmic `sequence` data, with `y_axis` as its y-axis ticks for plotting.
32+
Extracts the last recited syllable from one to two characters prior and after the said vowel.
3333
"""
34-
struct Sequence
35-
sequence::Vector{String}
36-
yaxis::Vector{Int64}
34+
function last_syllable(text::Buckwalter)::Tuple{Buckwalter,Buckwalter,Buckwalter}
35+
out1 = Buckwalter(replace(text[end-3:end-2], "o" => ""))
36+
out2 = Buckwalter(replace(text[end-3:end-1], "o" => ""))
37+
out3 = Buckwalter(replace(text[end-4:end-1], "o" => ""))
38+
return LastRecitedSyllable(out1), LastRecitedSyllable(out2), LastRecitedSyllable(out3)
3739
end
3840

3941
"""
40-
sequence(segments::Array{Segment}, type::Union{Type{Harakaat},Type{Segment}})
41-
42-
Extracts the sequence of the `segments` by indexing it into `x`` and `y`, where `x` is the index of the segment, and `y` is the index of its vowels or harakaat.
43-
It returns a tuple containing the following `y`, `y_dict` (the mapping dictionary with key represented by `x` and value represented by `y`), `syllables` represented by `x`.
44-
45-
```julia-repl
46-
julia> ar_raheem_alamiyn = ["ٱلرَّحِيمِ", "ٱلْعَٰلَمِينَ"]
47-
2-element Vector{String}:
48-
"ٱلرَّحِيمِ"
49-
"ٱلْعَٰلَمِينَ"
50-
51-
julia> r = Rhyme(true, Syllable(1, 1, 2))
52-
Rhyme(true, Syllable{Int64}(1, 1, 2))
53-
54-
julia> segments = encode.(r.(ar_raheem_alamiyn, true))
55-
2-element Vector{Segment}:
56-
Segment("~aH?Hiy", Harakaat[Harakaat("a", false), Harakaat("i", false)])
57-
Segment("lam?miy", Harakaat[Harakaat("a", false), Harakaat("i", false)])
58-
59-
julia> sequence(segments, Segment)
60-
(["~aH?Hiy", "lam?miy"], [1, 2], Dict("~aH?Hiy" => 1, "lam?miy" => 2))
61-
62-
julia> sequence(segments, Harakaat)
63-
(["a?i", "a?i"], [1, 1], Dict("a?i" => 1))
64-
65-
julia> syllables, y_vec, y_dict = transition(segments, Harakaat)
66-
(["a?i", "a?i"], [1, 1], Dict("a?i" => 1))
67-
68-
julia> using Makie
69-
70-
julia> using CairoMakie
71-
72-
julia> f = Figure(resolution=(500, 500));
73-
74-
julia> a1 = Axis(f[1,1],
75-
xlabel="Ayah Number",
76-
ylabel="Last Pronounced Syllable\n\n\n",
77-
title="Surah Al-Fatihah Rhythmic Patterns\n\n",
78-
yticks=(unique(y_vec), unique(syllables)),
79-
)
80-
Axis with 1 plots:
81-
┗━ Mesh{Tuple{GeometryBasics.Mesh{3, Float32, GeometryBasics.TriangleP{3, Float32, GeometryBasics.PointMeta{3, Float32, Point{3, Float32}, (:normals,), Tuple{Vec{3, Float32}}}}, GeometryBasics.FaceView{GeometryBasics.TriangleP{3, Float32, GeometryBasics.PointMeta{3, Float32, Point{3, Float32}, (:normals,), Tuple{Vec{3, Float32}}}}, GeometryBasics.PointMeta{3, Float32, Point{3, Float32}, (:normals,), Tuple{Vec{3, Float32}}}, GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}, StructArrays.StructVector{GeometryBasics.PointMeta{3, Float32, Point{3, Float32}, (:normals,), Tuple{Vec{3, Float32}}}, @NamedTuple{position::Vector{Point{3, Float32}}, normals::Vector{Vec{3, Float32}}}, Int64}, Vector{GeometryBasics.NgonFace{3, GeometryBasics.OffsetInteger{-1, UInt32}}}}}}}
82-
42+
to_number(texts::Array{LastRecitedSyllable})
8343
84-
julia> lines!(a1, collect(eachindex(syllables)), y_vec)
85-
Lines{Tuple{Vector{Point{2, Float32}}}}
86-
87-
julia> f
88-
```
44+
Converts
8945
"""
90-
function sequence(segments::Array{Segment}, type::Union{Type{Harakaat},Type{Segment}})
91-
if type == Harakaat
92-
syllables = [join(s.harakaat) for s in segments]
93-
else
94-
syllables = [join(s.segment) for s in segments]
95-
end
96-
97-
y = unique(syllables)
98-
y_dict = Dict{String,Int64}()
46+
function to_number(texts::Array{LastRecitedSyllable})
47+
y = unique(map(x -> x.syllable.text, texts))
48+
y_dict = Dict()
9949
for i in eachindex(y)
10050
if i == 1
10151
y_dict[y[i]] = i
@@ -107,81 +57,67 @@ function sequence(segments::Array{Segment}, type::Union{Type{Harakaat},Type{Segm
10757
y_dict[y[i]] = i
10858
end
10959
end
110-
111-
y_vec = Vector{Int64}()
112-
for i in syllables
60+
y_vec = Array{Int64,1}()
61+
for i in ychars
11362
push!(y_vec, y_dict[i])
11463
end
115-
return Sequence(syllables, y_vec)
64+
return y_vec, y_dict # scaling to 100 since algo will fail saying range step cannot 0
11665
end
11766

118-
function transition(seq::Sequence)
119-
seq = seq.sequence
120-
transition_counts = Dict{Tuple{String,String}, Int}()
121-
state_counts = Dict{String, Int}()
122-
123-
for (current, next) in zip(seq, @view seq[2:end])
124-
transition_counts[(current, next)] = get(transition_counts, (current, next), 0) + 1
125-
state_counts[current] = get(state_counts, current, 0) + 1
126-
end
127-
state_counts[seq[end]] = get(state_counts, seq[end], 0) + 1 # Count the last state
128-
129-
transition_probs = Dict{Tuple{String,String}, Float64}()
130-
for ((from, to), count) in transition_counts
131-
transition_probs[(from, to)] = count / state_counts[from]
67+
bw_texts = verses(crps_tbl[1])
68+
y1_chars, y2_chars, y3_chars = last_syllable(bw_texts)
69+
y1, y1_dict = encode_to_number(y1_chars)
70+
y2, y2_dict = encode_to_number(y2_chars)
71+
y3, y3_dict = encode_to_number(y3_chars)
72+
73+
function vis(x1::Array{String,1}, y1::Array{Int64,1};
74+
x2::Union{Nothing,Array{String,1}}=nothing,
75+
x3::Union{Nothing,Array{String,1}}=nothing,
76+
y2::Union{Nothing,Array{Int64,1}}=nothing,
77+
y3::Union{Nothing,Array{Int64,1}}=nothing,
78+
vis_args::LastRecitedVisArgs)
79+
f = vis_args.fig_args;
80+
a1 = Axis(f[1, 1],
81+
ylabel="Last Pronounced Syllable\n\n\n",
82+
title=vis_args.title,
83+
yticks=(unique(y1), unique(string.(x1))),
84+
xticks = collect(eachindex(x1))
85+
)
86+
lines!(a1, collect(eachindex(x1)), y1)
87+
88+
if x2 !== nothing && x3 !== nothing
89+
a2 = Axis(f[2, 1],
90+
ylabel="3 Characters\n\n\n",
91+
yticks=(unique(y2), unique(string.(x2))),
92+
xticks = collect(eachindex(x2))
93+
)
94+
a3 = Axis(f[3, 1],
95+
ylabel="3-4 Characters\n\n",
96+
yticks=(unique(y3), unique(string.(x3))),
97+
xticks = collect(eachindex(x3))
98+
)
99+
hidexdecorations!(a1)
100+
lines!(a2, collect(eachindex(x2)), y2)
101+
hidexdecorations!(a2)
102+
lines!(a3, collect(eachindex(x3)), y3)
103+
elseif x2 !== nothing && x3 === nothing
104+
a2 = Axis(f[2, 1],
105+
ylabel="3 Characters\n\n\n",
106+
yticks=(unique(y2), unique(string.(x2))),
107+
xticks = collect(eachindex(x2))
108+
)
109+
hidexdecorations!(a1)
110+
lines!(a2, collect(eachindex(x2)), y2)
111+
elseif x2 === nothing && x3 !== nothing
112+
a3 = Axis(f[3, 1],
113+
ylabel="3-4 Characters\n\n",
114+
yticks=(unique(y3), unique(string.(x3))),
115+
xticks = collect(eachindex(x3))
116+
)
117+
hidexdecorations!(a1)
118+
lines!(a3, collect(eachindex(x3)), y3)
119+
else
132120
end
133121

134-
return transition_probs
122+
f
135123
end
136-
137-
function GraphMakie.graphplot(probs::Dict{Tuple{String,String}, Float64}, resolution=(500, 500))
138-
states = unique(vcat(first.(keys(probs)), last.(keys(probs))))
139-
n = length(states)
140-
141-
# Create a mapping from states to indices
142-
state_to_index = Dict(state => i for (i, state) in enumerate(states))
143-
index_to_state = Dict(i => state for (state, i) in state_to_index)
144-
145-
# Create the graph
146-
g = DiGraph(n)
147-
148-
# Add edges and calculate edge weights
149-
edge_weights = Float64[]
150-
for ((from, to), prob) in probs
151-
add_edge!(g, state_to_index[from], state_to_index[to])
152-
push!(edge_weights, prob)
153-
end
154-
155-
# Create the plot
156-
f = Figure(resolution = resolution)
157-
ax = Axis(f[1,1])
158-
159-
# Use the actual state labels for nlabels
160-
nlabels = [string(index_to_state[i]) for i in 1:nv(g)]
161-
162-
# Create edge labels (probabilities)
163-
distances = collect(0.05:0.05:ne(g)*0.05)
164-
elabels = repr.(round.(distances, digits=2))
165-
# elabels = [string(round(w, digits=2)) for w in edge_weights]
166-
167-
# Create the graph plot
168-
graphplot!(ax, g;
169-
curve_distance=distances,
170-
nlabels = nlabels,
171-
nlabels_align = (:center, :center),
172-
nlabels_textsize = 14,
173-
node_size = 30,
174-
node_color = :lightblue,
175-
edge_width = 2 .* sqrt.(edge_weights ./ maximum(edge_weights)) .+ 1,
176-
edge_color = [RGB((w/maximum(edge_weights)), 0, 1-(w/maximum(edge_weights))) for w in edge_weights],
177-
elabels_textsize = 12,
178-
elabels_align = (:center, :center),
179-
arrow_size = 15,
180-
elabels = elabels)
181-
182-
hidedecorations!(ax)
183-
hidespines!(ax)
184-
ax.aspect = DataAspect()
185-
186-
return f
187-
end

src/utils/texts_wrapper.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
struct Arabic
2+
text::String
3+
end
4+
5+
struct Buckwalter
6+
text::String
7+
end

0 commit comments

Comments
 (0)