mycai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +119 -0
- package/bin/mycai.js +156 -0
- package/core/ghr_lattice_memory.js +401 -0
- package/core/hdc.js +312 -0
- package/core/memory.js +60 -0
- package/core/memory_engine.js +444 -0
- package/core/morphology.js +357 -0
- package/core/phase.js +87 -0
- package/core/reasoning_router.js +628 -0
- package/core/resonance.js +118 -0
- package/core/safetensors.js +62 -0
- package/core/sovereign_agent.js +164 -0
- package/core/spectral.js +259 -0
- package/core/spectral_decoder.js +868 -0
- package/dist/resonance_sdk.js +2703 -0
- package/dist/resonance_sdk.min.js +14 -0
- package/dist/spectral_core.wasm +0 -0
- package/index.d.ts +45 -0
- package/index.js +26 -0
- package/package.json +52 -0
- package/sdk/README.md +154 -0
- package/sdk/examples/banking_assistant.js +61 -0
- package/sdk/examples/browser_example.html +104 -0
- package/sdk/examples/express_integration.js +61 -0
- package/sdk/examples/node_example.js +33 -0
- package/sdk/examples/quickstart.js +55 -0
- package/sdk/index.js +22 -0
- package/sdk/ingestion.js +237 -0
- package/sdk/licensing.js +168 -0
- package/sdk/resonance_engine.js +594 -0
- package/sdk/resonance_sdk.js +792 -0
- package/sdk/storage.js +185 -0
- package/sdk/tests/sdk_verification.js +98 -0
- package/sdk/tests/test_commercial_sdk.js +136 -0
- package/sdk/types.d.ts +152 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turkish Morphology Module
|
|
3
|
+
* Handles normalization, vowel harmony analysis, suffix splitting, and feature extraction.
|
|
4
|
+
*/
|
|
5
|
+
export class TurkishMorphology {
|
|
6
|
+
constructor(roots = []) {
|
|
7
|
+
this.onVowels = new Set(['e', 'i', 'ö', 'ü']);
|
|
8
|
+
this.backVowels = new Set(['a', 'ı', 'o', 'u']);
|
|
9
|
+
this.allVowels = new Set(['a', 'e', 'ı', 'i', 'o', 'ö', 'u', 'ü']);
|
|
10
|
+
|
|
11
|
+
this.customRoots = roots;
|
|
12
|
+
|
|
13
|
+
// Suffix feature dictionary mapping
|
|
14
|
+
this.suffixFeatures = {
|
|
15
|
+
// Plural
|
|
16
|
+
'ler': { number: 'plural' },
|
|
17
|
+
'lar': { number: 'plural' },
|
|
18
|
+
|
|
19
|
+
// Cases
|
|
20
|
+
'den': { case: 'ablative' },
|
|
21
|
+
'dan': { case: 'ablative' },
|
|
22
|
+
'ten': { case: 'ablative' },
|
|
23
|
+
'tan': { case: 'ablative' },
|
|
24
|
+
'de': { case: 'locative' },
|
|
25
|
+
'da': { case: 'locative' },
|
|
26
|
+
'te': { case: 'locative' },
|
|
27
|
+
'ta': { case: 'locative' },
|
|
28
|
+
'e': { case: 'dative' },
|
|
29
|
+
'a': { case: 'dative' },
|
|
30
|
+
'in': { case: 'genitive' },
|
|
31
|
+
'ın': { case: 'genitive' },
|
|
32
|
+
'un': { case: 'genitive' },
|
|
33
|
+
'ün': { case: 'genitive' },
|
|
34
|
+
|
|
35
|
+
// Possessions
|
|
36
|
+
'im': { possession: '1sg' },
|
|
37
|
+
'ım': { possession: '1sg' },
|
|
38
|
+
'um': { possession: '1sg' },
|
|
39
|
+
'üm': { possession: '1sg' },
|
|
40
|
+
'in': { possession: '2sg' }, // overlaps with genitive, handled dynamically
|
|
41
|
+
'ın': { possession: '2sg' },
|
|
42
|
+
'un': { possession: '2sg' },
|
|
43
|
+
'ün': { possession: '2sg' },
|
|
44
|
+
'i': { possession: '3sg' },
|
|
45
|
+
'ı': { possession: '3sg' },
|
|
46
|
+
'u': { possession: '3sg' },
|
|
47
|
+
'ü': { possession: '3sg' },
|
|
48
|
+
'imiz': { possession: '1pl' },
|
|
49
|
+
'ımız': { possession: '1pl' },
|
|
50
|
+
'umuz': { possession: '1pl' },
|
|
51
|
+
'ümüz': { possession: '1pl' },
|
|
52
|
+
'iniz': { possession: '2pl' },
|
|
53
|
+
'ınız': { possession: '2pl' },
|
|
54
|
+
'ünüz': { possession: '2pl' },
|
|
55
|
+
'unuz': { possession: '2pl' },
|
|
56
|
+
'leri': { possession: '3pl' },
|
|
57
|
+
'ları': { possession: '3pl' },
|
|
58
|
+
|
|
59
|
+
// Tense/Aspect/Mood
|
|
60
|
+
'iyor': { tense: 'present' },
|
|
61
|
+
'ıyor': { tense: 'present' },
|
|
62
|
+
'uyor': { tense: 'present' },
|
|
63
|
+
'üyor': { tense: 'present' },
|
|
64
|
+
'ecek': { tense: 'future' },
|
|
65
|
+
'acak': { tense: 'future' },
|
|
66
|
+
'miş': { tense: 'narrative_past' },
|
|
67
|
+
'mış': { tense: 'narrative_past' },
|
|
68
|
+
'müş': { tense: 'narrative_past' },
|
|
69
|
+
'muş': { tense: 'narrative_past' },
|
|
70
|
+
'di': { tense: 'past' },
|
|
71
|
+
'dı': { tense: 'past' },
|
|
72
|
+
'du': { tense: 'past' },
|
|
73
|
+
'dü': { tense: 'past' },
|
|
74
|
+
'ti': { tense: 'past' },
|
|
75
|
+
'tı': { tense: 'past' },
|
|
76
|
+
'tu': { tense: 'past' },
|
|
77
|
+
'tü': { tense: 'past' },
|
|
78
|
+
|
|
79
|
+
// Infinitives and derivations
|
|
80
|
+
'mek': { aspect: 'infinitive' },
|
|
81
|
+
'mak': { aspect: 'infinitive' },
|
|
82
|
+
'lik': { derivation: 'noun' },
|
|
83
|
+
'lık': { derivation: 'noun' },
|
|
84
|
+
'luk': { derivation: 'noun' },
|
|
85
|
+
'lük': { derivation: 'noun' },
|
|
86
|
+
'li': { derivation: 'with' },
|
|
87
|
+
'lı': { derivation: 'with' },
|
|
88
|
+
'lu': { derivation: 'with' },
|
|
89
|
+
'lü': { derivation: 'with' },
|
|
90
|
+
'siz': { derivation: 'without' },
|
|
91
|
+
'sız': { derivation: 'without' },
|
|
92
|
+
'suz': { derivation: 'without' },
|
|
93
|
+
'süz': { derivation: 'without' },
|
|
94
|
+
'ce': { case: 'equative' },
|
|
95
|
+
'ca': { case: 'equative' },
|
|
96
|
+
'ça': { case: 'equative' },
|
|
97
|
+
'çe': { case: 'equative' },
|
|
98
|
+
// Copula (Assertive)
|
|
99
|
+
'dir': { copula: 'assertive' },
|
|
100
|
+
'dır': { copula: 'assertive' },
|
|
101
|
+
'dur': { copula: 'assertive' },
|
|
102
|
+
'dür': { copula: 'assertive' },
|
|
103
|
+
'tir': { copula: 'assertive' },
|
|
104
|
+
'tır': { copula: 'assertive' },
|
|
105
|
+
'tur': { copula: 'assertive' },
|
|
106
|
+
'tür': { copula: 'assertive' },
|
|
107
|
+
// Relative Modifier
|
|
108
|
+
'ki': { pronoun: 'relative' },
|
|
109
|
+
'kiler': { pronoun: 'relative_plural' },
|
|
110
|
+
// Adverbial / Manner
|
|
111
|
+
'cesine': { aspect: 'manner' },
|
|
112
|
+
'casına': { aspect: 'manner' },
|
|
113
|
+
'ken': { adverb: 'temporal' },
|
|
114
|
+
// Moods
|
|
115
|
+
'meli': { mood: 'necessity' },
|
|
116
|
+
'malı': { mood: 'necessity' },
|
|
117
|
+
'se': { mood: 'conditional' },
|
|
118
|
+
'sa': { mood: 'conditional' }
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Known suffixes ordered by length descending for greedy matching
|
|
122
|
+
this.suffixesKnown = Object.keys(this.suffixFeatures).sort((a, b) => b.length - a.length);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Allow dynamic roots fallback
|
|
126
|
+
get roots() {
|
|
127
|
+
if (this.customRoots && this.customRoots.length > 0) {
|
|
128
|
+
return this.customRoots;
|
|
129
|
+
}
|
|
130
|
+
if (typeof globalThis !== 'undefined' && globalThis.TR_CORPUS_ROOTS) {
|
|
131
|
+
return globalThis.TR_CORPUS_ROOTS;
|
|
132
|
+
}
|
|
133
|
+
// Minimal fallback root list
|
|
134
|
+
return [
|
|
135
|
+
// Genel yaygın kökler
|
|
136
|
+
'ev', 'göl', 'araba', 'kitap', 'el', 'baş', 'git', 'gel', 'yap', 'al', 'ver', 'gör',
|
|
137
|
+
'bilgisayar', 'bil', 'öğren', 'anla', 'düşün', 'büyük', 'küçük', 'güzel', 'iyi', 'kötü',
|
|
138
|
+
'yapay', 'zeka', 'beyin', 'sinir', 'dil', 'kelime', 'cümle', 'türkçe', 'bilim', 'teknik',
|
|
139
|
+
'okul', 'öğrenci', 'öğretmen', 'yazılım', 'donanım', 'renk',
|
|
140
|
+
// Vücut / anatomi
|
|
141
|
+
'ayak', 'göz', 'kulak', 'diz', 'omuz', 'karın', 'göğüs', 'sırt', 'boyun', 'kol',
|
|
142
|
+
'bacak', 'parmak', 'bilek', 'topuk', 'alın', 'bel', 'diş', 'burun', 'ağız', 'ciğer',
|
|
143
|
+
'kalp', 'akciğer', 'mide', 'böbrek', 'karaciğer', 'damar',
|
|
144
|
+
// Tıbbi terimler
|
|
145
|
+
'hasta', 'doktor', 'hemşire', 'ilaç', 'doz', 'tanı', 'tedavi', 'ameliyat',
|
|
146
|
+
'hastalık', 'ağrı', 'şişlik', 'ateş', 'nabız', 'tansiyon', 'kan', 'idrar',
|
|
147
|
+
'reçete', 'aşı', 'virüs', 'enfeksiyon', 'yara', 'kırık', 'ödem', 'alerji',
|
|
148
|
+
'şeker', 'diyabet', 'gebelik', 'doğum', 'ölçüm', 'muayene', 'rapor', 'tahlil',
|
|
149
|
+
// Genel isimler
|
|
150
|
+
'yol', 'su', 'ekmek', 'para', 'iş', 'gün', 'yıl', 'saat', 'hafta', 'ay',
|
|
151
|
+
'yer', 'şehir', 'ülke', 'köy', 'insan', 'kadın', 'erkek', 'çocuk', 'anne', 'baba',
|
|
152
|
+
'ad', 'isim', 'aile', 'soru', 'cevap', 'kayıt', 'bilgi', 'durum', 'sonuç',
|
|
153
|
+
// Sıfatlar ve zarflar
|
|
154
|
+
'yeni', 'eski', 'uzun', 'kısa', 'sık', 'az', 'çok', 'son', 'ilk', 'her',
|
|
155
|
+
// Fiiller (ek kökler)
|
|
156
|
+
'oku', 'yaz', 'bak', 'çalış', 'ye', 'iç', 'uyu', 'kalk', 'otur', 'koş',
|
|
157
|
+
'sor', 'söyle', 'dinle', 'bekle', 'başla', 'bitir', 'aç', 'kapat', 'getir', 'götür',
|
|
158
|
+
'koy', 'çıkar', 'kaydet', 'güncelle', 'kontrol'
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
determineVowelHarmony(root) {
|
|
163
|
+
const lv = this.getLastVowel(root);
|
|
164
|
+
return lv && this.onVowels.has(lv) ? 'front' : 'back';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getLastVowel(word) {
|
|
168
|
+
for (let i = word.length - 1; i >= 0; i--) {
|
|
169
|
+
if (this.allVowels.has(word[i])) return word[i];
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
normalize(text) {
|
|
175
|
+
return text.toLowerCase().trim()
|
|
176
|
+
.replace(/i̇/g, 'i') // fix combined i characters
|
|
177
|
+
.replace(/[^a-zçgğıoöşuüâîû\s]/g, ''); // keep valid Turkish characters
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
analyze(word) {
|
|
181
|
+
const w = this.normalize(word).split(/\s+/)[0];
|
|
182
|
+
const result = {
|
|
183
|
+
word: w,
|
|
184
|
+
root: w,
|
|
185
|
+
suffixes: [],
|
|
186
|
+
harmony: this.determineVowelHarmony(w),
|
|
187
|
+
morphemes: [],
|
|
188
|
+
features: {},
|
|
189
|
+
vowelCount: 0,
|
|
190
|
+
syllables: []
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (!w) return result;
|
|
194
|
+
|
|
195
|
+
result.vowelCount = [...w].filter(c => this.allVowels.has(c)).length;
|
|
196
|
+
result.syllables = this.syllabify(w);
|
|
197
|
+
|
|
198
|
+
// Greedy root-suffix splitter matching longest root with reverse consonant mutation support
|
|
199
|
+
let foundRoot = w;
|
|
200
|
+
let foundSuffs = [];
|
|
201
|
+
|
|
202
|
+
for (const root of this.roots) {
|
|
203
|
+
let isMatch = false;
|
|
204
|
+
let matchedLength = root.length;
|
|
205
|
+
|
|
206
|
+
if (w.startsWith(root)) {
|
|
207
|
+
isMatch = true;
|
|
208
|
+
} else {
|
|
209
|
+
// Consonant mutation check (e.g. kitap -> kitabım, git -> gidiyor)
|
|
210
|
+
const last = root[root.length - 1];
|
|
211
|
+
if (['p', 'ç', 't', 'k'].includes(last)) {
|
|
212
|
+
const stem = root.slice(0, -1);
|
|
213
|
+
let mutatedStem = '';
|
|
214
|
+
if (last === 'p') mutatedStem = stem + 'b';
|
|
215
|
+
else if (last === 'ç') mutatedStem = stem + 'c';
|
|
216
|
+
else if (last === 't') mutatedStem = stem + 'd';
|
|
217
|
+
else if (last === 'k') mutatedStem = stem + 'ğ';
|
|
218
|
+
|
|
219
|
+
// Renk -> rengi exception
|
|
220
|
+
if (root === 'renk') mutatedStem = stem + 'g';
|
|
221
|
+
|
|
222
|
+
if (w.startsWith(mutatedStem) && w.length > root.length) {
|
|
223
|
+
// Next char in word must be a vowel for mutation to happen
|
|
224
|
+
const nextChar = w[mutatedStem.length];
|
|
225
|
+
if (this.allVowels.has(nextChar)) {
|
|
226
|
+
isMatch = true;
|
|
227
|
+
matchedLength = mutatedStem.length;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (isMatch && root.length >= 2 && root.length < w.length) {
|
|
234
|
+
const suffix = w.slice(matchedLength);
|
|
235
|
+
if (root.length > (foundRoot === w ? 0 : foundRoot.length)) {
|
|
236
|
+
foundRoot = root;
|
|
237
|
+
foundSuffs = this.splitSuffix(suffix);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
result.root = foundRoot;
|
|
243
|
+
result.suffixes = foundSuffs;
|
|
244
|
+
result.morphemes = [foundRoot, ...foundSuffs].filter(Boolean);
|
|
245
|
+
result.harmony = this.determineVowelHarmony(foundRoot);
|
|
246
|
+
|
|
247
|
+
// Enrich features
|
|
248
|
+
foundSuffs.forEach(suff => {
|
|
249
|
+
const feat = this.suffixFeatures[suff];
|
|
250
|
+
if (feat) {
|
|
251
|
+
Object.assign(result.features, feat);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
splitSuffix(suffix) {
|
|
259
|
+
if (!suffix) return [];
|
|
260
|
+
const result = [];
|
|
261
|
+
let rem = suffix;
|
|
262
|
+
while (rem.length > 0) {
|
|
263
|
+
let matched = false;
|
|
264
|
+
for (const s of this.suffixesKnown) {
|
|
265
|
+
if (rem.endsWith(s)) {
|
|
266
|
+
result.unshift(s);
|
|
267
|
+
rem = rem.slice(0, rem.length - s.length);
|
|
268
|
+
matched = true;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (!matched) {
|
|
273
|
+
result.unshift(rem); // fallback for unmatched leading segment
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
syllabify(word) {
|
|
281
|
+
const w = this.normalize(word);
|
|
282
|
+
if (!w) return [];
|
|
283
|
+
|
|
284
|
+
// Find indices of all vowels
|
|
285
|
+
const vowelIndices = [];
|
|
286
|
+
for (let i = 0; i < w.length; i++) {
|
|
287
|
+
if (this.allVowels.has(w[i])) {
|
|
288
|
+
vowelIndices.push(i);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (vowelIndices.length <= 1) {
|
|
293
|
+
return [w];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const syllables = [];
|
|
297
|
+
let start = 0;
|
|
298
|
+
|
|
299
|
+
// Split word step-by-step based on vowel gaps according to Turkish grammar rules
|
|
300
|
+
for (let k = 0; k < vowelIndices.length - 1; k++) {
|
|
301
|
+
const v1 = vowelIndices[k];
|
|
302
|
+
const v2 = vowelIndices[k + 1];
|
|
303
|
+
const consonantCount = v2 - v1 - 1;
|
|
304
|
+
|
|
305
|
+
let splitPoint;
|
|
306
|
+
if (consonantCount === 0) {
|
|
307
|
+
// V-V -> e.g. fi-il
|
|
308
|
+
splitPoint = v1 + 1;
|
|
309
|
+
} else if (consonantCount === 1) {
|
|
310
|
+
// V-C-V -> division before the consonant ( liaison / ulama rule )
|
|
311
|
+
splitPoint = v1 + 1;
|
|
312
|
+
} else if (consonantCount === 2) {
|
|
313
|
+
// V-C-C-V -> division between the two consonants
|
|
314
|
+
splitPoint = v1 + 2;
|
|
315
|
+
} else {
|
|
316
|
+
// V-C-C-C-V or more -> division before the last consonant
|
|
317
|
+
splitPoint = v2 - 1;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
syllables.push(w.slice(start, splitPoint));
|
|
321
|
+
start = splitPoint;
|
|
322
|
+
}
|
|
323
|
+
syllables.push(w.slice(start));
|
|
324
|
+
return syllables.filter(Boolean);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
syllabifyPhrase(phrase) {
|
|
328
|
+
const words = phrase.toLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
329
|
+
if (words.length === 0) return [];
|
|
330
|
+
if (words.length === 1) return this.syllabify(words[0]);
|
|
331
|
+
|
|
332
|
+
const resultSyllables = [];
|
|
333
|
+
const processedWords = [...words];
|
|
334
|
+
|
|
335
|
+
for (let i = 0; i < processedWords.length - 1; i++) {
|
|
336
|
+
const w1 = processedWords[i];
|
|
337
|
+
const w2 = processedWords[i + 1];
|
|
338
|
+
if (w1.length === 0 || w2.length === 0) continue;
|
|
339
|
+
|
|
340
|
+
const lastChar = w1[w1.length - 1];
|
|
341
|
+
const firstChar = w2[0];
|
|
342
|
+
|
|
343
|
+
// Liaison: ends in consonant, starts in vowel
|
|
344
|
+
if (!this.allVowels.has(lastChar) && this.allVowels.has(firstChar)) {
|
|
345
|
+
processedWords[i] = w1.slice(0, -1);
|
|
346
|
+
processedWords[i + 1] = lastChar + w2;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (const w of processedWords) {
|
|
351
|
+
if (w.length > 0) {
|
|
352
|
+
resultSyllables.push(...this.syllabify(w));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return resultSyllables;
|
|
356
|
+
}
|
|
357
|
+
}
|
package/core/phase.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase Engine Module
|
|
3
|
+
* Analyzes phase components, phase coherence, alignment, and differences in phase-coded HDC vectors.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class PhaseEngine {
|
|
7
|
+
constructor() {}
|
|
8
|
+
|
|
9
|
+
extractPhase(representation) {
|
|
10
|
+
const D = representation.D;
|
|
11
|
+
const phase = new Float32Array(D);
|
|
12
|
+
if (representation.type === 'complex') {
|
|
13
|
+
phase.set(representation.values);
|
|
14
|
+
} else {
|
|
15
|
+
// For real, bipolar, binary, compute phase of complex projection (z = x + 0i)
|
|
16
|
+
for (let i = 0; i < D; i++) {
|
|
17
|
+
phase[i] = representation.values[i] >= 0.0 ? 0.0 : Math.PI;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return phase;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
phaseDifference(a, b) {
|
|
24
|
+
const D = a.D;
|
|
25
|
+
const diff = new Float32Array(D);
|
|
26
|
+
const pA = this.extractPhase(a);
|
|
27
|
+
const pB = this.extractPhase(b);
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < D; i++) {
|
|
30
|
+
let d = pA[i] - pB[i];
|
|
31
|
+
// Wrap to [-PI, PI)
|
|
32
|
+
while (d < -Math.PI) d += 2 * Math.PI;
|
|
33
|
+
while (d >= Math.PI) d -= 2 * Math.PI;
|
|
34
|
+
diff[i] = d;
|
|
35
|
+
}
|
|
36
|
+
return diff;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
phaseAlignment(a, b) {
|
|
40
|
+
// Calculates circular variance of phase difference.
|
|
41
|
+
// 1 - variance ranges from 0 (no alignment/random) to 1 (perfect alignment).
|
|
42
|
+
const diff = this.phaseDifference(a, b);
|
|
43
|
+
let sumCos = 0;
|
|
44
|
+
let sumSin = 0;
|
|
45
|
+
const D = diff.length;
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < D; i++) {
|
|
48
|
+
sumCos += Math.cos(diff[i]);
|
|
49
|
+
sumSin += Math.sin(diff[i]);
|
|
50
|
+
}
|
|
51
|
+
const R = Math.sqrt(sumCos * sumCos + sumSin * sumSin) / D;
|
|
52
|
+
return R; // Resulting vector length
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
phaseCoherence(a, b) {
|
|
56
|
+
// Phase Coherence is the magnitude of the mean phase difference vector:
|
|
57
|
+
// C = | (1/D) * sum( e^{i * (theta_a - theta_b)} ) |
|
|
58
|
+
const pA = this.extractPhase(a);
|
|
59
|
+
const pB = this.extractPhase(b);
|
|
60
|
+
const D = a.D;
|
|
61
|
+
|
|
62
|
+
let sumCos = 0;
|
|
63
|
+
let sumSin = 0;
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < D; i++) {
|
|
66
|
+
const diff = pA[i] - pB[i];
|
|
67
|
+
sumCos += Math.cos(diff);
|
|
68
|
+
sumSin += Math.sin(diff);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Magnitude of average complex phase difference
|
|
72
|
+
return Math.sqrt(sumCos * sumCos + sumSin * sumSin) / D;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
phaseSimilarity(a, b) {
|
|
76
|
+
// Cosine similarity in phase domain: (1/D) * sum( cos(theta_a - theta_b) )
|
|
77
|
+
const pA = this.extractPhase(a);
|
|
78
|
+
const pB = this.extractPhase(b);
|
|
79
|
+
const D = a.D;
|
|
80
|
+
|
|
81
|
+
let sumCos = 0;
|
|
82
|
+
for (let i = 0; i < D; i++) {
|
|
83
|
+
sumCos += Math.cos(pA[i] - pB[i]);
|
|
84
|
+
}
|
|
85
|
+
return sumCos / D;
|
|
86
|
+
}
|
|
87
|
+
}
|