semantic-typescript 2.0.0 → 2.0.1

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/readme.fr.md CHANGED
@@ -1,1390 +1,1390 @@
1
- # **Semantic-TypeScript**
2
-
3
- **Flux, indexés.** Vos données, sous un contrôle précis.
4
-
5
- [English](./readme.md) · [简体中文](./readme.cn.md) · [繁體中文](./readme.tw.md) · [日本語](./readme.jp.md) · [한국어](./readme.kr.md) · [Deutsch](./readme.de.md) · Français · [Español](./readme.es.md) · [Русский](./readme.ru.md)
6
-
7
- Une bibliothèque de traitement de flux typée sûre qui unifie **les générateurs JavaScript**, **Java Stream** et
8
- **l'adressage de données par index**. Chaque élément porte un index `bigint`, chaque pipeline est paresseux, et
9
- chaque opération terminale est un choix explicite.
10
-
11
- ```bash
12
- npm install semantic-typescript
13
- ```
14
-
15
- - Zéro dépendance au runtime, pur ESM, fournit ses propres types.
16
- - 684 tests de non-régression : `npm test`.
17
- - Historique des changements complet : [CHANGELOG.md](./CHANGELOG.md).
18
-
19
- ---
20
-
21
- ## Table des matières
22
-
23
- 1. [Philosophie de conception](#1-design-philosophy)
24
- 2. [Architecture](#2-architecture)
25
- 3. [Démarrage rapide](#3-quick-start)
26
- 4. [Fonctions de fabrique](#4-factory-functions)
27
- 5. [Opérateurs de transformation](#5-transformation-operators)
28
- 6. [Collectables et opérations terminales](#6-collectables-and-terminal-operations)
29
- 7. [Statistiques et fenêtres](#7-statistics-and-windows)
30
- 8. [Le modèle d'index](#8-the-index-model)
31
- 9. [Le modèle asynchrone](#9-the-asynchronous-model)
32
- 10. [Analyse périodique : anneaux, champs et scores](#10-periodic-analysis-rings-fields-and-scores)
33
- 11. [Fonctions utilitaires](#11-utility-helpers)
34
- 12. [Performance et pièges](#12-performance-and-pitfalls)
35
- 13. [Anneaux de phase : un cycle exact](#13-phase-rings)
36
- 14. [Analyse du signal : fenêtres, spectres et temps](#14-signal-analysis)
37
- 15. [Licence](#15-license)
38
-
39
- ---
40
-
41
- <a id="1-design-philosophy"></a>
42
- ## 1. Philosophie de conception
43
-
44
- Trois idées, un seul pipeline :
45
-
46
- **L'index, c'est de la donnée.** Chaque élément est adressé par un `bigint`. L'inversion, la rotation, le mélange et
47
- le partitionnement sont de l'arithmétique d'index, pas des opérations sur tampon. Rien n'est matérialisé tant qu'un
48
- collectable ne le demande.
49
-
50
- **La paresse est le défaut.** Une fabrique encapsule un générateur ; un opérateur en encapsule un autre. Aucun
51
- élément n'est demandé tant qu'une opération terminale ne s'exécute, si bien qu'une source infinie est parfaitement
52
- légale tant que quelque chose en aval l'arrête.
53
-
54
- **Les terminaux sont explicites.** `map` renvoie un `Semantic`, pas un tableau. Les valeurs n'apparaissent que
55
- lorsque vous nommez un collectable — `toUnordered()`, `toOrdered()`, `toNumericStatistics()`, `toWindow()` — et
56
- chacun énonce ce qu'il coûte et quel ordre il produit.
57
-
58
- La même discipline s'applique à la moitié mathématique de la bibliothèque. Un `Complex`, un `Angle` ou une
59
- `Frequency` est une valeur immuable dont les invariants sont vérifiés à la construction ; un `Optional` remplace
60
- `null` ; et un anneau de phase énonce exactement combien d'éléments il contient, quelle période il a et
61
- dans quelle mesure il est équilibré.
62
-
63
- ---
64
-
65
- <a id="2-architecture"></a>
66
- ## 2. Architecture
67
-
68
- ```
69
- factory ──▶ Semantic ──▶ Semantic ──▶ Collectable ──▶ result
70
- (source) (map) (filter) (terminal)
71
- ▲
72
- └── every operator returns a new Semantic wrapping a generator
73
- ```
74
-
75
- | Couche | Module | Responsabilité |
76
- | :--- | :--- | :--- |
77
- | Sources | `synchronous/collectors`, `asynchronous/collectors` | Encapsule un itérable, un générateur, une promesse, un blob, une cible d'événement |
78
- | Pipelines | `synchronous/semantic`, `asynchronous/semantic` | Opérateurs paresseux et réécriture d'index |
79
- | Collectables | `synchronous/semantic`, `asynchronous/semantic` | Terminaux ordonnés, non ordonnés, statistiques, fenêtres et personnalisés |
80
- | Collecteurs | `synchronous/collector`, `asynchronous/collector` | Les primitives de pliage sur lesquelles chaque terminal est bâti |
81
- | Mathématiques | `mathematics/*` | Angles, complexes, fréquences, spectres, fenêtres, estimateurs, épicycles, anneaux de phase |
82
- | Structures | `structure/*` | Listes, ensembles, tables, arbres, forêts, anneaux, champs |
83
- | Erreurs | `structure/error` | La hiérarchie typée `SemanticError` et ses gardes de marque |
84
-
85
- Les familles synchrone et asynchrone partagent un seul vocabulaire : les mêmes noms d'opérateurs, la même
86
- sémantique d'index, les mêmes collectables. Les variantes asynchrones se contentent de renvoyer des promesses, et
87
- elles mettent les écritures en file d'attente afin qu'une lecture émise après une écriture l'observe toujours.
88
-
89
- ### 2.1 Ce que cette version a déplacé et cassé
90
-
91
- Trois modules ont changé de place ou de forme, et une série de valeurs par défaut a changé de sens. Chaque ancien comportement reste accessible par une option explicite ; rien n'a été retiré sans remplacement.
92
-
93
- | Changement | Avant | Maintenant |
94
- | :--- | :--- | :--- |
95
- | Forêts | `structure/tree/rootedForest`, `structure/tree/unionFindForest` | `structure/forest/rootedForest`, `structure/forest/unionFindForest` ; `tree/index.ts` ne les réexporte plus |
96
- | Erreurs | des erreurs natives ad hoc | une hiérarchie de quinze classes dans `structure/error.ts`, avec une garde de marque par classe et l'alias `SemanticRangeError` |
97
- | Options d'estimateur | des définitions figées | `mathematics/quantile.ts` exporte `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator` et `VarianceOptions` |
98
- | `quantile` | Hyndman–Fan type 1 | type 7 — R `quantile`, NumPy `linear`, Excel `PERCENTILE.INC` |
99
- | `skewness` / `kurtosis` | `g1` / `g2` | les estimateurs corrigés du biais `G1` / `G2` |
100
- | `variance` / `standardDeviation` | `ddof = 0` | `ddof = 1`, enveloppé dans un `Optional` |
101
- | `decibel` | une fonction `20·log10` | supprimée ; `amplitudeDecibel` (20·log10) et `powerDecibel` (10·log10) |
102
- | `Spectrum.flatness()` | les magnitudes sur les deux moitiés | les puissances sur la moitié unilatérale |
103
- | `Spectrum.bandEnergy()` | la somme brute bilatérale | unilatérale par défaut, avec repliement de la moitié conjuguée |
104
- | `shortTimeFourierTransform` / `spectrogram` | Hann symétrique, trames complètes seulement | Hann périodique, `boundary: "zeros"`, `padded: true` |
105
-
106
- Les chapitres 6, 7 et 10 documentent les remplacements ; les conséquences mesurées sont dans [CHANGELOG.md](./CHANGELOG.md).
107
-
108
- ---
109
-
110
- <a id="3-quick-start"></a>
111
- ## 3. Démarrage rapide
112
-
113
- ```typescript
114
- import {
115
- useOf, useFrom, useRange, useText, useStringify, usePromise
116
- } from "semantic-typescript";
117
-
118
- // Numeric aggregation
119
- const sum: number = useOf(10, 20, 30, 40)
120
- .map(n => n * 2)
121
- .toNumericStatistics()
122
- .summate(); // 200
123
-
124
- // BigInt aggregation
125
- const bigSum: bigint = useOf(10n, 20n, 30n, 40n)
126
- .map(n => n * 2n)
127
- .toBigintStatistics()
128
- .summate(); // 200n
129
-
130
- // Reordering is index arithmetic — collect with toOrdered() for it to take effect
131
- const reversed: number[] = useFrom([1, 2, 3, 4, 5]).reverse().toOrdered().toArray(); // [5,4,3,2,1]
132
- const rotated: number[] = useFrom([1, 2, 3, 4, 5]).translate(2).toOrdered().toArray(); // [4,5,1,2,3]
133
-
134
- // Lazy evaluation over a large range
135
- const multiples: bigint[] = useRange(0n, 1_000_000n)
136
- .filter(n => n % 17n === 0n)
137
- .limit(10n)
138
- .toUnordered()
139
- .toArray(); // [0n,17n,…,153n]
140
-
141
- // Unicode-safe string processing (code points, not UTF-16 units)
142
- useText("semantic 👍").toUnordered().forEach(character => console.log(character));
143
-
144
- // Circular-safe serialisation
145
- const cyclic: any = { a: 1 }; cyclic.self = cyclic;
146
- useStringify(cyclic); // '{"a":1}'
147
-
148
- // Terminals hand the elements to real structures, not only to arrays
149
- useOf(3, 1, 4, 1, 5).toCollectable().toOrderedSet().toArray(); // [1,3,4,5]
150
- useFrom([3, 1, 4, 1, 5]).toCollectable().toBinarySearchTree().size(); // 5n
151
-
152
- // Async sources
153
- const total = await usePromise(Promise.resolve(7)).toNumericStatistics().summate(); // 7
154
- ```
155
-
156
- ---
157
-
158
- <a id="4-factory-functions"></a>
159
- ## 4. Fonctions de fabrique
160
-
161
- La construction est toujours en O(1) — une fabrique se contente d'encapsuler un générateur. Le tableau indique le
162
- coût de *consommation* de `n` éléments, plus l'espace supplémentaire dont la source elle-même a besoin.
163
-
164
- | Fabrique | Signature (résultat) | Temps (consommer n) | Espace (source) | Notes |
165
- | :--- | :--- | :--- | :--- | :--- |
166
- | `useOf` | `useOf<E>(...elements) → SynchronousSemantic<E>` | O(n) | O(n) | Les index vont de 0..n−1 ; l'argument spread est déjà matérialisé |
167
- | `useFrom` | `useFrom<E>(Iterable<E>) → SynchronousSemantic<E>`<br>`useFrom<E>(AsyncIterable<E>) → AsynchronousSemantic<E>` | O(n) | O(1) | Encapsule tout itérable ; itéré une fois à la collecte |
168
- | `useRange` | `useRange<N>(start, end, step?) → SynchronousSemantic<N>` | O(n) | O(1) | `end` est exclusif ; `step` doit être non nul ; `number` et `bigint` fonctionnent tous deux |
169
- | `useFill` | `useFill<E>(element \| supplier, count) → SynchronousSemantic<E>` | O(n) | O(1) | Un fournisseur est appelé une fois par élément |
170
- | `useEmpty` | `useEmpty<E>() → SynchronousSemantic<E>` | O(1) | O(1) | Élément neutre pour `concat` |
171
- | `useText` | `useText(text, delimiter?)` | O(n) | O(n) pour le texte | Itère sur les **points de code**, si bien que les emoji et le CJK restent des éléments uniques |
172
- | `useAttribute` | `useAttribute<T>(object)` | O(v + e) | O(d + v) | Parcourt récursivement les objets imbriqués avec protection anti-cycle |
173
- | `useGenerate` | `useGenerate<E>(supplier, interrupt)` | O(n) × coût du fournisseur | O(1) | **Doit** avoir un `interrupt` d'arrêt ou un `limit` en aval |
174
- | `useIterate` | `useIterate<E>(Iterable<E>)` / `(AsyncIterable<E>)` | O(n) par collecte | O(1) | Rejoue l'itérateur à chaque collecte |
175
- | `useInterval` | `useInterval(period, delay?) → AsynchronousSemantic<number>` | n × period (temps réel) | O(1) | Émet le temps écoulé |
176
- | `usePromise` | `usePromise<T>(promise) → AsynchronousSemantic<T>` | O(1) | O(1) | Un seul élément à l'index `0n` |
177
- | `useBlob` | `useBlob(blob, chunk?) → AsynchronousSemantic<Uint8Array>` | O(blob size) | O(chunk) par copie | Le morceau par défaut est de 64 Kio |
178
- | `useSubscription` | `useSubscription(target, subscriber, event) → AsynchronousSemantic<Event>` | un élément par événement | O(1) | `limit()` / `takeWhile()` définissent la frontière de désabonnement |
179
-
180
- ```typescript
181
- useOf(1, 2, 3).toUnordered().toArray(); // [1,2,3]
182
- useFrom(new Set([1, 2, 2])).toUnordered().toArray(); // [1,2]
183
- useRange(0, 5).toUnordered().toArray(); // [0,1,2,3,4]
184
- useRange(10n, 0n, -5n).toUnordered().toArray(); // [10n,5n]
185
- useFill("x", 3n).toUnordered().toArray(); // ["x","x","x"]
186
- useText("A😊B").toUnordered().toArray(); // ["A","😊","B"]
187
- useGenerate(() => 7, (_e, index) => index >= 3n).toUnordered().toArray(); // [7,7,7]
188
- ```
189
-
190
- ---
191
-
192
- <a id="5-transformation-operators"></a>
193
- ## 5. Opérateurs de transformation
194
-
195
- Chaque opérateur renvoie un nouveau `Semantic` ; aucun ne touche un élément tant qu'un collectable ne s'exécute.
196
- Le temps est le coût supplémentaire **par élément** ; l'espace est ce que l'opérateur conserve pendant l'exécution.
197
-
198
- | Opérateur | Objectif | Effet sur l'index | Temps | Espace |
199
- | :--- | :--- | :--- | :--- | :--- |
200
- | `map(mapper)` | transformation 1:1 | transmis | O(1) | O(1) |
201
- | `filter(predicate)` | conserve les éléments correspondants | transmis | O(1) | O(1) |
202
- | `flat(mapper)` / `flatMap(mapper)` | aplatissement 1:N | re-indexé à partir de 0 | O(1) par élément émis | O(1) |
203
- | `distinct(keyExtractor?)` | supprime les clés répétées, conserve la première | transmis | O(1) en moyenne | O(n) |
204
- | `peek(consumer)` | effet de bord, l'élément passe à travers | transmis | O(1) | O(1) |
205
- | `limit(count)` | prend les *n* premiers | re-indexé à partir de 0 | O(1) | O(1) |
206
- | `skip(count)` | supprime les *n* premiers | transmis | O(1) | O(1) |
207
- | `sub(start, end)` | tranche positionnelle `[start, end)` | transmis | O(1) | O(1) |
208
- | `takeWhile(predicate)` | prend tant que le prédicat est vrai | transmis | O(1) | O(1) |
209
- | `dropWhile(predicate)` | supprime tant que le prédicat est vrai | re-indexé à partir de 0 | O(1) | O(1) |
210
- | `concat(other)` | ajoute un semantic ou un itérable | poursuit l'index | O(1) | O(1) |
211
- | `redirect(redirector)` | calcule un nouvel index comme vous le souhaitez | **le vôtre** | O(1) | O(1) |
212
- | `reverse()` | `redirect((_e, i) => -i - 1n)` | négatif et décalé | O(1) | O(1) |
213
- | `translate(offset \| mapper)` | décale chaque index | `i + offset` | O(1) | O(1) |
214
- | `shuffle(mapper?)` | rend l'index aléatoire | aléatoire | O(1) | O(1) |
215
- | `pipe(conversion)` | confie le générateur à une fonction personnalisée | à votre discrétion | le vôtre | le vôtre |
216
- | `semantic()` | remonte un collectable dans un semantic | inchangé | O(1) | O(1) |
217
-
218
- **`reverse`, `translate` et `shuffle` ne réécrivent que des index.** Rien n'est mis en tampon ni tourné tant que
219
- `toOrdered()` n'a pas normalisé les index modulo le nombre d'éléments :
220
-
221
- ```typescript
222
- useFrom([1, 2, 3, 4, 5]).reverse().toOrdered().toArray(); // [5,4,3,2,1]
223
- useFrom([1, 2, 3, 4, 5]).translate(2).toOrdered().toArray(); // [4,5,1,2,3] (rotation)
224
- useFrom([1, 2, 3, 4, 5]).translate(7).toOrdered().toArray(); // [4,5,1,2,3] (7 ≡ 2 mod 5)
225
- useFrom([1, 2, 3]).shuffle((_e, i) => -i - 1n).toOrdered().toArray(); // [3,2,1]
226
- ```
227
-
228
- ---
229
-
230
- <a id="6-collectables-and-terminal-operations"></a>
231
- ## 6. Collectables et opérations terminales
232
-
233
- ### 6.1 Collectables
234
-
235
- Le collectable possède à la fois l'**ordre** et la **comptabilité des index**. `n` est le nombre d'éléments.
236
-
237
- | Collectable | Structure | Temps | Ordre | À utiliser quand |
238
- | :--- | :--- | :--- | :--- | :--- |
239
- | `toUnordered()` | `Map<bigint, E>` indexée par l'index | O(n) | insertion | débit maximal ; les index bruts sont préservés intacts |
240
- | `toOrdered()` | tableau trié par index d'élément | O(n log n) | par index normalisé | une inversion/rotation doit prendre effet |
241
- | `toOrdered(comparator)` | trié par votre comparateur | O(n log n) | par votre comparateur | vues triées |
242
- | `toNumericStatistics()` | tampon trié par valeur | O(n log n) | par valeur | statistiques sur `number` |
243
- | `toBigintStatistics()` | tampon trié par valeur | O(n log n) | par valeur | statistiques sur `bigint` |
244
- | `toWindow()` | tampon trié par valeur | O(n log n) | par valeur | `slide` / `tumble` |
245
- | `toCollectable(mapper)` | ce que renvoie le mapper | — | — | collectables personnalisés |
246
-
247
- ### 6.2 Terminaux
248
-
249
- Chaque terminal vit sur le **collectable**, pas sur le `Semantic`. C'est ce que « les terminaux sont explicites » veut dire en pratique : un pipeline ne produit rien tant qu'un collectable n'est pas nommé, et `toCollectable()` sans mapper est la façon la moins coûteuse de le nommer.
250
-
251
- | Terminal | Renvoie | Temps | Notes |
252
- | :--- | :--- | :--- | :--- |
253
- | `toArray()` / `toSet()` | `Array<E>` / `Set<E>` | O(n) | Vide le tampon ; `toSet` dédoublonne par identité |
254
- | `toMap(key, value?)` | `Map<K, V>` | O(n) en moyenne | La dernière écriture l'emporte |
255
- | `count()` / `isEmpty()` | `bigint` / `boolean` | O(1) | Le tampon le sait déjà |
256
- | `forEach(action)` | `void` | O(n) | `action(element, index)` |
257
- | `join(…)` | `string` | O(n) | `join()`, `join(delimiter)`, `join(prefix, delimiter, suffix)` |
258
- | `reduce(…)` | `Optional<E>` / `E` / `R` | O(n) | Flux vide → `Optional` vide |
259
- | `group(classifier)` / `groupBy(key, value)` | `Map<K, E[]>` | O(n) en moyenne | — |
260
- | `partition(count)` | `Array<Array<E>>` | O(n) | Round-robin par index |
261
- | `partitionBy(classifier)` | `Array<Array<E>>` | O(n) | Le numéro de seau provient du classifieur |
262
- | `anyMatch` / `allMatch` / `nonMatch` | `boolean` | O(n) au pire, avec court-circuit | — |
263
- | `findAny` / `findFirst` / `findLast` | `Optional<E>` | O(1)–O(n) | Positionnel dans l'ordre matérialisé |
264
- | `findAt(k)` / `findNegativeAt(k)` | `Optional<E>` | O(k) / O(n−k) | Compte depuis le début / la fin |
265
- | `findMaximum(cmp?)` / `findMinimum(cmp?)` | `Optional<E>` | O(n) | Comparaison structurelle par défaut |
266
- | `frequency()` | `Map<E, bigint>` | O(n) en moyenne | Collectables de statistiques uniquement |
267
- | `write(stream, accumulator?)` | `WritableStream<S>` | O(n) | Terminal de type E/S |
268
- | `collect(collector)` | `R` | O(n) | Le pliage générique derrière chaque terminal ci-dessus |
269
-
270
- ### 6.3 Collecteurs de structure
271
-
272
- `toArray`, `toSet` et `toMap` terminent un pipeline dans un conteneur JavaScript. Douze autres terminaux le terminent dans l'une des structures de la bibliothèque elle-même, si bien que le résultat porte les invariants que cette structure garantit plutôt que ceux qu'un tableau possède par accident. Ils sont disponibles sur chaque collectable, y compris le collectable nu rendu par `toCollectable()`.
273
-
274
- | Terminal | Renvoie | Ordre | Coût |
275
- | :--- | :--- | :--- | :--- |
276
- | `toArray()` | `Array<E>` | ordre de parcours | O(n) |
277
- | `toSet()` | `Set<E>` | ordre d'insertion, SameValueZero | O(n) en moyenne |
278
- | `toMap(key, value?)` | `Map<K, V>` | ordre d'insertion ; la dernière écriture l'emporte | O(n) en moyenne |
279
- | `toOrderedSet(comparator?)` | `SynchronousOrderedSet<E>` | croissant selon le comparateur | O(n²) au pire |
280
- | `toUnorderedSet()` | `SynchronousUnorderedSet<E>` | non spécifié (seaux de hachage) | O(n) attendu |
281
- | `toOrderedMap(key, value?)` | `SynchronousOrderedMap<K, V>` | ordre d'insertion ; la dernière écriture l'emporte | O(n) attendu |
282
- | `toUnorderedMap(key, value?)` | `SynchronousUnorderedMap<K, V>` | non spécifié | O(n) attendu |
283
- | `toList()` | `SynchronousList<E>` | ordre de parcours, doublons conservés | O(n) |
284
- | `toRing(span?)` | `SynchronousRing<E>` | l'ordre de parcours fixe les positions sur l'anneau | O(n) |
285
- | `toLRUSet(capacity)` | `SynchronousUnorderedLRUSet<E>` | par récence ; seuls les `capacity` derniers survivent | O(n²) au pire |
286
- | `toLRUMap(capacity, key, value?)` | `SynchronousUnorderedLRUMap<K, V>` | par récence | O(n) attendu |
287
- | `toSkipSet(comparator?)` | `SynchronousSkipSet<E>` | croissant selon le comparateur, navigable | O(n²) au pire |
288
- | `toSkipMap(key, value?)` | `SynchronousSkipMap<K, V>` | selon le comparateur de clés, navigable | O(n) attendu |
289
- | `toBinarySearchTree(comparator?)` | `SynchronousBinarySearchTree<E>` | croissant selon le comparateur, équilibré en hauteur | O(n log n) |
290
- | `toRootedForest()` | `SynchronousRootedForest<E>` | l'ordre de parcours devient la structure | O(n) |
291
-
292
- Le type du résultat suit le type d'élément du collectable lui-même, donc aucun terminal ne prend d'argument de type : un `SynchronousCollectable<E>` vous remet directement `Array<E>`, `SynchronousList<E>` ou `SynchronousOrderedSet<E>`. Seuls restent les arguments métier — le comparateur de `toOrderedSet` / `toSkipSet` / `toBinarySearchTree`, la capacité de la paire LRU, l'étendue de `toRing`, et les extracteurs de clé et de valeur de la famille des tables.
293
-
294
- Un anneau est un ensemble, donc les éléments répétés gardent leur première position ; toutes les autres structures ci-dessus conservent les doublons sauf si leur propre contrat en décide autrement (`toOrderedSet`, `toUnorderedSet` et `toSkipSet` sont des ensembles ; `toBinarySearchTree` et `toList` n'en sont pas).
295
-
296
- ```typescript
297
- const numbers = useOf(3, 1, 4, 1, 5).toCollectable();
298
-
299
- numbers.toOrderedSet().toArray(); // [1,3,4,5]
300
- numbers.toUnorderedSet().size(); // 4n — the duplicate 1 collapses
301
- numbers.toList().toArray(); // [3,1,4,1,5]
302
- numbers.toSkipSet().first().get(); // 1 — navigable by comparator
303
- numbers.toBinarySearchTree().size(); // 5n — duplicates retained
304
- numbers.toRootedForest().getRoots().length; // 1
305
- useOf(1, 2, 3).toCollectable().toRing().phaseAt(3n).get().toDegreeValue(); // 0
306
- useOf(3, 1, 3).toCollectable().toOrderedMap(n => n, n => `v${n}`).get(3).get(); // "v3"
307
- ```
308
-
309
- Les collectables asynchrones exposent les mêmes terminaux et résolvent chacun dans une promesse ; la forme de l'appel ne gagne donc qu'un `await`.
310
-
311
- ---
312
-
313
- <a id="7-statistics-and-windows"></a>
314
- ## 7. Statistiques et fenêtres
315
-
316
- ### 7.1 La surface statistique
317
-
318
- ```typescript
319
- useOf(3, 1, 4, 1, 5).toNumericStatistics().summate(); // 14
320
- useOf(3, 1, 4, 1, 5).toNumericStatistics().average(); // 2.8
321
- useOf(3, 1, 4, 1, 5).toNumericStatistics().median(); // 3
322
- useOf(3, 1, 4, 1, 5).toNumericStatistics().mode(); // 1
323
- useOf(3, 1, 4, 1, 5).toNumericStatistics().variance().get(); // 3.2 (n − 1)
324
- useOf(3, 1, 4, 1, 5).toNumericStatistics().standardDeviation().get(); // 1.7889 (n − 1)
325
- ```
326
-
327
- Les deux familles statistiques exposent la même surface — `summate`, `average`, `median`, `mode`, `variance`, `standardDeviation`, `frequency`, `findMaximum`, `findMinimum` — avec de l'arithmétique `bigint` pour `toBigintStatistics()`. Les mesures de dispersion et la famille des quantiles renvoient un `Optional`, car un échantillon peut être trop petit pour qu'elles soient définies ; déballez-les avec `.get()`, `.orElse(…)` ou `.isPresent()`.
328
-
329
- ### 7.2 Estimateurs et valeurs par défaut
330
-
331
- Les valeurs par défaut sont les définitions qu'utilisent R, NumPy, pandas et Excel : une valeur calculée ici se compare donc à ces outils sans facteur de correction.
332
-
333
- | Statistique | Défaut | Auparavant | Retrouver l'ancienne valeur avec |
334
- | :--- | :--- | :--- | :--- |
335
- | `quantile(probability)` | Hyndman–Fan type 7 | type 1 | `{ method: "type-1" }` |
336
- | `interquartileRange()` | Hyndman–Fan type 7 | type 1 | `{ method: "type-1" }` |
337
- | `skewness()` | `G1` corrigé du biais | `g1` | `{ estimator: "g1" }` |
338
- | `kurtosis()` | `G2` d'excès corrigé du biais | `g2` | `{ estimator: "g2" }` |
339
- | `variance()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
340
- | `standardDeviation()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
341
-
342
- Les neuf types de Hyndman–Fan sont disponibles sous `"type-1"` … `"type-9"`, et `{ excess: false }` rajoute trois au kurtosis pour rapporter le quatrième moment standardisé non excessif. Les types d'options — `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator`, `VarianceOptions` — sont exportés par `mathematics/quantile.ts`.
343
-
344
- ```typescript
345
- const sample = useOf(1, 2, 3, 4).toNumericStatistics();
346
-
347
- sample.variance().get(); // 1.6667 (5/3, ddof = 1)
348
- sample.variance({ ddof: 0 }).get(); // 1.25
349
- sample.standardDeviation().get(); // 1.2910
350
- sample.skewness().get(); // 0
351
- sample.kurtosis().get(); // -1.2 (G2)
352
- sample.kurtosis({ estimator: "g2" }).get(); // -1.36
353
- sample.kurtosis({ excess: false }).get(); // 1.8
354
-
355
- const quartiles = useOf(1, 2, 3, 4, 5, 6, 7, 8).toNumericStatistics();
356
- quartiles.quantile(0.75).get(); // 6.25 (type 7)
357
- quartiles.quantile(0.75, { method: "type-1" }).get(); // 6 (nearest rank)
358
- quartiles.interquartileRange().get(); // 3.5
359
- ```
360
-
361
- `G1` et `G2` sont indéfinis en dessous de trois et quatre observations, et la variance et l'écart-type d'échantillon le sont en dessous de deux observations avec `ddof = 1`. Cela est rapporté par un `Optional` vide plutôt que par un zéro trompeur :
362
-
363
- ```typescript
364
- useOf(1, 2, 3).toNumericStatistics().kurtosis().isEmpty(); // true (G2 needs four observations)
365
- useOf(1, 2).toNumericStatistics().skewness().isEmpty(); // true (G1 needs three)
366
- useOf(1).toNumericStatistics().variance().isEmpty(); // true (ddof = 1 needs two)
367
- useOf(1).toNumericStatistics().variance({ ddof: 0 }).get(); // 0
368
- ```
369
-
370
- Les vues bigint acceptent les mêmes options et arrondissent chaque résultat en un `bigint` entier.
371
-
372
- ### 7.3 Fenêtres
373
-
374
- Les fenêtres réexposent les éléments tamponnés sous forme de pipeline de pipelines, si bien qu'une fenêtre peut être consommée sans la matérialiser d'abord en tableau. Les deux formes incluent une dernière fenêtre partielle.
375
-
376
- ```typescript
377
- useFrom([1, 2, 3, 4]).toWindow().slide(2n, 1n).toUnordered().toArray()
378
- .map(window => window.toUnordered().toArray()); // [[1,2],[2,3],[3,4],[4]]
379
-
380
- useFrom([1, 2, 3, 4]).toWindow().tumble(2n).toUnordered().toArray()
381
- .map(window => window.toUnordered().toArray()); // [[1,2],[3,4]]
382
- ```
383
-
384
- ---
385
-
386
- <a id="8-the-index-model"></a>
387
- ## 8. Le modèle d'index
388
-
389
- Un index est un `bigint` attaché à chaque élément. Les opérateurs le **transmettent**, le **re-indexent**
390
- à partir de zéro, ou vous laissent le **calculer**.
391
-
392
- La collecte ordonnée normalise chaque index modulo le nombre d'éléments, puis trie sur le résultat. C'est
393
- pourquoi `translate(7)` sur cinq éléments équivaut à `translate(2)` : `7 ≡ 2 (mod 5)`.
394
-
395
- La collecte non ordonnée conserve l'index que le pipeline a produit et renvoie les éléments dans l'ordre
396
- d'arrivée. Les index négatifs sont légaux et se trient simplement avant les non négatifs une fois normalisés.
397
-
398
- ---
399
-
400
- <a id="9-the-asynchronous-model"></a>
401
- ## 9. Le modèle asynchrone
402
-
403
- `AsynchronousSemantic` reflète la surface synchrone, en renvoyant des promesses. Les écritures sont mises en file
404
- d'attente, si bien qu'une lecture émise après une écriture l'observe toujours.
405
-
406
- ```typescript
407
- const ring = new AsynchronousRing([1, 2]);
408
- const added = ring.add(3); // queued
409
- const sampled = ring.elementAt(2n); // observes the write above
410
- await added;
411
- (await sampled).get().element; // 3
412
- ```
413
-
414
- Les fabriques asynchrones par nature — `usePromise`, `useBlob`, `useInterval`, `useSubscription` — renvoient
415
- directement un `AsynchronousSemantic`, et `useFrom` / `useIterate` se mettent à niveau lorsqu'on leur confie un
416
- `AsyncIterable`.
417
-
418
- ---
419
-
420
- <a id="10-periodic-analysis-rings-fields-and-scores"></a>
421
- ## 10. Analyse périodique : anneaux, champs et scores
422
-
423
- La moitié mathématique de la bibliothèque traite un cycle comme un objet de première classe plutôt que comme une
424
- table de correspondance. Trois constructions travaillent ensemble.
425
-
426
- ### 10.1 Un anneau est un entier
427
-
428
- Un `Ring` est un **anneau n-fois** : le nombre d'éléments, la période positionnelle et le nombre de phases
429
- distinctes sont le même entier `n`. La phase d'une position est calculée à partir de son résidu exact, si bien que
430
- l'anneau est périodique au bit près :
431
-
432
- ```typescript
433
- import { SynchronousRing } from "semantic-typescript";
434
-
435
- const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
436
- ring.phaseAt(0n).get().toDegreeValue(); // 0
437
- ring.phaseAt(1n).get().toDegreeValue(); // 60
438
- ring.phaseAt(6n).get().getValue() === ring.phaseAt(0n).get().getValue(); // true, exactly
439
- ring.purity(); // 1
440
- ring.isBalanced(); // true
441
- ring.sequences().map(z => z.magnitude()); // [0, 6, 0, 0, 0, 0]
442
- ```
443
-
444
- `frequency` est la vitesse de rotation **ambiante**, non un pas de phase entre positions : l'environnement
445
- déplace chaque phaseur du même angle, si bien qu'il ne peut jamais briser le motif intrinsèque. `span` fixe le
446
- nombre de tours qu'occupe l'anneau.
447
-
448
- `purity()` est la part d'énergie portée par le mode fondamental,
449
- `max(|c₁|², |c_{n−1}|²) / (n · Σ|z|²)`. Elle vaut `1` exactement lorsque l'anneau est constitué des racines
450
- `n`-ièmes de l'unité à une rotation près, et elle coûte `O(n)` car seuls deux bins sont nécessaires. Les deux
451
- sens sont testés, car un anneau peut parcourir le cercle dans un sens ou dans l'autre.
452
-
453
- ### 10.2 Un champ est un score borné
454
-
455
- Un `PhaseField` associe une phase relative à un score dans `[-1, 1]` :
456
-
457
- ```typescript
458
- import { useField, useRadian } from "semantic-typescript";
459
-
460
- const cosine = useField([1]); // cos(2πΔ): +1 aligned, −1 opposed
461
- const mixed = useField([0.4, 0.3, 0.3]); // first three harmonics
462
-
463
- cosine.sample(useRadian(0)); // 1
464
- cosine.sample(useRadian(Math.PI)); // -1
465
- ```
466
-
467
- Le score est une série de cosinus tronquée, `a₀ + Σ wₘ·cos(2πmΔ)`, et le constructeur impose
468
- `|a₀| + Σ|wₘ| ≤ 1`. Par l'inégalité triangulaire, le score ne peut jamais sortir de `[-1, 1]` — la borne est une
469
- **conséquence de la construction**, et non un écrêtage appliqué après coup.
470
-
471
- La raison pour laquelle le champ est une série et non une table : une table à `n` points reproduit exactement ses
472
- propres échantillons, mais son interpolant trigonométrique dépasse entre ceux-ci. Une table à cinq points notée
473
- `[+1, +0.5, −1, −0.5, 0]` interpole sur `[−1.0428, 1.0041]`, ce qui brise déjà la promesse.
474
- Une série bornée ne le peut pas.
475
-
476
- ### 10.3 L'analyse mesure les données à chaque élément
477
-
478
- Chaque élément de l'anneau est une phase de référence. `analyse` replace chaque observation sur l'anneau et la
479
- mesure par rapport à chaque référence :
480
-
481
- ```typescript
482
- const ring = new SynchronousRing(["a", "b", "c", "d", "e"]);
483
- const report = ring.analyse(["a", "c", "c", "a", "zz", "e"]);
484
-
485
- report.matched; // 5
486
- report.missed; // 1 ('zz' is not on the ring)
487
- report.references.map(r => r.count); // [2, 0, 2, 0, 1]
488
- ```
489
-
490
- Chaque ligne de référence rapporte `count`, la phase relative moyenne `mean` et la longueur résultante moyenne
491
- `resultant`. Une identité mérite d'être retenue : **`resultant` est identique pour chaque référence**,
492
- car changer la ligne de base ne fait que faire tourner toutes les phases relatives d'une constante. Ce qui diffère
493
- d'une référence à l'autre, c'est le compte et la moyenne.
494
-
495
- ```typescript
496
- const ring = new SynchronousRing(["a", "b", "c"]);
497
- ring.scoresAt(0, cosine); // one score per position, in [-1, 1]
498
- ```
499
-
500
- ### 10.4 Instruments associés
501
-
502
- | Appel | Répond | Coût |
503
- | :--- | :--- | :--- |
504
- | `purity()` | l'anneau est-il toujours les racines `n`-ièmes de l'unité ? | O(n) |
505
- | `resultant(t?)` | à quel point les phaseurs sont-ils concentrés maintenant ? | O(n) |
506
- | `analyse(data)` | comment ces données se situent-elles par rapport à chaque élément ? | O(n · m) |
507
- | `toSpectrum()` | l'anneau comme spectre, nombre d'onde sur l'axe des fréquences | O(n²) |
508
- | `useAngularSpectrum(ring, instants, plan)` | le plan `(ω, n)` au fil du temps | O(n² · frames) |
509
-
510
- Deux primitives supplémentaires complètent le modèle : `combineRings(a, b)` joint deux anneaux sur leur plus petit
511
- commun multiple (un pullback, pas un produit — les paires qui ne peuvent pas partager un instant sont simplement
512
- absentes), et `rate()` donne à chaque élément sa propre fréquence intrinsèque, afin qu'un anneau puisse dériver par
513
- rapport à l'environnement. `jetAt`, `windingAt` et `errorIntegralAt` exposent l'état différentiel de cette dérive.
514
-
515
- ### 10.5 Fenêtres spectrales
516
-
517
- Les constructions d'anneaux sont une moitié de la surface analytique ; l'autre travaille sur des signaux échantillonnés. Sa primitive la plus réutilisée est la fenêtre, et chaque forme ici possède deux variantes. La variante symétrique divise la trame par `count − 1` ; la variante périodique (DFT paire) la divise par `count`, un échantillon de moins qu'une période complète, de sorte que le taper est continu sous l'extension périodique que suppose la DFT.
518
-
519
- ```typescript
520
- useHannWindow(1, 4); // 0.75 symmetric
521
- useHannPeriodicWindow(1, 4); // 0.5 periodic
522
- coherentGain(useHannPeriodicWindow, 16); // 0.5
523
- equivalentNoiseBandwidth(useHannPeriodicWindow, 16); // 1.5
524
- ```
525
-
526
- | Forme | Symétrique | Périodique | Notes |
527
- | :--- | :--- | :--- | :--- |
528
- | Rectangulaire | `useRectangularWindow` | — | sans taper ; pas de distinction périodique |
529
- | Hann | `useHannWindow` | `useHannPeriodicWindow` | le défaut de l'analyse à court terme |
530
- | Hamming | `useHammingWindow` | `useHammingPeriodicWindow` | — |
531
- | Blackman | `useBlackmanWindow` | `useBlackmanPeriodicWindow` | — |
532
- | Nuttall | `useNuttallWindow` | `useNuttallPeriodicWindow` | Blackman–Harris à quatre termes |
533
- | Flat-top | `useFlatTopWindow` | `useFlatTopPeriodicWindow` | la précision d'amplitude avant la résolution |
534
- | Kaiser | `useKaiserWindow(beta)` | `useKaiserPeriodicWindow(beta)` | `beta` fixe la largeur du lobe principal |
535
-
536
- Utilisez la variante périodique pour l'analyse et l'overlap-add : à un saut d'une demi-trame, la fenêtre de Hann périodique somme à `1.000000` quelle que soit la longueur, tandis que la variante symétrique ondule de `0.841 dB` à `N = 16` et encore de `0.013 dB` à `N = 1024`. Gardez la variante symétrique pour la conception de filtres, où la trame n'est pas une période du taper.
537
-
538
- Les fenêtres flat-top renvoient des **poids négatifs** près des extrémités de la trame, parce qu'une fenêtre flat-top est exactement cela. Ils sont renvoyés tels quels plutôt qu'écrêtés, car écrêter perturbe le lobe principal et détruit la faible perte de scalloping qui justifie l'existence de la fenêtre :
539
-
540
- ```typescript
541
- useFlatTopWindow(0, 1024); // -0.000421 — negative by definition
542
- Array.from({ length: 1024 }, (_unused, index) => useFlatTopWindow(index, 1024))
543
- .filter(weight => weight < 0).length; // 578
544
- ```
545
-
546
- ### 10.6 Niveaux, platitude et énergie de bande
547
-
548
- `decibel` a disparu en tant que fonction unique, parce qu'une amplitude et une puissance ne partagent pas le même facteur. `amplitudeDecibel` applique `20·log10` à un rapport de type tension ; `powerDecibel` applique `10·log10` à une puissance, une énergie, un carré moyen ou une densité spectrale de puissance :
549
-
550
- ```typescript
551
- amplitudeDecibel(0.25); // -12.0412
552
- powerDecibel(0.25); // -6.0206
553
- ```
554
-
555
- `Bin.decibel` et `Spectrum.decibelOf` sont des niveaux d'amplitude ; `Spectrum.powerDecibelOf` est le niveau de puissance correspondant. Donner une puissance à la forme amplitude rapporte `20·log10(power)` et double le niveau en décibels.
556
-
557
- Deux mesures de spectre ont changé de lecture :
558
-
559
- ```typescript
560
- const twoBins = new Spectrum(new Sample(2, 1), [new Complex(2, 0), new Complex(1, 0)]);
561
- twoBins.flatness(); // 0.8 — geometric over arithmetic mean of power
562
-
563
- const shape = new Spectrum(new Sample(4, 1), [
564
- new Complex(1, 0), new Complex(2, 0), new Complex(0, 0), new Complex(0, 0),
565
- ]);
566
- shape.bandEnergy(new Frequency(0), new Frequency(2)); // 9
567
- shape.bandEnergy(new Frequency(0), new Frequency(2), "two-sided"); // 5
568
- ```
569
-
570
- `flatness()` compare les moyennes géométrique et arithmétique de `power()` sur la moitié unilatérale du spectre, et non la moyenne des magnitudes sur les deux moitiés, et le doublement d'amplitude purement affichage de `oneSided()` n'est délibérément pas appliqué. `bandEnergy()` utilise par défaut la lecture unilatérale, en repliant la moitié conjuguée : DC et Nyquist comptent une fois et chaque bin intérieur compte deux fois ; une bande couvrant tout l'axe satisfait alors Parseval, `Σ|X|² = N·Σx²`. Passez `"two-sided"` pour un spectre complexe, dont les bins sont déjà indépendants.
571
-
572
- ### 10.7 Analyse à court terme
573
-
574
- `Sampler.shortTimeFourierTransform` et `Sampler.spectrogram` suivent les conventions `stft` de SciPy : fenêtre de Hann périodique, bordure remplie de zéros aux deux extrémités et queue remplie de zéros.
575
-
576
- ```typescript
577
- const sampler = new Sampler(new Sample(64, 1));
578
- const spectrogram = sampler.spectrogram(time => Math.cos(2 * Math.PI * 4 * time), 16, 8);
579
-
580
- spectrogram.size(); // 9 frames
581
- spectrogram.getBoundary(); // "zeros"
582
- spectrogram.getTime(6); // 0.75 — frame centres, not frame starts
583
- sampler.spectrogram(time => Math.cos(2 * Math.PI * 4 * time), 16, 8, useHannWindow,
584
- { boundary: "none", padded: false }).size(); // 7 — complete frames only
585
- ```
586
-
587
- L'extension centre la première trame sur le premier échantillon, et le remplissage de queue rend les derniers échantillons atteignables : 64 échantillons avec une trame de 16 et un saut de 8 donnent 9 trames au lieu de 7, et 1000 échantillons avec une trame de 256 et un saut de 128 donnent 9 trames au lieu de 6. Les temps de trame sont des centres — `(k·hop + (frameSize mod 2)/2)/rate` pour une vue étendue, `(k·hop + frameSize/2)/rate` pour une vue non étendue — et `toFrame()` rogne l'extension avant d'inverser.
588
-
589
- ---
590
-
591
- <a id="11-utility-helpers"></a>
592
- ## 11. Fonctions utilitaires
593
-
594
- ### 11.1 La hiérarchie d'erreurs
595
-
596
- `structure/error.ts` exporte une hiérarchie de quinze classes pour les erreurs levées *autour* d'un pipeline — par votre propre code, un collecteur personnalisé ou un adaptateur :
597
-
598
- ```text
599
- SemanticError
600
- ├── IllegalOperationError
601
- ├── ValidationError
602
- │ ├── IndexError
603
- │ ├── RangeError (also exported as SemanticRangeError)
604
- │ └── ConfigurationError
605
- ├── CapacityError
606
- ├── StateError
607
- │ └── IllegalStateError
608
- ├── ConcurrencyError
609
- ├── CollectionError
610
- ├── SerializationError
611
- ├── NumericError
612
- │ └── ArithmeticError
613
- └── PhaseError
614
- ```
615
-
616
- Chaque classe fixe `name`, transmet la `cause` native, porte un sac `context` optionnel et se sérialise via `toJSON()` ; chacune possède une garde de marque du même nom.
617
-
618
- ```typescript
619
- const error = new CapacityError("LRU capacity exhausted", { context: { capacity: 0 } });
620
-
621
- error.name; // "CapacityError"
622
- isCapacityError(error); // true
623
- isValidationError(error); // false — a capacity failure is not a validation failure
624
- isSemanticError(error); // true
625
- error.toJSON(); // { name: "CapacityError", message: "…", context: { capacity: 0 } }
626
- ```
627
-
628
- `SemanticRangeError` est la même classe que le `RangeError` exporté, donc `instanceof` tient pour les deux noms ; importer `RangeError` masque le constructeur global pour tout le module, et c'est pourquoi l'alias existe. Aucune des deux classes n'est levée par les vérifications d'arguments de la bibliothèque, qui continuent de lever les `TypeError` et `RangeError` natifs — la hiérarchie est le vocabulaire de la couche au-dessus.
629
-
630
- ### 11.2 Table des utilitaires
631
-
632
- | Assistant | Objectif |
633
- | :--- | :--- |
634
- | `useCompare(a, b)` | Ordre total entre primitives mixtes |
635
- | `useToNumber` / `useToBigInt` | Conversion numérique sans perte et vérifiée en plage |
636
- | `useRandom` | Assistants aléatoires avec ou sans graine |
637
- | `useTraverse` | Parcours structurel d'objets et de tableaux imbriqués |
638
- | `useStringify` | Sérialisation JSON sûre face aux cycles |
639
- | `useHash` / `maskOf` / `register` / `unregister` | Hachage structurel et registre de stratégies de hachage |
640
- | `invalidate` / `validate` | Gardes qui lèvent une forme d'erreur cohérente |
641
- | `Optional<T>` | Le remplacement de `null` de la bibliothèque : `Optional.of(x)` / `Optional.empty()` |
642
-
643
- ### 11.3 `useTraverse`
644
-
645
- `useTraverse(target, callback)` aplatit n'importe quel objet en ses **feuilles non nulles**. Il descend dans les objets imbriqués, parcourt les valeurs itérables par position, ignore `null` et `undefined` et ne rentre jamais dans un objet déjà visité — un graphe d'objets comportant un cycle se termine donc au lieu de faire déborder la pile.
646
-
647
- ```typescript
648
- const source = { id: 7, tags: ["a", "b"], owner: { name: "Ada", note: null } };
649
- const leaves: string[] = [];
650
-
651
- useTraverse(source, (key, value, path) => {
652
- leaves.push(`${path.join(".")} = ${String(value)}`);
653
- return true;
654
- });
655
-
656
- leaves; // ["id = 7", "tags.0 = a", "tags.1 = b", "owner.name = Ada"]
657
- ```
658
-
659
- Le rappel s'exécute une fois par feuille et reçoit la clé propre de la feuille (son indice lorsqu'elle provient d'un itérable), la valeur, et le chemin qui y mène. Renvoyer `false` abandonne **tout** le parcours, pas seulement la branche courante :
660
-
661
- ```typescript
662
- let visited = 0;
663
- useTraverse({ a: 1, b: 2, c: 3 }, () => {
664
- visited += 1;
665
- return false;
666
- });
667
- visited; // 1 — the first leaf already stops the walk
668
- ```
669
-
670
- Un cycle est sûr pour la même raison :
671
-
672
- ```typescript
673
- const cyclic: any = { name: "root", child: { name: "leaf" } };
674
- cyclic.child.parent = cyclic;
675
-
676
- const names: string[] = [];
677
- useTraverse(cyclic, (_key, value) => {
678
- if (typeof value === "string") names.push(value);
679
- return true;
680
- });
681
- names; // ["root", "leaf"] — the back edge to `cyclic` is not revisited
682
- ```
683
-
684
- **Chaque rappel reçoit son propre instantané de chemin.** Le parcours ne garde en interne qu'un tableau de travail et le copie, au moment où une feuille est remise, dans un nouveau tableau gelé par `Object.freeze`. L'instantané ne change plus ensuite : il peut être conservé tel quel, deux rappels ne partagent jamais un tableau, et relancer le parcours ne perturbe pas les instantanés déjà remis. Son type est `ReadonlyArray`, y écrire est donc une erreur de compilation, et le tableau gelé lève en plus un `TypeError` à l'exécution. Cette sûreté coûte une allocation `O(depth)` par feuille rapportée ; une branche qui ne rapporte rien n'alloue rien de plus.
685
-
686
- ```typescript
687
- const paths: Array<ReadonlyArray<PropertyKey>> = [];
688
- useTraverse({ a: { b: 1 }, c: 2 }, (_key, _value, path) => {
689
- paths.push(path); // safe to keep as-is
690
- return true;
691
- });
692
- paths[0] === paths[1]; // false — one array per call
693
- paths.map(path => path.join(".")); // ["a.b", "c"]
694
- ```
695
-
696
- `useAttribute` est ce parcours transformé en fabrique : elle rapporte les mêmes feuilles sous forme de paires `{ key, value }`, donc les règles — et le coût — sont identiques.
697
-
698
- ```typescript
699
- useAttribute({ a: 1, b: { c: 2 } }).toUnordered().toArray();
700
- // [{ key: "a", value: 1 }, { key: "c", value: 2 }]
701
- ```
702
-
703
- ---
704
-
705
- <a id="12-performance-and-pitfalls"></a>
706
- ## 12. Performance et pièges
707
-
708
- **La paresse a un prix par étage.** Chaque opérateur ajoute un saut de générateur. Pour des boucles numériques
709
- serrées sur des tableaux en mémoire, une simple boucle `for` battra n'importe quel pipeline ; le pipeline gagne
710
- lorsque la source est paresseuse, infinie, ou que le pipeline est assez long pour que la discipline d'index paie.
711
-
712
- **`toOrdered()` trie ; `toUnordered()` ne trie pas.** Si une inversion semble « ne pas avoir marché », le
713
- collecteur était non ordonné — une réécriture d'index ne devient un ordre que lorsque les index sont normalisés.
714
-
715
- **`useGenerate` sans `interrupt` ne se termine jamais.** Fournissez toujours un prédicat d'arrêt ou un
716
- `limit` / `takeWhile` en aval.
717
-
718
- **`Ring` est un ensemble à ordre d'insertion.** Les éléments sont uniques et conservent l'ordre dans lequel ils ont
719
- été ajoutés pour la première fois. Un `add` en double renvoie `false`, et un `set` qui en introduirait un lève une
720
- exception. C'est ce qui permet à une phase d'être une fonction de l'*élément* plutôt que de la position
721
- qu'il occupe par hasard.
722
-
723
- **`Spectrum` suppose un signal réel dans ses assistants unilatéraux.** `oneSided()`, `getNyquist()` et
724
- `centroid()` supposent une symétrie conjuguée. Le spectre spatial d'un anneau de phase est complexe et en
725
- général non symétrique — un anneau qui parcourt le cercle à rebours place toute son énergie dans le bin
726
- `n − 1`, au-delà de Nyquist. Lisez les bins directement.
727
-
728
- **Les collecteurs de structure sont des terminaux, pas des vues.** Chacun vide le pipeline puis construit une structure complète : il coûte donc O(n) en temps et en espace et ne peut pas tourner sur une source infinie. Utilisez-en un à la fin d'un pipeline, jamais à l'intérieur d'une boucle sur un pipeline.
729
-
730
- **`variance()` et ses compagnons renvoient un `Optional`.** `variance().get()` sur un échantillon trop petit lève une exception ; utilisez `.orElse(0)` ou vérifiez `.isPresent()` quand la taille de l'échantillon ne dépend pas de vous. Il en va de même pour `standardDeviation`, `quantile`, `interquartileRange`, `skewness` et `kurtosis`.
731
-
732
- **Les fenêtres périodiques sont les fenêtres d'analyse.** `useHannWindow` et les autres formes symétriques sont destinées à la conception de filtres ; une analyse à court terme veut `useHannPeriodicWindow` et ses sœurs, sinon la somme d'overlap-add ondule à chaque jonction de trames.
733
-
734
- ---
735
-
736
- <a id="13-phase-rings"></a>
737
- ## 13. Anneaux de phase : un cycle exact
738
-
739
- Le chapitre 10 a introduit l'anneau comme modèle du cycle et l'a utilisé pour mesurer un jeu de
740
- données contre chaque phase. Ce chapitre en donne la surface complète : comment une position devient
741
- un angle, ce que la fréquence ambiante change et ne change pas, et les instruments qui lisent
742
- l'anneau contre le temps, contre les données et contre un autre anneau.
743
-
744
- Un `SynchronousRing` est une liste positionnelle adossée à un anneau circulaire doublement chaîné
745
- dont la tête et la queue sont jointes. C'est aussi un *ensemble* ordonné par insertion : aucun
746
- élément n'apparaît deux fois, et une écriture refusée laisse intacts à la fois les liens et les
747
- échantillons. Ce qui en fait un anneau plutôt qu'une liste, c'est que la phase d'une position est
748
- une fonction pure de son reste modulo la longueur courante ; l'anneau est donc périodique au bit
749
- près et son équilibre ne peut pas dériver.
750
-
751
- ### 13.1 Un anneau est un entier, calculé exactement
752
-
753
- Un anneau porte `n` éléments, et le nombre d'éléments, la période positionnelle et le nombre de
754
- phases distinctes sont le même entier. La position `i` porte
755
-
756
- `phase + span · 2π · (((i mod n) + n) mod n) / n`
757
-
758
- le reste étant réduit en `bigint` avant de devenir un `number`. C'est toute la raison pour laquelle
759
- `phaseAt(i)` et `phaseAt(i + n)` sont *identiques* et pas seulement proches : le second appel se
760
- réduit au même entier, évalue le même produit et produit le même double. Un indice bien au-delà de
761
- la plage entière exacte de `number` retombe sur la bonne position, et un indice négatif parcourt
762
- l'anneau à l'envers.
763
-
764
- | Appel | Retour | Signification |
765
- | :--- | :--- | :--- |
766
- | `new SynchronousRing(source, options?)` | anneau | `source` est un itérable quelconque, doublons écartés |
767
- | `getPhase()` / `getFrequency()` / `getSpan()` | `Angle`, `Frequency`, `number` | les trois paramètres de construction |
768
- | `size()` | `bigint` | le `n` qui est aussi la période |
769
- | `phaseAt(index)` | `Optional<Angle>` | phase intrinsèque seule ; ignore temps, taux et fréquence |
770
- | `elementAt(index)` | `Optional<RingElement>` | élément, phase intrinsèque, fréquence ambiante, phaseur |
771
- | `frequencyAt(index)` | `Optional<Frequency>` | le taux ambiant, vide pour un anneau vide |
772
- | `toComplex()` / `toComplex(index)` | `Complex[]` / `Optional<Complex>` | phaseurs intrinsèques |
773
-
774
- `RingOptions` ajoute deux fonctions par élément : `magnitude(element, index)` donne la longueur du
775
- phaseur et vaut un par défaut, `rate(element, index)` donne à l'élément sa propre fréquence
776
- intrinsèque et vaut zéro par défaut, soit un marqueur fixe.
777
-
778
- ```typescript
779
- import { SynchronousRing } from "semantic-typescript";
780
-
781
- const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
782
-
783
- ring.size(); // 6n
784
- ring.phaseAt(0n).get().toDegreeValue(); // 0
785
- ring.phaseAt(1n).get().toDegreeValue(); // 60
786
- ring.phaseAt(-1n).get().toDegreeValue(); // 300
787
- ring.phaseAt(6n).get().getValue() === ring.phaseAt(0n).get().getValue(); // true
788
- ring.phaseAt(10n ** 20n).get().toDegreeValue(); // 240
789
- ```
790
-
791
- `phaseAt(-1n)` vaut `300` sur un anneau de six éléments, et `10n ** 20n` se réduit à `4` modulo six,
792
- donc se lit `240`. Aucun des deux cas n'accumule de produit flottant, ce qui garde l'identité exacte
793
- quand la représentation de l'indice change.
794
-
795
- ### 13.2 La fréquence ambiante et l'état à un instant
796
-
797
- `frequency` est la **vitesse de rotation ambiante** de l'environnement dans lequel l'anneau est
798
- plongé, pas un pas de phase entre positions. L'environnement avance la phase de chaque élément du
799
- même angle : il ne peut donc pas changer le motif intrinsèque, ni briser l'équilibre, ni modifier
800
- un résultat de `phaseAt`. La valeur par défaut est `0 Hz`.
801
-
802
- `relativeAt(index, time)` soustrait cet angle commun et laisse la coordonnée contre laquelle un
803
- `PhaseField` est évalué. `viewAt` et `viewsAt` renvoient l'état complet à un instant, y compris le
804
- `rate` propre de l'élément, et les accesseurs différentiels exposent la dérive :
805
-
806
- | Appel | Formule | Lit |
807
- | :--- | :--- | :--- |
808
- | `relativeAt(index, time)` | `intrinsèque + tauxPropre · t − ambiant · t` | la phase que voit l'environnement |
809
- | `viewAt(index, time)` / `viewsAt(time)` | idem, plus élément, indice, temps, taux, phaseur | l'état complet |
810
- | `jetAt(index, time)` | phase à `t`, taux propre, `slip = tauxPropre − ambiant` | l'état différentiel |
811
- | `windingAt(index, time)` | `trunc(slip · t)` | tours entiers glissés, tronqués vers zéro |
812
- | `errorIntegralAt(index, time, origin?)` | `∫ phase relative dt`, en tours · secondes | depuis combien de temps un élément avance |
813
- | `resultant(time?)` | `abs(Σ w · e^{iθ}) / Σ w` | concentration de phase, insensible à la rotation ambiante |
814
- | `analyse(data, time?)` | comptes et moyennes par référence | les données mesurées contre chaque élément |
815
- | `scoreAt(index, time, field)` / `scoresAt(time, field)` | le champ échantillonné à la phase relative | un score borné par position |
816
-
817
- ```typescript
818
- import { SynchronousRing, useHertz } from "semantic-typescript";
819
-
820
- const moving = new SynchronousRing(["a", "b", "c", "d", "e", "f"], {
821
- frequency: useHertz(1),
822
- });
823
-
824
- moving.getFrequency().getHertz(); // 1
825
- moving.phaseAt(1n).get().toDegreeValue(); // 60 — intrinsic
826
- moving.relativeAt(1n, 0.25).get().toDegreeValue(); // 330 — 60 − 90
827
- moving.viewAt(1n, 0.25).get().relative.toDegreeValue(); // 330
828
- moving.jetAt(1n, 0.25).get().slip; // -1
829
- moving.purity(); // 1 — rotation cannot break it
830
- ```
831
-
832
- À `1 Hz`, l'environnement avance d'un quart de tour en `0.25 s` : la phase relative de la position
833
- un vaut donc `60 − 90 = 330` degrés, tandis que sa phase intrinsèque reste `60`. Le jet rapporte un
834
- `slip` de `-1` hertz parce que l'élément lui-même ne bouge pas, et `purity()` vaut toujours `1` :
835
- faire tourner tous les phaseurs du même angle n'est pas une déformation.
836
-
837
- ### 13.3 Pureté et équilibre
838
-
839
- `purity()` est la part de l'énergie de l'anneau portée par le mode fondamental :
840
-
841
- `max(abs(c₁)², abs(c_{n−1})²) / (n · Σ abs(z)²)`
842
-
843
- Les deux bins fondamentaux sont testés parce qu'un anneau peut parcourir le cercle dans les deux
844
- sens. Un anneau construit avec `span: 5` sur six éléments avance de `300` degrés, soit `−60`
845
- degrés : il tourne à l'envers, place toute son énergie dans `c₅` et serait déclaré impur par une
846
- formule qui ne regarderait que `c₁`. Le coût reste `O(n)` puisque seuls ces deux bins et l'énergie
847
- totale sont nécessaires.
848
-
849
- Les cas limites font partie du contrat. Un anneau de moins de deux éléments n'a pas de mode
850
- fondamental et rapporte `purity() === 0` et `isBalanced() === false`, quel que soit son contenu. Un
851
- anneau dont tous les phaseurs ont une longueur nulle n'a pas d'énergie et rapporte aussi zéro.
852
- `isBalanced(epsilon?)` vaut `1 − purity() <= epsilon`, avec `1E-9` comme tolérance par défaut.
853
-
854
- ```typescript
855
- import { SynchronousRing } from "semantic-typescript";
856
-
857
- const forward = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
858
- const backward = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 5 });
859
- const weighted = new SynchronousRing(["a", "b", "c", "d"], {
860
- magnitude: element => element === "a" ? 1 : 0,
861
- });
862
-
863
- forward.purity(); // 1
864
- backward.phaseAt(1n).get().toDegreeValue(); // 300 — it runs backwards
865
- backward.purity(); // 1 — the energy sits in c₅
866
- new SynchronousRing(["only"]).purity(); // 0
867
- new SynchronousRing(["only"]).isBalanced(); // false
868
- weighted.purity(); // 0.25
869
- ```
870
-
871
- ### 13.4 Longueur résultante et composantes symétriques
872
-
873
- `resultant(time?)` est la longueur résultante moyenne : le module de la somme des phaseurs divisé
874
- par la somme des modules. Contrairement à `purity`, qui demande si l'anneau est encore les racines
875
- `n`-ièmes de l'unité, `resultant` demande à quel point les phaseurs sont concentrés maintenant. Un
876
- anneau équilibré rapporte `0` ; un anneau dont tous les éléments partagent un angle rapporterait
877
- `1`, mais la sémantique d'ensemble interdit les doublons, si bien que `1` apparaît en pratique
878
- lorsque `magnitude` annule tous les éléments sauf un.
879
-
880
- `zeroSequence()` est le phaseur moyen, c'est-à-dire `c₀ / n`. `sequences()` renvoie les `n`
881
- composantes symétriques, ordre croissant, par une transformée directe. `toSpectrum()` les enveloppe
882
- dans un `Spectrum` dont le bin `j` rapporte `j` cycles par tour d'anneau : l'axe des fréquences est
883
- donc l'axe des nombres d'onde.
884
-
885
- ```typescript
886
- import { SynchronousRing } from "semantic-typescript";
887
-
888
- const round = (value: number): number => Number(value.toFixed(4));
889
- const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
890
-
891
- round(ring.resultant().get()); // 0
892
- round(ring.zeroSequence().get().magnitude()); // 0
893
- ring.sequences().map(component => round(component.magnitude())); // [0, 6, 0, 0, 0, 0]
894
- ring.toSpectrum().get().size(); // 6
895
- ring.toSpectrum().get().getResolution().getHertz(); // 1
896
- ```
897
-
898
- Les composantes sont complexes et en général **pas conjuguées-symétriques** : un anneau équilibré
899
- met toute son énergie dans le bin un, un anneau qui tourne à l'envers dans le bin `n − 1`. Les
900
- helpers unilatéraux de `Spectrum` supposent un signal réel ; lisez les bins directement au lieu de
901
- les replier.
902
-
903
- ### 13.5 Analyser des données contre chaque élément
904
-
905
- `analyse(data, time?)` rattache chaque observation à l'anneau par identité et la mesure contre
906
- chaque élément comme phase de référence. Les observations que l'anneau ne détient pas sont comptées
907
- comme `missed` plutôt que devinées, car l'anneau est la seule source de phases. Chaque ligne de
908
- référence rapporte `count`, la phase relative moyenne `mean` et la longueur résultante moyenne
909
- `resultant`.
910
-
911
- Une identité vaut la peine d'être retenue : **`resultant` est le même pour chaque référence**.
912
- Changer la base ne fait que faire tourner toutes les phases relatives d'une constante, et une
913
- rotation ne peut pas changer une longueur résultante. Ce qui diffère par référence, c'est le compte
914
- et la moyenne.
915
-
916
- ```typescript
917
- import { SynchronousRing } from "semantic-typescript";
918
-
919
- const round = (value: number): number => Number(value.toFixed(4));
920
- const ring = new SynchronousRing(["a", "b", "c", "d", "e"]);
921
- const report = ring.analyse(["a", "c", "c", "a", "zz", "e"]);
922
-
923
- report.matched; // 5
924
- report.missed; // 1
925
- report.references.map(row => row.count); // [2, 0, 2, 0, 1]
926
- report.references.map(row => round(row.resultant)); // [0.1453, 0.1453, 0.1453, 0.1453, 0.1453]
927
- ```
928
-
929
- L'évaluation est l'autre face de la même pièce. Un `PhaseField` est une série de cosinus bornée
930
- (chapitre 10.2), et `scoreAt`/`scoresAt` l'échantillonnent à la phase relative : le même anneau peut
931
- donc être évalué contre un alignement, contre un motif harmonique ou contre n'importe quel champ
932
- sans que l'anneau change.
933
-
934
- ```typescript
935
- import { SynchronousRing, useField } from "semantic-typescript";
936
-
937
- const round = (value: number): number => Number(value.toFixed(4));
938
- const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
939
- const cosine = useField([1]);
940
-
941
- round(ring.scoreAt(0n, 0, cosine).get()); // 1
942
- round(ring.scoreAt(1n, 0, cosine).get()); // 0.5
943
- ring.scoresAt(0, cosine).map(score => round(score)); // [1, 0.5, -0.5, -1, -0.5, 0.5]
944
- ```
945
-
946
- ### 13.6 Combiner des anneaux et le spectre angulaire
947
-
948
- `combineRings(left, right)` joint deux anneaux sur leur plus petit commun multiple, pas sur leur
949
- produit. L'instant `i` de l'anneau joint lit `i mod m` de l'anneau gauche et `i mod n` de l'anneau
950
- droit ; l'anneau joint se referme donc après `lcm(m, n)` instants. Les paires qui ne peuvent jamais
951
- coexister — celles dont les positions diffèrent modulo `gcd(m, n)` — sont simplement absentes :
952
- c'est pourquoi le résultat est un pullback et non un produit, et `aligns(left, right)` indique si
953
- une paire donnée est représentable.
954
-
955
- `useAngularSpectrum(ring, instants, plan)` construit le plan `(ω, n)` : chaque instant apporte une
956
- trame de phaseurs, la trame `k` du mode `j` est la `j`-ième composante symétrique de cette trame, et
957
- `cells[frame][order]` est le coefficient. Prendre une colonne donne la série temporelle d'un seul
958
- mode spatial, et `spectrum(order)` en est le spectre temporel.
959
-
960
- ```typescript
961
- import { Sample, SynchronousRing, combineRings, useAngularSpectrum } from "semantic-typescript";
962
-
963
- const left = new SynchronousRing(["a", "b", "c", "d"]);
964
- const right = new SynchronousRing(["x", "y", "z", "w", "v", "u"]);
965
- const joined = combineRings(left, right);
966
-
967
- joined.period; // 12n
968
- joined.divisor; // 2n
969
- joined.ring.size(); // 12n
970
- joined.aligns(0n, 0n); // true
971
- joined.aligns(1n, 0n); // false
972
- joined.ring.get(0n).join("-"); // a-x
973
-
974
- const angular = useAngularSpectrum(left, [0, 0.25, 0.5, 0.75], new Sample(4, 1));
975
- angular.order; // 4
976
- angular.frameCount; // 4
977
- angular.cells[0][1].magnitude(); // 4
978
- angular.spectrum(1).get().size(); // 4
979
- ```
980
-
981
- ### 13.7 Pièges
982
-
983
- **L'exactitude est exacte.** `phaseAt(i)` et `phaseAt(i + n)` sont le même double, pas deux valeurs
984
- à une tolérance près. Un anneau construit en accumulant `2π / n` par élément échouerait à cela ;
985
- celui-ci réduit d'abord l'indice en `bigint`.
986
-
987
- **`frequency` n'est pas un pas de phase.** C'est la vitesse de rotation ambiante, nulle par défaut,
988
- et faire tourner tous les phaseurs du même angle ne peut pas briser le motif intrinsèque. Pour faire
989
- avancer les positions les unes par rapport aux autres, utilisez `rate`, pas `frequency`.
990
-
991
- **`purity()` doit regarder les deux bins fondamentaux.** Un anneau qui tourne à l'envers reste les
992
- racines `n`-ièmes de l'unité ; `c₁` seul le déclarerait impur.
993
-
994
- **`n < 2` est un cas limite défini.** `purity()` renvoie `0` et `isBalanced()` `false` ; un anneau
995
- vide renvoie des optionnels vides depuis les méthodes d'échantillonnage.
996
-
997
- **Un span qui partage un facteur avec `n` effondre l'orbite.** Six éléments avec `span: 2` ne
998
- visitent que trois phases distinctes, et l'anneau n'est plus équilibré.
999
-
1000
- **`resultant` est invariant par rotation, pas par le temps.** La rotation ambiante le laisse
1001
- inchangé ; les `rate` par élément le changent à mesure que l'anneau se déphase.
1002
-
1003
- **L'anneau est un ensemble, et `analyse` apparie par identité.** Les doublons sont écartés à la
1004
- construction, et un objet structurellement égal à un élément de l'anneau mais qui n'est pas la même
1005
- référence est compté comme manqué.
1006
-
1007
- **`toSpectrum()` est complexe.** Ses bins ne sont pas conjugués-symétriques ; les helpers
1008
- unilatéraux de `Spectrum` ne s'appliquent pas. Lisez les bins.
1009
-
1010
- **`windingAt` tronque vers zéro, et `errorIntegralAt` intègre la phase.** Un demi-tour de glissement
1011
- rapporte `0n` ; l'intégrale d'erreur est en tours · secondes, pas un taux.
1012
-
1013
- ```typescript
1014
- import { SynchronousRing, useHertz } from "semantic-typescript";
1015
-
1016
- const collapsed = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 2 });
1017
- collapsed.phaseAt(1n).get().toDegreeValue(); // 120
1018
- collapsed.phaseAt(3n).get().toDegreeValue(); // 0
1019
- Number(collapsed.purity().toFixed(4)); // 0
1020
-
1021
- const falling = new SynchronousRing(["a", "b", "c", "d"], {
1022
- frequency: useHertz(2),
1023
- rate: () => useHertz(0.5),
1024
- });
1025
- falling.jetAt(0n, 2).get().slip; // -1.5
1026
- falling.windingAt(0n, 1).get(); // -1n
1027
- falling.windingAt(0n, 0.5).get(); // 0n
1028
-
1029
- const objects = new SynchronousRing([{ key: 1 }, { key: 2 }]);
1030
- objects.analyse([{ key: 1 }]).missed; // 1 — identity, not structure
1031
- ```
1032
-
1033
- ---
1034
-
1035
- <a id="14-signal-analysis"></a>
1036
- ## 14. Analyse du signal : fenêtres, spectres et temps
1037
-
1038
- Le chapitre 10 traitait un anneau comme un cycle de phases. Ce chapitre en est le pendant temporel :
1039
- un enregistrement réel qui porte, outre ses échantillons, les deux contextes dont toute formule
1040
- spectrale a besoin — le plan d'échantillonnage qui transforme un indice de bin en hertz, et la
1041
- fenêtre qui fixe à la fois le gain cohérent et la bande de bruit équivalente.
1042
-
1043
- L'objet-valeur est `SynchronousSignalCollectable`, son jumeau mis en file est
1044
- `AsynchronousSignalCollectable`. Tous deux exposent la même surface `SignalCollectable` :
1045
- vingt-trois méthodes, chacune déléguant à exactement une fabrique `useSynchronous<Method>` dans
1046
- `synchronous/collectors`. La séparation est délibérée. La classe est une façade documentée, la
1047
- fabrique possède l'accumulation, et le constructeur calcule une fois, à cette longueur,
1048
- `sum(w) / count` et `count * sum(w^2) / sum(w)^2`. Un appelant qui doit se souvenir de « diviser
1049
- par le gain cohérent » finira par l'oublier, et les 6,02 décibels qui en résultent ressemblent à
1050
- une mesure plausible.
1051
-
1052
- ### 14.1 Un signal porte sa propre calibration
1053
-
1054
- Un signal se construit à partir d'un itérable et d'un taux, d'un `Frame` existant, ou du
1055
- constructeur avec un `Sample` explicite. La fenêtre par défaut est la fenêtre de Hann
1056
- périodique ; toute analyse qui n'en nomme pas une l'utilise, et toute analyse accepte un
1057
- remplacement. Les accesseurs de métadonnées lisent en temps constant un état figé à la
1058
- construction, de sorte qu'un signal peut circuler sans que le contexte dérive.
1059
-
1060
- | Appel | Retour | Signification |
1061
- | :--- | :--- | :--- |
1062
- | `SynchronousSignalCollectable.useRate(values, rate, window?)` | signal | durée `count / rate` |
1063
- | `SynchronousSignalCollectable.useFrame(frame, window?)` | signal | conserve le plan du frame |
1064
- | `new SynchronousSignalCollectable(plan, values, window?)` | signal | le nombre de trames doit égaler le nombre d'échantillons |
1065
- | `getSample()` / `getValues()` / `getWindow()` | plan, échantillons, fenêtre | le contexte figé |
1066
- | `getRate()` | `number` | échantillons par seconde |
1067
- | `getNyquist()` / `getResolution()` | `Frequency` | `rate / 2` et `rate / count` |
1068
- | `getCoherentGain()` / `getNoiseBandwidth()` | `number` | recalculés à cette longueur exacte |
1069
- | `size()` / `at(index)` / `toFrame()` / `equals(other)` / `toString()` | — | sémantique de valeur |
1070
-
1071
- ```typescript
1072
- import { SynchronousSignalCollectable } from "semantic-typescript";
1073
-
1074
- const signal = SynchronousSignalCollectable.useRate(
1075
- Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 64)),
1076
- 64,
1077
- );
1078
-
1079
- signal.size(); // 64n
1080
- signal.getRate(); // 64
1081
- signal.getNyquist().getHertz(); // 32
1082
- signal.getResolution().getHertz(); // 1
1083
- signal.getCoherentGain(); // 0.5
1084
- signal.getNoiseBandwidth(); // 1.5
1085
- ```
1086
-
1087
- `getResolution()` est l'espacement des bins, non le pouvoir de résolution : il vaut `rate / count`
1088
- parce que la transformée a `count` bins, et le remplissage par des zéros change l'espacement sans
1089
- changer ce que la fenêtre peut séparer (section 14.8).
1090
-
1091
- ### 14.2 Les vingt-trois méthodes
1092
-
1093
- Les méthodes forment six familles. Sept sont des lectures spectrales, deux des transformées
1094
- temps–fréquence, quatre des grandeurs analytiques, trois des statistiques de séquence, cinq des
1095
- descripteurs de forme, et deux renvoient un autre signal.
1096
-
1097
- | Méthode | Retour | Convention |
1098
- | :--- | :--- | :--- |
1099
- | `spectrum(size?, window?)` | `Spectrum` | brut et non normalisé, grille complexe complète |
1100
- | `oneSided(size?, window?)` | `Bin[]` | moitié conjuguée repliée, DC et Nyquist non doublés |
1101
- | `amplitude(window?)` | `number[]` | `2·abs(X) / (count · CG)`, DC et Nyquist une seule fois |
1102
- | `power(window?)` | `number[]` | `A² / 2` par bin intérieur, `A²` en DC et Nyquist |
1103
- | `decibel(window?)` | `number[]` | `20·log10(amplitude)` |
1104
- | `powerDecibel(window?)` | `number[]` | `10·log10(puissance)` |
1105
- | `powerSpectralDensity(frameSize?, hopSize?, window?)` | `SpectralDensity` | moyenne de Welch, `abs(X)² / (rate · Σw²)` |
1106
- | `shortTimeFourierTransform(frameSize, hopSize, window?)` | `Spectrum[]` | bords étendus, temps au centre des trames |
1107
- | `spectrogram(frameSize, hopSize, window?)` | `Spectrogram` | les mêmes colonnes avec un axe de temps |
1108
- | `analyticSignal()` | `Complex[]` | paire de Hilbert `x + i·H(x)` |
1109
- | `instantaneousPhase()` | `Angle[]` | repliée dans `[0, 2π)` |
1110
- | `instantaneousFrequency(rate?, gate?)` | `number[]` | différence de phase déroulée, filtrée par l'enveloppe |
1111
- | `envelope()` | `number[]` | `abs(z[n])`, circulaire aux bords |
1112
- | `zeroCrossingRate(perSecond?)` | `number` | passages par zéro par paire d'échantillons |
1113
- | `spectralFlux(frameSize?, hopSize?, window?)` | `number[]` | distance de norme unitaire entre trames |
1114
- | `autocorrelation(maximumLag?, normalization?)` | `number[]` | coefficient par défaut, `ρ[0] = 1` |
1115
- | `spectralCentroid(window?)` | `Optional<Frequency>` | fréquence moyenne pondérée par la puissance |
1116
- | `spectralBandwidth(window?)` | `Optional<Frequency>` | écart-type pondéré par la puissance |
1117
- | `spectralRolloff(ratio?, window?)` | `Optional<Frequency>` | premier bin atteignant `ratio` de la puissance |
1118
- | `spectralFlatness(window?)` | `number` | moyenne géométrique sur arithmétique de la puissance |
1119
- | `signalToNoiseRatio(frameSize?, hopSize?, window?, floorQuantile?)` | `Optional<number>` | plancher médian, `10·log10` |
1120
- | `detrended(mode?)` | signal | `"none"`, `"constant"` (défaut), `"linear"` |
1121
- | `windowed(window?)` | signal | applique une fenêtre et l'adopte |
1122
-
1123
- Les arguments optionnels ne sont pas décoratifs. `frameSize` vaut `min(256, size)` par défaut, le
1124
- saut une demi-trame arrondie vers le bas, `instantaneousFrequency` filtre à `1E-3` de l'enveloppe
1125
- de crête, et `spectralRolloff` utilise `0.85`. Chaque défaut est la valeur qu'une implémentation de
1126
- référence choisit lorsque le paramètre est nommé sans être fourni.
1127
-
1128
- ### 14.3 Amplitude, puissance et densité
1129
-
1130
- Trois normalisations sont nécessaires parce que trois questions différentes sont posées.
1131
-
1132
- `amplitude()` répond « quelle est la taille du sinus sur ce bin » :
1133
- `2·abs(X[k]) / (count · CG)` pour un bin intérieur et `abs(X[k]) / (count · CG)` en DC et à
1134
- Nyquist. Le facteur deux replie la moitié conjuguée ; le gain cohérent retire la fenêtre. Omettre
1135
- l'un ou l'autre fait lire `0.5` à un sinus d'amplitude unité, soit moins 6,02 décibels.
1136
-
1137
- `power()` répond « quel carré moyen aurait un sinus sur ce bin » : `A² / 2` pour un bin intérieur
1138
- et `A²` en DC et à Nyquist, car ces deux-là n'oscillent pas. C'est la convention de crête, et elle
1139
- ne satisfait délibérément pas Parseval : la fenêtre étale chaque raie sur son lobe principal, donc
1140
- la somme de tous les bins dépasse le carré moyen de la bande de bruit équivalente — `1.5` pour
1141
- Hann.
1142
-
1143
- `powerSpectralDensity()` répond « quelle puissance par hertz » : `abs(X)² / (rate · Σw²)`, moyenné
1144
- sur des segments recouvrants. Seule la somme des poids au carré rend un plancher de bruit blanc
1145
- non biaisé ; diviser par `count` lit `0.375` pour un bruit unité, diviser par `count · CG²` lit
1146
- `1.5`.
1147
-
1148
- ```typescript
1149
- import { SynchronousSignalCollectable } from "semantic-typescript";
1150
-
1151
- const round = (value: number): number => Number(value.toFixed(4));
1152
- const tone = SynchronousSignalCollectable.useRate(
1153
- Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1154
- 256,
1155
- );
1156
-
1157
- round(tone.amplitude()[32]); // 1
1158
- round(tone.power()[32]); // 0.5
1159
- round(tone.power().reduce((sum, power) => sum + power, 0)); // 0.75
1160
- round(tone.powerSpectralDensity(64n, 32n).totalPower()); // 0.5
1161
- tone.powerSpectralDensity(64n, 32n).getSegments(); // 7n
1162
- ```
1163
-
1164
- Le couple de totaux est le contrôle à retenir : la somme en convention de crête vaut `0.75` tandis
1165
- que la densité s'intègre à `0.5`, et le rapport est exactement la bande de bruit. La densité est
1166
- aussi la seule des trois qui survive à un changement de longueur de trame.
1167
-
1168
- ### 14.4 Niveaux : les deux conventions de décibel
1169
-
1170
- Une amplitude et une puissance ne partagent pas de facteur, donc pas de logarithme.
1171
- `amplitudeDecibel` applique `20·log10` et convient à une tension, un déplacement ou une pression
1172
- acoustique ; `powerDecibel` applique `10·log10` et convient à une puissance, une énergie, un carré
1173
- moyen ou une densité. Les noms de méthodes du signal suivent la même séparation.
1174
-
1175
- ```typescript
1176
- import { amplitudeDecibel, powerDecibel, SynchronousSignalCollectable } from "semantic-typescript";
1177
-
1178
- const round = (value: number): number => Number(value.toFixed(4));
1179
- const tone = SynchronousSignalCollectable.useRate(
1180
- Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1181
- 256,
1182
- );
1183
-
1184
- amplitudeDecibel(0.25); // -12.0412
1185
- powerDecibel(0.25); // -6.0206
1186
- round(tone.decibel()[32]); // 0
1187
- round(tone.powerDecibel()[32]); // -3.0103
1188
- ```
1189
-
1190
- Les deux lectures du même son sont correctes : son amplitude vaut un (zéro décibel) et son carré
1191
- moyen vaut un demi (moins 3,01 décibels). Faire passer une puissance par la convention d'amplitude
1192
- double le niveau en décibels, transformant le moins 6,02 correct en moins 12,04.
1193
-
1194
- ### 14.5 Le repli unilatéral
1195
-
1196
- Un signal réel vérifie `X[count − k] = conj(X[k])`, la moitié des fréquences négatives ne porte
1197
- donc aucune information indépendante. Le repli double chaque bin intérieur et laisse les deux bins
1198
- auto-conjugués — DC et, pour une longueur paire, Nyquist — exactement tels que la transformée les a
1199
- produits. Ces deux-là sont leur propre miroir ; les doubler invente de l'énergie et rapporte
1200
- exactement 6,02 décibels de trop.
1201
-
1202
- ```typescript
1203
- import { SynchronousSignalCollectable } from "semantic-typescript";
1204
-
1205
- const round = (value: number): number => Number(value.toFixed(4));
1206
- const edge = SynchronousSignalCollectable.useRate(
1207
- Array.from({ length: 16 }, (_unused, index) => 2 + Math.cos(Math.PI * index)),
1208
- 16,
1209
- );
1210
- const bins = edge.oneSided();
1211
-
1212
- bins.length; // 9
1213
- round(bins[0].magnitude()); // 16
1214
- round(bins[8].magnitude()); // 8
1215
- round(edge.amplitude()[0]); // 2
1216
- round(edge.amplitude()[8]); // 1
1217
- ```
1218
-
1219
- Les magnitudes brutes valent `16` et `8` parce que les poids de la fenêtre somment à huit ; diviser
1220
- par `count · CG = 8` en fait les amplitudes calibrées `2` et `1`. Doubler les bins de bord lirait
1221
- `4` et `2`, et l'erreur ne lèverait jamais d'exception.
1222
-
1223
- ### 14.6 Vues temps–fréquence
1224
-
1225
- `shortTimeFourierTransform` et `spectrogram` découpent l'enregistrement comme la transformée à
1226
- court terme de référence : une demi-trame de zéros à chaque extrémité, puis une queue complétée par
1227
- des zéros afin qu'il reste un nombre entier de sauts. La première trame est centrée sur le premier
1228
- échantillon, la dernière atteint le dernier échantillon, et la trame `m` est centrée à
1229
- `m · hop / rate` secondes. Ne garder que les trames complètes jetterait jusqu'à `frameSize − 1`
1230
- échantillons — pour 1000 échantillons, une trame de 256 et un saut de 128, cela fait 104
1231
- échantillons, environ un dixième de l'enregistrement.
1232
-
1233
- ```typescript
1234
- import { SynchronousSignalCollectable } from "semantic-typescript";
1235
-
1236
- const tone = SynchronousSignalCollectable.useRate(
1237
- Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 4 * index / 64)),
1238
- 64,
1239
- );
1240
- const spectrogram = tone.spectrogram(16n, 8n);
1241
-
1242
- spectrogram.size(); // 9
1243
- spectrogram.getFrameSize(); // 16
1244
- spectrogram.getBoundary(); // "zeros"
1245
- spectrogram.getTime(0); // 0
1246
- spectrogram.getTime(6); // 0.75
1247
- tone.shortTimeFourierTransform(16n, 8n).length; // 9
1248
- tone.spectralFlux(16n, 8n).length; // 8
1249
- ```
1250
-
1251
- Chaque colonne est un `Spectrum` de `frameSize` bins, exactement ce que `spectrum()` produirait
1252
- pour la même trame, et `getTime()` rapporte des centres de trame, non des débuts.
1253
-
1254
- ### 14.7 Fenêtres, gain cohérent et COLA
1255
-
1256
- Chaque forme a deux variantes. La variante symétrique divise par `count − 1` ; la variante
1257
- périodique (DFT paire) divise par `count`, un échantillon de moins qu'une période complète, de
1258
- sorte que la fenêtre est continue sous l'extension périodique que la DFT suppose. L'analyse et
1259
- l'addition-recouvrement exigent la forme périodique ; la conception de filtres garde la symétrique.
1260
-
1261
- La différence est mesurable et ne lève jamais d'erreur. À cinquante pour cent de recouvrement, la
1262
- fenêtre de Hann périodique somme à un en toute position ; la forme symétrique ondule, de `0.84`
1263
- décibel à seize échantillons, `0.21` à soixante-quatre et encore `0.013` à mille vingt-quatre. La
1264
- classe expose le gain cohérent et la bande de bruit équivalente de la fenêtre qu'elle détient, si
1265
- bien que la correction n'est jamais codée en dur : un Hann symétrique lit `0.4375` à huit
1266
- échantillons, pas `0.5`.
1267
-
1268
- ```typescript
1269
- import {
1270
- coherentGain, equivalentNoiseBandwidth, useFlatTopWindow,
1271
- useHannPeriodicWindow, useHannWindow,
1272
- } from "semantic-typescript";
1273
-
1274
- const round = (value: number): number => Number(value.toFixed(4));
1275
-
1276
- round(useHannWindow(1, 4)); // 0.75
1277
- round(useHannPeriodicWindow(1, 4)); // 0.5
1278
- coherentGain(useHannPeriodicWindow, 16); // 0.5
1279
- equivalentNoiseBandwidth(useHannPeriodicWindow, 16); // 1.5
1280
- useFlatTopWindow(0, 1024); // -0.000421
1281
- Array.from({ length: 1024 }, (_unused, i) => useFlatTopWindow(i, 1024)).filter(w => w < 0).length; // 578
1282
- ```
1283
-
1284
- La ligne du flat-top n'est pas une erreur du tableau. Une fenêtre flat-top est partiellement
1285
- négative par définition — ici 578 poids sur 1024 — et ramener ces poids à zéro perturberait le
1286
- lobe principal et détruirait la faible perte de scalloping pour laquelle la fenêtre existe. La
1287
- bibliothèque renvoie la série inchangée.
1288
-
1289
- ### 14.8 Pièges
1290
-
1291
- **Le remplissage par des zéros interpole, il ne résout jamais.** Une longueur de transformée
1292
- supérieure au nombre d'échantillons échantillonne la même DTFT plus densément : la *position* du
1293
- pic s'affine et l'espacement des bins devient `rate / size`, mais la capacité à séparer deux
1294
- composantes reste gouvernée par la longueur et la forme de la fenêtre. Un son exactement entre deux
1295
- bins lit `82.1093` et `80.8598` sur la grille non complétée, et `128` à sa propre fréquence sur la
1296
- grille complétée.
1297
-
1298
- ```typescript
1299
- import { SynchronousSignalCollectable, useRectangularWindow } from "semantic-typescript";
1300
-
1301
- const round = (value: number): number => Number(value.toFixed(4));
1302
- const tone = SynchronousSignalCollectable.useRate(
1303
- Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 100.5 * index / 256)),
1304
- 256,
1305
- );
1306
- const flat = tone.spectrum(256n, useRectangularWindow);
1307
- const padded = tone.spectrum(1024n, useRectangularWindow);
1308
-
1309
- flat.getResolution().getHertz(); // 1
1310
- padded.getResolution().getHertz(); // 0.25
1311
- round(flat.magnitudeAt(100).get()); // 82.1093
1312
- round(flat.magnitudeAt(101).get()); // 80.8598
1313
- round(padded.magnitudeAt(402).get()); // 128
1314
- padded.frequencyAt(402).get().getHertz(); // 100.5
1315
- ```
1316
-
1317
- Deux sons égaux espacés de `0.4` bin restent fusionnés quel que soit le remplissage : avec un
1318
- enregistrement de 256 points et une fenêtre de Hann, compléter à 8192 ne produit qu'un seul maximum
1319
- local, pas deux.
1320
-
1321
- **La transformée de Hilbert est circulaire.** `analyticSignal`, `envelope` et
1322
- `instantaneousPhase` traitent le premier et le dernier échantillon comme voisins, si bien qu'un
1323
- enregistrement dont la période ne divise pas la longueur est contaminé aux deux bords. Le milieu de
1324
- l'enregistrement est fiable, les bords non.
1325
-
1326
- ```typescript
1327
- import { SynchronousSignalCollectable } from "semantic-typescript";
1328
-
1329
- const round = (value: number): number => Number(value.toFixed(4));
1330
- const edge = SynchronousSignalCollectable.useRate(
1331
- Array.from({ length: 512 }, (_unused, index) => Math.cos(2 * Math.PI * 8.3 * index / 512)),
1332
- 512,
1333
- );
1334
- const envelope = edge.envelope();
1335
-
1336
- round(envelope.slice(8, -8).reduce((sum, value) => sum + value, 0) / 496); // 1.0025
1337
- round(Math.max(...envelope.slice(0, 8))); // 1.2592
1338
- round(Math.min(...envelope.slice(-8))); // 0.1314
1339
- ```
1340
-
1341
- **Une enveloppe qui s'annule ne définit pas de phase.** Là où l'enveloppe s'effondre, la différence
1342
- de deux angles de bruit d'arrondi peut atteindre un demi-tour, ce qui se lit comme la moitié du
1343
- taux d'échantillonnage. Le seuil est ce qui garde ce nombre significatif, et le défaut est `1E-3`
1344
- de l'enveloppe de crête.
1345
-
1346
- ```typescript
1347
- import { SynchronousSignalCollectable } from "semantic-typescript";
1348
-
1349
- const round = (value: number): number => Number(value.toFixed(4));
1350
- const beat = SynchronousSignalCollectable.useRate(
1351
- Array.from({ length: 4096 }, (_unused, index) =>
1352
- Math.cos(2 * Math.PI * 100 * index / 2048) + Math.cos(2 * Math.PI * 102 * index / 2048)),
1353
- 2048,
1354
- );
1355
-
1356
- round(Math.max(...beat.instantaneousFrequency(2048, 0).map(Math.abs))); // 920.218
1357
- round(Math.max(...beat.instantaneousFrequency().map(Math.abs))); // 101
1358
- ```
1359
-
1360
- **Une corrélation non normalisée n'est pas une similarité.** La somme brute `r[k]` croît avec la
1361
- longueur de l'enregistrement, deux longueurs ne sont donc pas comparables. La règle par défaut
1362
- `"coefficient"` divise par `r[0]` et garde chaque valeur dans `[-1, 1]` ; `"unbiased"` divise par
1363
- `count − k` et paie d'une variance qui croît avec le décalage.
1364
-
1365
- ```typescript
1366
- import { SynchronousSignalCollectable } from "semantic-typescript";
1367
-
1368
- const round = (value: number): number => Number(value.toFixed(4));
1369
- const periodic = SynchronousSignalCollectable.useRate(
1370
- Array.from({ length: 1024 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 1024)),
1371
- 1024,
1372
- );
1373
- const rho = periodic.autocorrelation(200n);
1374
-
1375
- round(rho[0]); // 1
1376
- round(rho[128]); // 0.875
1377
- round(rho[64]); // -0.9375
1378
- round(periodic.autocorrelation(200n, "none")[0]); // 512
1379
- ```
1380
-
1381
- Une dernière lecture mérite un avertissement : `spectralFlux` normalise chaque trame avant de la
1382
- comparer, un simple changement de niveau produit donc un flux nul. Sa première et sa dernière
1383
- valeur sont des artefacts de bord de la frontière de zéros, pas des attaques.
1384
-
1385
- ---
1386
-
1387
- <a id="15-license"></a>
1388
- ## 15. Licence
1389
-
1390
- MIT © Eloy Kim
1
+ # **Semantic-TypeScript**
2
+
3
+ **Flux, indexés.** Vos données, sous un contrôle précis.
4
+
5
+ [English](./readme.md) · [简体中文](./readme.cn.md) · [繁體中文](./readme.tw.md) · [日本語](./readme.jp.md) · [한국어](./readme.kr.md) · [Deutsch](./readme.de.md) · Français · [Español](./readme.es.md) · [Русский](./readme.ru.md)
6
+
7
+ Une bibliothèque de traitement de flux typée sûre qui unifie **les générateurs JavaScript**, **Java Stream** et
8
+ **l'adressage de données par index**. Chaque élément porte un index `bigint`, chaque pipeline est paresseux, et
9
+ chaque opération terminale est un choix explicite.
10
+
11
+ ```bash
12
+ npm install semantic-typescript
13
+ ```
14
+
15
+ - Zéro dépendance au runtime, pur ESM, fournit ses propres types.
16
+ - 684 tests de non-régression : `npm test`.
17
+ - Historique des changements complet : [CHANGELOG.md](./CHANGELOG.md).
18
+
19
+ ---
20
+
21
+ ## Table des matières
22
+
23
+ 1. [Philosophie de conception](#1-design-philosophy)
24
+ 2. [Architecture](#2-architecture)
25
+ 3. [Démarrage rapide](#3-quick-start)
26
+ 4. [Fonctions de fabrique](#4-factory-functions)
27
+ 5. [Opérateurs de transformation](#5-transformation-operators)
28
+ 6. [Collectables et opérations terminales](#6-collectables-and-terminal-operations)
29
+ 7. [Statistiques et fenêtres](#7-statistics-and-windows)
30
+ 8. [Le modèle d'index](#8-the-index-model)
31
+ 9. [Le modèle asynchrone](#9-the-asynchronous-model)
32
+ 10. [Analyse périodique : anneaux, champs et scores](#10-periodic-analysis-rings-fields-and-scores)
33
+ 11. [Fonctions utilitaires](#11-utility-helpers)
34
+ 12. [Performance et pièges](#12-performance-and-pitfalls)
35
+ 13. [Anneaux de phase : un cycle exact](#13-phase-rings)
36
+ 14. [Analyse du signal : fenêtres, spectres et temps](#14-signal-analysis)
37
+ 15. [Licence](#15-license)
38
+
39
+ ---
40
+
41
+ <a id="1-design-philosophy"></a>
42
+ ## 1. Philosophie de conception
43
+
44
+ Trois idées, un seul pipeline :
45
+
46
+ **L'index, c'est de la donnée.** Chaque élément est adressé par un `bigint`. L'inversion, la rotation, le mélange et
47
+ le partitionnement sont de l'arithmétique d'index, pas des opérations sur tampon. Rien n'est matérialisé tant qu'un
48
+ collectable ne le demande.
49
+
50
+ **La paresse est le défaut.** Une fabrique encapsule un générateur ; un opérateur en encapsule un autre. Aucun
51
+ élément n'est demandé tant qu'une opération terminale ne s'exécute, si bien qu'une source infinie est parfaitement
52
+ légale tant que quelque chose en aval l'arrête.
53
+
54
+ **Les terminaux sont explicites.** `map` renvoie un `Semantic`, pas un tableau. Les valeurs n'apparaissent que
55
+ lorsque vous nommez un collectable — `toUnordered()`, `toOrdered()`, `toNumericStatistics()`, `toWindow()` — et
56
+ chacun énonce ce qu'il coûte et quel ordre il produit.
57
+
58
+ La même discipline s'applique à la moitié mathématique de la bibliothèque. Un `Complex`, un `Angle` ou une
59
+ `Frequency` est une valeur immuable dont les invariants sont vérifiés à la construction ; un `Optional` remplace
60
+ `null` ; et un anneau de phase énonce exactement combien d'éléments il contient, quelle période il a et
61
+ dans quelle mesure il est équilibré.
62
+
63
+ ---
64
+
65
+ <a id="2-architecture"></a>
66
+ ## 2. Architecture
67
+
68
+ ```
69
+ factory ──▶ Semantic ──▶ Semantic ──▶ Collectable ──▶ result
70
+ (source) (map) (filter) (terminal)
71
+ ▲
72
+ └── every operator returns a new Semantic wrapping a generator
73
+ ```
74
+
75
+ | Couche | Module | Responsabilité |
76
+ | :--- | :--- | :--- |
77
+ | Sources | `synchronous/collectors`, `asynchronous/collectors` | Encapsule un itérable, un générateur, une promesse, un blob, une cible d'événement |
78
+ | Pipelines | `synchronous/semantic`, `asynchronous/semantic` | Opérateurs paresseux et réécriture d'index |
79
+ | Collectables | `synchronous/semantic`, `asynchronous/semantic` | Terminaux ordonnés, non ordonnés, statistiques, fenêtres et personnalisés |
80
+ | Collecteurs | `synchronous/collector`, `asynchronous/collector` | Les primitives de pliage sur lesquelles chaque terminal est bâti |
81
+ | Mathématiques | `mathematics/*` | Angles, complexes, fréquences, spectres, fenêtres, estimateurs, épicycles, anneaux de phase |
82
+ | Structures | `structure/*` | Listes, ensembles, tables, arbres, forêts, anneaux, champs |
83
+ | Erreurs | `structure/error` | La hiérarchie typée `SemanticError` et ses gardes de marque |
84
+
85
+ Les familles synchrone et asynchrone partagent un seul vocabulaire : les mêmes noms d'opérateurs, la même
86
+ sémantique d'index, les mêmes collectables. Les variantes asynchrones se contentent de renvoyer des promesses, et
87
+ elles mettent les écritures en file d'attente afin qu'une lecture émise après une écriture l'observe toujours.
88
+
89
+ ### 2.1 Ce que cette version a déplacé et cassé
90
+
91
+ Trois modules ont changé de place ou de forme, et une série de valeurs par défaut a changé de sens. Chaque ancien comportement reste accessible par une option explicite ; rien n'a été retiré sans remplacement.
92
+
93
+ | Changement | Avant | Maintenant |
94
+ | :--- | :--- | :--- |
95
+ | Forêts | `structure/tree/rootedForest`, `structure/tree/unionFindForest` | `structure/forest/rootedForest`, `structure/forest/unionFindForest` ; `tree/index.ts` ne les réexporte plus |
96
+ | Erreurs | des erreurs natives ad hoc | une hiérarchie de quinze classes dans `structure/error.ts`, avec une garde de marque par classe et l'alias `SemanticRangeError` |
97
+ | Options d'estimateur | des définitions figées | `mathematics/quantile.ts` exporte `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator` et `VarianceOptions` |
98
+ | `quantile` | Hyndman–Fan type 1 | type 7 — R `quantile`, NumPy `linear`, Excel `PERCENTILE.INC` |
99
+ | `skewness` / `kurtosis` | `g1` / `g2` | les estimateurs corrigés du biais `G1` / `G2` |
100
+ | `variance` / `standardDeviation` | `ddof = 0` | `ddof = 1`, enveloppé dans un `Optional` |
101
+ | `decibel` | une fonction `20·log10` | supprimée ; `amplitudeDecibel` (20·log10) et `powerDecibel` (10·log10) |
102
+ | `Spectrum.flatness()` | les magnitudes sur les deux moitiés | les puissances sur la moitié unilatérale |
103
+ | `Spectrum.bandEnergy()` | la somme brute bilatérale | unilatérale par défaut, avec repliement de la moitié conjuguée |
104
+ | `shortTimeFourierTransform` / `spectrogram` | Hann symétrique, trames complètes seulement | Hann périodique, `boundary: "zeros"`, `padded: true` |
105
+
106
+ Les chapitres 6, 7 et 10 documentent les remplacements ; les conséquences mesurées sont dans [CHANGELOG.md](./CHANGELOG.md).
107
+
108
+ ---
109
+
110
+ <a id="3-quick-start"></a>
111
+ ## 3. Démarrage rapide
112
+
113
+ ```typescript
114
+ import {
115
+ useOf, useFrom, useRange, useText, useStringify, usePromise
116
+ } from "semantic-typescript";
117
+
118
+ // Numeric aggregation
119
+ const sum: number = useOf(10, 20, 30, 40)
120
+ .map(n => n * 2)
121
+ .toNumericStatistics()
122
+ .summate(); // 200
123
+
124
+ // BigInt aggregation
125
+ const bigSum: bigint = useOf(10n, 20n, 30n, 40n)
126
+ .map(n => n * 2n)
127
+ .toBigintStatistics()
128
+ .summate(); // 200n
129
+
130
+ // Reordering is index arithmetic — collect with toOrdered() for it to take effect
131
+ const reversed: number[] = useFrom([1, 2, 3, 4, 5]).reverse().toOrdered().toArray(); // [5,4,3,2,1]
132
+ const rotated: number[] = useFrom([1, 2, 3, 4, 5]).translate(2).toOrdered().toArray(); // [4,5,1,2,3]
133
+
134
+ // Lazy evaluation over a large range
135
+ const multiples: bigint[] = useRange(0n, 1_000_000n)
136
+ .filter(n => n % 17n === 0n)
137
+ .limit(10n)
138
+ .toUnordered()
139
+ .toArray(); // [0n,17n,…,153n]
140
+
141
+ // Unicode-safe string processing (code points, not UTF-16 units)
142
+ useText("semantic 👍").toUnordered().forEach(character => console.log(character));
143
+
144
+ // Circular-safe serialisation
145
+ const cyclic: any = { a: 1 }; cyclic.self = cyclic;
146
+ useStringify(cyclic); // '{"a":1}'
147
+
148
+ // Terminals hand the elements to real structures, not only to arrays
149
+ useOf(3, 1, 4, 1, 5).toCollectable().toOrderedSet().toArray(); // [1,3,4,5]
150
+ useFrom([3, 1, 4, 1, 5]).toCollectable().toBinarySearchTree().size(); // 5n
151
+
152
+ // Async sources
153
+ const total = await usePromise(Promise.resolve(7)).toNumericStatistics().summate(); // 7
154
+ ```
155
+
156
+ ---
157
+
158
+ <a id="4-factory-functions"></a>
159
+ ## 4. Fonctions de fabrique
160
+
161
+ La construction est toujours en O(1) — une fabrique se contente d'encapsuler un générateur. Le tableau indique le
162
+ coût de *consommation* de `n` éléments, plus l'espace supplémentaire dont la source elle-même a besoin.
163
+
164
+ | Fabrique | Signature (résultat) | Temps (consommer n) | Espace (source) | Notes |
165
+ | :--- | :--- | :--- | :--- | :--- |
166
+ | `useOf` | `useOf<E>(...elements) → SynchronousSemantic<E>` | O(n) | O(n) | Les index vont de 0..n−1 ; l'argument spread est déjà matérialisé |
167
+ | `useFrom` | `useFrom<E>(Iterable<E>) → SynchronousSemantic<E>`<br>`useFrom<E>(AsyncIterable<E>) → AsynchronousSemantic<E>` | O(n) | O(1) | Encapsule tout itérable ; itéré une fois à la collecte |
168
+ | `useRange` | `useRange<N>(start, end, step?) → SynchronousSemantic<N>` | O(n) | O(1) | `end` est exclusif ; `step` doit être non nul ; `number` et `bigint` fonctionnent tous deux |
169
+ | `useFill` | `useFill<E>(element \| supplier, count) → SynchronousSemantic<E>` | O(n) | O(1) | Un fournisseur est appelé une fois par élément |
170
+ | `useEmpty` | `useEmpty<E>() → SynchronousSemantic<E>` | O(1) | O(1) | Élément neutre pour `concat` |
171
+ | `useText` | `useText(text, delimiter?)` | O(n) | O(n) pour le texte | Itère sur les **points de code**, si bien que les emoji et le CJK restent des éléments uniques |
172
+ | `useAttribute` | `useAttribute<T>(object)` | O(v + e) | O(d + v) | Parcourt récursivement les objets imbriqués avec protection anti-cycle |
173
+ | `useGenerate` | `useGenerate<E>(supplier, interrupt)` | O(n) × coût du fournisseur | O(1) | **Doit** avoir un `interrupt` d'arrêt ou un `limit` en aval |
174
+ | `useIterate` | `useIterate<E>(Iterable<E>)` / `(AsyncIterable<E>)` | O(n) par collecte | O(1) | Rejoue l'itérateur à chaque collecte |
175
+ | `useInterval` | `useInterval(period, delay?) → AsynchronousSemantic<number>` | n × period (temps réel) | O(1) | Émet le temps écoulé |
176
+ | `usePromise` | `usePromise<T>(promise) → AsynchronousSemantic<T>` | O(1) | O(1) | Un seul élément à l'index `0n` |
177
+ | `useBlob` | `useBlob(blob, chunk?) → AsynchronousSemantic<Uint8Array>` | O(blob size) | O(chunk) par copie | Le morceau par défaut est de 64 Kio |
178
+ | `useSubscription` | `useSubscription(target, subscriber, event) → AsynchronousSemantic<Event>` | un élément par événement | O(1) | `limit()` / `takeWhile()` définissent la frontière de désabonnement |
179
+
180
+ ```typescript
181
+ useOf(1, 2, 3).toUnordered().toArray(); // [1,2,3]
182
+ useFrom(new Set([1, 2, 2])).toUnordered().toArray(); // [1,2]
183
+ useRange(0, 5).toUnordered().toArray(); // [0,1,2,3,4]
184
+ useRange(10n, 0n, -5n).toUnordered().toArray(); // [10n,5n]
185
+ useFill("x", 3n).toUnordered().toArray(); // ["x","x","x"]
186
+ useText("A😊B").toUnordered().toArray(); // ["A","😊","B"]
187
+ useGenerate(() => 7, (_e, index) => index >= 3n).toUnordered().toArray(); // [7,7,7]
188
+ ```
189
+
190
+ ---
191
+
192
+ <a id="5-transformation-operators"></a>
193
+ ## 5. Opérateurs de transformation
194
+
195
+ Chaque opérateur renvoie un nouveau `Semantic` ; aucun ne touche un élément tant qu'un collectable ne s'exécute.
196
+ Le temps est le coût supplémentaire **par élément** ; l'espace est ce que l'opérateur conserve pendant l'exécution.
197
+
198
+ | Opérateur | Objectif | Effet sur l'index | Temps | Espace |
199
+ | :--- | :--- | :--- | :--- | :--- |
200
+ | `map(mapper)` | transformation 1:1 | transmis | O(1) | O(1) |
201
+ | `filter(predicate)` | conserve les éléments correspondants | transmis | O(1) | O(1) |
202
+ | `flat(mapper)` / `flatMap(mapper)` | aplatissement 1:N | re-indexé à partir de 0 | O(1) par élément émis | O(1) |
203
+ | `distinct(keyExtractor?)` | supprime les clés répétées, conserve la première | transmis | O(1) en moyenne | O(n) |
204
+ | `peek(consumer)` | effet de bord, l'élément passe à travers | transmis | O(1) | O(1) |
205
+ | `limit(count)` | prend les *n* premiers | re-indexé à partir de 0 | O(1) | O(1) |
206
+ | `skip(count)` | supprime les *n* premiers | transmis | O(1) | O(1) |
207
+ | `sub(start, end)` | tranche positionnelle `[start, end)` | transmis | O(1) | O(1) |
208
+ | `takeWhile(predicate)` | prend tant que le prédicat est vrai | transmis | O(1) | O(1) |
209
+ | `dropWhile(predicate)` | supprime tant que le prédicat est vrai | re-indexé à partir de 0 | O(1) | O(1) |
210
+ | `concat(other)` | ajoute un semantic ou un itérable | poursuit l'index | O(1) | O(1) |
211
+ | `redirect(redirector)` | calcule un nouvel index comme vous le souhaitez | **le vôtre** | O(1) | O(1) |
212
+ | `reverse()` | `redirect((_e, i) => -i - 1n)` | négatif et décalé | O(1) | O(1) |
213
+ | `translate(offset \| mapper)` | décale chaque index | `i + offset` | O(1) | O(1) |
214
+ | `shuffle(mapper?)` | rend l'index aléatoire | aléatoire | O(1) | O(1) |
215
+ | `pipe(conversion)` | confie le générateur à une fonction personnalisée | à votre discrétion | le vôtre | le vôtre |
216
+ | `semantic()` | remonte un collectable dans un semantic | inchangé | O(1) | O(1) |
217
+
218
+ **`reverse`, `translate` et `shuffle` ne réécrivent que des index.** Rien n'est mis en tampon ni tourné tant que
219
+ `toOrdered()` n'a pas normalisé les index modulo le nombre d'éléments :
220
+
221
+ ```typescript
222
+ useFrom([1, 2, 3, 4, 5]).reverse().toOrdered().toArray(); // [5,4,3,2,1]
223
+ useFrom([1, 2, 3, 4, 5]).translate(2).toOrdered().toArray(); // [4,5,1,2,3] (rotation)
224
+ useFrom([1, 2, 3, 4, 5]).translate(7).toOrdered().toArray(); // [4,5,1,2,3] (7 ≡ 2 mod 5)
225
+ useFrom([1, 2, 3]).shuffle((_e, i) => -i - 1n).toOrdered().toArray(); // [3,2,1]
226
+ ```
227
+
228
+ ---
229
+
230
+ <a id="6-collectables-and-terminal-operations"></a>
231
+ ## 6. Collectables et opérations terminales
232
+
233
+ ### 6.1 Collectables
234
+
235
+ Le collectable possède à la fois l'**ordre** et la **comptabilité des index**. `n` est le nombre d'éléments.
236
+
237
+ | Collectable | Structure | Temps | Ordre | À utiliser quand |
238
+ | :--- | :--- | :--- | :--- | :--- |
239
+ | `toUnordered()` | `Map<bigint, E>` indexée par l'index | O(n) | insertion | débit maximal ; les index bruts sont préservés intacts |
240
+ | `toOrdered()` | tableau trié par index d'élément | O(n log n) | par index normalisé | une inversion/rotation doit prendre effet |
241
+ | `toOrdered(comparator)` | trié par votre comparateur | O(n log n) | par votre comparateur | vues triées |
242
+ | `toNumericStatistics()` | tampon trié par valeur | O(n log n) | par valeur | statistiques sur `number` |
243
+ | `toBigintStatistics()` | tampon trié par valeur | O(n log n) | par valeur | statistiques sur `bigint` |
244
+ | `toWindow()` | tampon trié par valeur | O(n log n) | par valeur | `slide` / `tumble` |
245
+ | `toCollectable(mapper)` | ce que renvoie le mapper | — | — | collectables personnalisés |
246
+
247
+ ### 6.2 Terminaux
248
+
249
+ Chaque terminal vit sur le **collectable**, pas sur le `Semantic`. C'est ce que « les terminaux sont explicites » veut dire en pratique : un pipeline ne produit rien tant qu'un collectable n'est pas nommé, et `toCollectable()` sans mapper est la façon la moins coûteuse de le nommer.
250
+
251
+ | Terminal | Renvoie | Temps | Notes |
252
+ | :--- | :--- | :--- | :--- |
253
+ | `toArray()` / `toSet()` | `Array<E>` / `Set<E>` | O(n) | Vide le tampon ; `toSet` dédoublonne par identité |
254
+ | `toMap(key, value?)` | `Map<K, V>` | O(n) en moyenne | La dernière écriture l'emporte |
255
+ | `count()` / `isEmpty()` | `bigint` / `boolean` | O(1) | Le tampon le sait déjà |
256
+ | `forEach(action)` | `void` | O(n) | `action(element, index)` |
257
+ | `join(…)` | `string` | O(n) | `join()`, `join(delimiter)`, `join(prefix, delimiter, suffix)` |
258
+ | `reduce(…)` | `Optional<E>` / `E` / `R` | O(n) | Flux vide → `Optional` vide |
259
+ | `group(classifier)` / `groupBy(key, value)` | `Map<K, E[]>` | O(n) en moyenne | — |
260
+ | `partition(count)` | `Array<Array<E>>` | O(n) | Round-robin par index |
261
+ | `partitionBy(classifier)` | `Array<Array<E>>` | O(n) | Le numéro de seau provient du classifieur |
262
+ | `anyMatch` / `allMatch` / `nonMatch` | `boolean` | O(n) au pire, avec court-circuit | — |
263
+ | `findAny` / `findFirst` / `findLast` | `Optional<E>` | O(1)–O(n) | Positionnel dans l'ordre matérialisé |
264
+ | `findAt(k)` / `findNegativeAt(k)` | `Optional<E>` | O(k) / O(n−k) | Compte depuis le début / la fin |
265
+ | `findMaximum(cmp?)` / `findMinimum(cmp?)` | `Optional<E>` | O(n) | Comparaison structurelle par défaut |
266
+ | `frequency()` | `Map<E, bigint>` | O(n) en moyenne | Collectables de statistiques uniquement |
267
+ | `write(stream, accumulator?)` | `WritableStream<S>` | O(n) | Terminal de type E/S |
268
+ | `collect(collector)` | `R` | O(n) | Le pliage générique derrière chaque terminal ci-dessus |
269
+
270
+ ### 6.3 Collecteurs de structure
271
+
272
+ `toArray`, `toSet` et `toMap` terminent un pipeline dans un conteneur JavaScript. Douze autres terminaux le terminent dans l'une des structures de la bibliothèque elle-même, si bien que le résultat porte les invariants que cette structure garantit plutôt que ceux qu'un tableau possède par accident. Ils sont disponibles sur chaque collectable, y compris le collectable nu rendu par `toCollectable()`.
273
+
274
+ | Terminal | Renvoie | Ordre | Coût |
275
+ | :--- | :--- | :--- | :--- |
276
+ | `toArray()` | `Array<E>` | ordre de parcours | O(n) |
277
+ | `toSet()` | `Set<E>` | ordre d'insertion, SameValueZero | O(n) en moyenne |
278
+ | `toMap(key, value?)` | `Map<K, V>` | ordre d'insertion ; la dernière écriture l'emporte | O(n) en moyenne |
279
+ | `toOrderedSet(comparator?)` | `SynchronousOrderedSet<E>` | croissant selon le comparateur | O(n²) au pire |
280
+ | `toUnorderedSet()` | `SynchronousUnorderedSet<E>` | non spécifié (seaux de hachage) | O(n) attendu |
281
+ | `toOrderedMap(key, value?)` | `SynchronousOrderedMap<K, V>` | ordre d'insertion ; la dernière écriture l'emporte | O(n) attendu |
282
+ | `toUnorderedMap(key, value?)` | `SynchronousUnorderedMap<K, V>` | non spécifié | O(n) attendu |
283
+ | `toList()` | `SynchronousList<E>` | ordre de parcours, doublons conservés | O(n) |
284
+ | `toRing(span?)` | `SynchronousRing<E>` | l'ordre de parcours fixe les positions sur l'anneau | O(n) |
285
+ | `toLRUSet(capacity)` | `SynchronousUnorderedLRUSet<E>` | par récence ; seuls les `capacity` derniers survivent | O(n²) au pire |
286
+ | `toLRUMap(capacity, key, value?)` | `SynchronousUnorderedLRUMap<K, V>` | par récence | O(n) attendu |
287
+ | `toSkipSet(comparator?)` | `SynchronousSkipSet<E>` | croissant selon le comparateur, navigable | O(n²) au pire |
288
+ | `toSkipMap(key, value?)` | `SynchronousSkipMap<K, V>` | selon le comparateur de clés, navigable | O(n) attendu |
289
+ | `toBinarySearchTree(comparator?)` | `SynchronousBinarySearchTree<E>` | croissant selon le comparateur, équilibré en hauteur | O(n log n) |
290
+ | `toRootedForest()` | `SynchronousRootedForest<E>` | l'ordre de parcours devient la structure | O(n) |
291
+
292
+ Le type du résultat suit le type d'élément du collectable lui-même, donc aucun terminal ne prend d'argument de type : un `SynchronousCollectable<E>` vous remet directement `Array<E>`, `SynchronousList<E>` ou `SynchronousOrderedSet<E>`. Seuls restent les arguments métier — le comparateur de `toOrderedSet` / `toSkipSet` / `toBinarySearchTree`, la capacité de la paire LRU, l'étendue de `toRing`, et les extracteurs de clé et de valeur de la famille des tables.
293
+
294
+ Un anneau est un ensemble, donc les éléments répétés gardent leur première position ; toutes les autres structures ci-dessus conservent les doublons sauf si leur propre contrat en décide autrement (`toOrderedSet`, `toUnorderedSet` et `toSkipSet` sont des ensembles ; `toBinarySearchTree` et `toList` n'en sont pas).
295
+
296
+ ```typescript
297
+ const numbers = useOf(3, 1, 4, 1, 5).toCollectable();
298
+
299
+ numbers.toOrderedSet().toArray(); // [1,3,4,5]
300
+ numbers.toUnorderedSet().size(); // 4n — the duplicate 1 collapses
301
+ numbers.toList().toArray(); // [3,1,4,1,5]
302
+ numbers.toSkipSet().first().get(); // 1 — navigable by comparator
303
+ numbers.toBinarySearchTree().size(); // 5n — duplicates retained
304
+ numbers.toRootedForest().getRoots().length; // 1
305
+ useOf(1, 2, 3).toCollectable().toRing().phaseAt(3n).get().toDegreeValue(); // 0
306
+ useOf(3, 1, 3).toCollectable().toOrderedMap(n => n, n => `v${n}`).get(3).get(); // "v3"
307
+ ```
308
+
309
+ Les collectables asynchrones exposent les mêmes terminaux et résolvent chacun dans une promesse ; la forme de l'appel ne gagne donc qu'un `await`.
310
+
311
+ ---
312
+
313
+ <a id="7-statistics-and-windows"></a>
314
+ ## 7. Statistiques et fenêtres
315
+
316
+ ### 7.1 La surface statistique
317
+
318
+ ```typescript
319
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().summate(); // 14
320
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().average(); // 2.8
321
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().median(); // 3
322
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().mode(); // 1
323
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().variance().get(); // 3.2 (n − 1)
324
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().standardDeviation().get(); // 1.7889 (n − 1)
325
+ ```
326
+
327
+ Les deux familles statistiques exposent la même surface — `summate`, `average`, `median`, `mode`, `variance`, `standardDeviation`, `frequency`, `findMaximum`, `findMinimum` — avec de l'arithmétique `bigint` pour `toBigintStatistics()`. Les mesures de dispersion et la famille des quantiles renvoient un `Optional`, car un échantillon peut être trop petit pour qu'elles soient définies ; déballez-les avec `.get()`, `.orElse(…)` ou `.isPresent()`.
328
+
329
+ ### 7.2 Estimateurs et valeurs par défaut
330
+
331
+ Les valeurs par défaut sont les définitions qu'utilisent R, NumPy, pandas et Excel : une valeur calculée ici se compare donc à ces outils sans facteur de correction.
332
+
333
+ | Statistique | Défaut | Auparavant | Retrouver l'ancienne valeur avec |
334
+ | :--- | :--- | :--- | :--- |
335
+ | `quantile(probability)` | Hyndman–Fan type 7 | type 1 | `{ method: "type-1" }` |
336
+ | `interquartileRange()` | Hyndman–Fan type 7 | type 1 | `{ method: "type-1" }` |
337
+ | `skewness()` | `G1` corrigé du biais | `g1` | `{ estimator: "g1" }` |
338
+ | `kurtosis()` | `G2` d'excès corrigé du biais | `g2` | `{ estimator: "g2" }` |
339
+ | `variance()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
340
+ | `standardDeviation()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
341
+
342
+ Les neuf types de Hyndman–Fan sont disponibles sous `"type-1"` … `"type-9"`, et `{ excess: false }` rajoute trois au kurtosis pour rapporter le quatrième moment standardisé non excessif. Les types d'options — `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator`, `VarianceOptions` — sont exportés par `mathematics/quantile.ts`.
343
+
344
+ ```typescript
345
+ const sample = useOf(1, 2, 3, 4).toNumericStatistics();
346
+
347
+ sample.variance().get(); // 1.6667 (5/3, ddof = 1)
348
+ sample.variance({ ddof: 0 }).get(); // 1.25
349
+ sample.standardDeviation().get(); // 1.2910
350
+ sample.skewness().get(); // 0
351
+ sample.kurtosis().get(); // -1.2 (G2)
352
+ sample.kurtosis({ estimator: "g2" }).get(); // -1.36
353
+ sample.kurtosis({ excess: false }).get(); // 1.8
354
+
355
+ const quartiles = useOf(1, 2, 3, 4, 5, 6, 7, 8).toNumericStatistics();
356
+ quartiles.quantile(0.75).get(); // 6.25 (type 7)
357
+ quartiles.quantile(0.75, { method: "type-1" }).get(); // 6 (nearest rank)
358
+ quartiles.interquartileRange().get(); // 3.5
359
+ ```
360
+
361
+ `G1` et `G2` sont indéfinis en dessous de trois et quatre observations, et la variance et l'écart-type d'échantillon le sont en dessous de deux observations avec `ddof = 1`. Cela est rapporté par un `Optional` vide plutôt que par un zéro trompeur :
362
+
363
+ ```typescript
364
+ useOf(1, 2, 3).toNumericStatistics().kurtosis().isEmpty(); // true (G2 needs four observations)
365
+ useOf(1, 2).toNumericStatistics().skewness().isEmpty(); // true (G1 needs three)
366
+ useOf(1).toNumericStatistics().variance().isEmpty(); // true (ddof = 1 needs two)
367
+ useOf(1).toNumericStatistics().variance({ ddof: 0 }).get(); // 0
368
+ ```
369
+
370
+ Les vues bigint acceptent les mêmes options et arrondissent chaque résultat en un `bigint` entier.
371
+
372
+ ### 7.3 Fenêtres
373
+
374
+ Les fenêtres réexposent les éléments tamponnés sous forme de pipeline de pipelines, si bien qu'une fenêtre peut être consommée sans la matérialiser d'abord en tableau. Les deux formes incluent une dernière fenêtre partielle.
375
+
376
+ ```typescript
377
+ useFrom([1, 2, 3, 4]).toWindow().slide(2n, 1n).toUnordered().toArray()
378
+ .map(window => window.toUnordered().toArray()); // [[1,2],[2,3],[3,4],[4]]
379
+
380
+ useFrom([1, 2, 3, 4]).toWindow().tumble(2n).toUnordered().toArray()
381
+ .map(window => window.toUnordered().toArray()); // [[1,2],[3,4]]
382
+ ```
383
+
384
+ ---
385
+
386
+ <a id="8-the-index-model"></a>
387
+ ## 8. Le modèle d'index
388
+
389
+ Un index est un `bigint` attaché à chaque élément. Les opérateurs le **transmettent**, le **re-indexent**
390
+ à partir de zéro, ou vous laissent le **calculer**.
391
+
392
+ La collecte ordonnée normalise chaque index modulo le nombre d'éléments, puis trie sur le résultat. C'est
393
+ pourquoi `translate(7)` sur cinq éléments équivaut à `translate(2)` : `7 ≡ 2 (mod 5)`.
394
+
395
+ La collecte non ordonnée conserve l'index que le pipeline a produit et renvoie les éléments dans l'ordre
396
+ d'arrivée. Les index négatifs sont légaux et se trient simplement avant les non négatifs une fois normalisés.
397
+
398
+ ---
399
+
400
+ <a id="9-the-asynchronous-model"></a>
401
+ ## 9. Le modèle asynchrone
402
+
403
+ `AsynchronousSemantic` reflète la surface synchrone, en renvoyant des promesses. Les écritures sont mises en file
404
+ d'attente, si bien qu'une lecture émise après une écriture l'observe toujours.
405
+
406
+ ```typescript
407
+ const ring = new AsynchronousRing([1, 2]);
408
+ const added = ring.add(3); // queued
409
+ const sampled = ring.elementAt(2n); // observes the write above
410
+ await added;
411
+ (await sampled).get().element; // 3
412
+ ```
413
+
414
+ Les fabriques asynchrones par nature — `usePromise`, `useBlob`, `useInterval`, `useSubscription` — renvoient
415
+ directement un `AsynchronousSemantic`, et `useFrom` / `useIterate` se mettent à niveau lorsqu'on leur confie un
416
+ `AsyncIterable`.
417
+
418
+ ---
419
+
420
+ <a id="10-periodic-analysis-rings-fields-and-scores"></a>
421
+ ## 10. Analyse périodique : anneaux, champs et scores
422
+
423
+ La moitié mathématique de la bibliothèque traite un cycle comme un objet de première classe plutôt que comme une
424
+ table de correspondance. Trois constructions travaillent ensemble.
425
+
426
+ ### 10.1 Un anneau est un entier
427
+
428
+ Un `Ring` est un **anneau n-fois** : le nombre d'éléments, la période positionnelle et le nombre de phases
429
+ distinctes sont le même entier `n`. La phase d'une position est calculée à partir de son résidu exact, si bien que
430
+ l'anneau est périodique au bit près :
431
+
432
+ ```typescript
433
+ import { SynchronousRing } from "semantic-typescript";
434
+
435
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
436
+ ring.phaseAt(0n).get().toDegreeValue(); // 0
437
+ ring.phaseAt(1n).get().toDegreeValue(); // 60
438
+ ring.phaseAt(6n).get().getValue() === ring.phaseAt(0n).get().getValue(); // true, exactly
439
+ ring.purity(); // 1
440
+ ring.isBalanced(); // true
441
+ ring.sequences().map(z => z.magnitude()); // [0, 6, 0, 0, 0, 0]
442
+ ```
443
+
444
+ `frequency` est la vitesse de rotation **ambiante**, non un pas de phase entre positions : l'environnement
445
+ déplace chaque phaseur du même angle, si bien qu'il ne peut jamais briser le motif intrinsèque. `span` fixe le
446
+ nombre de tours qu'occupe l'anneau.
447
+
448
+ `purity()` est la part d'énergie portée par le mode fondamental,
449
+ `max(|c₁|², |c_{n−1}|²) / (n · Σ|z|²)`. Elle vaut `1` exactement lorsque l'anneau est constitué des racines
450
+ `n`-ièmes de l'unité à une rotation près, et elle coûte `O(n)` car seuls deux bins sont nécessaires. Les deux
451
+ sens sont testés, car un anneau peut parcourir le cercle dans un sens ou dans l'autre.
452
+
453
+ ### 10.2 Un champ est un score borné
454
+
455
+ Un `PhaseField` associe une phase relative à un score dans `[-1, 1]` :
456
+
457
+ ```typescript
458
+ import { useField, useRadian } from "semantic-typescript";
459
+
460
+ const cosine = useField([1]); // cos(2πΔ): +1 aligned, −1 opposed
461
+ const mixed = useField([0.4, 0.3, 0.3]); // first three harmonics
462
+
463
+ cosine.sample(useRadian(0)); // 1
464
+ cosine.sample(useRadian(Math.PI)); // -1
465
+ ```
466
+
467
+ Le score est une série de cosinus tronquée, `a₀ + Σ wₘ·cos(2πmΔ)`, et le constructeur impose
468
+ `|a₀| + Σ|wₘ| ≤ 1`. Par l'inégalité triangulaire, le score ne peut jamais sortir de `[-1, 1]` — la borne est une
469
+ **conséquence de la construction**, et non un écrêtage appliqué après coup.
470
+
471
+ La raison pour laquelle le champ est une série et non une table : une table à `n` points reproduit exactement ses
472
+ propres échantillons, mais son interpolant trigonométrique dépasse entre ceux-ci. Une table à cinq points notée
473
+ `[+1, +0.5, −1, −0.5, 0]` interpole sur `[−1.0428, 1.0041]`, ce qui brise déjà la promesse.
474
+ Une série bornée ne le peut pas.
475
+
476
+ ### 10.3 L'analyse mesure les données à chaque élément
477
+
478
+ Chaque élément de l'anneau est une phase de référence. `analyse` replace chaque observation sur l'anneau et la
479
+ mesure par rapport à chaque référence :
480
+
481
+ ```typescript
482
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e"]);
483
+ const report = ring.analyse(["a", "c", "c", "a", "zz", "e"]);
484
+
485
+ report.matched; // 5
486
+ report.missed; // 1 ('zz' is not on the ring)
487
+ report.references.map(r => r.count); // [2, 0, 2, 0, 1]
488
+ ```
489
+
490
+ Chaque ligne de référence rapporte `count`, la phase relative moyenne `mean` et la longueur résultante moyenne
491
+ `resultant`. Une identité mérite d'être retenue : **`resultant` est identique pour chaque référence**,
492
+ car changer la ligne de base ne fait que faire tourner toutes les phases relatives d'une constante. Ce qui diffère
493
+ d'une référence à l'autre, c'est le compte et la moyenne.
494
+
495
+ ```typescript
496
+ const ring = new SynchronousRing(["a", "b", "c"]);
497
+ ring.scoresAt(0, cosine); // one score per position, in [-1, 1]
498
+ ```
499
+
500
+ ### 10.4 Instruments associés
501
+
502
+ | Appel | Répond | Coût |
503
+ | :--- | :--- | :--- |
504
+ | `purity()` | l'anneau est-il toujours les racines `n`-ièmes de l'unité ? | O(n) |
505
+ | `resultant(t?)` | à quel point les phaseurs sont-ils concentrés maintenant ? | O(n) |
506
+ | `analyse(data)` | comment ces données se situent-elles par rapport à chaque élément ? | O(n · m) |
507
+ | `toSpectrum()` | l'anneau comme spectre, nombre d'onde sur l'axe des fréquences | O(n²) |
508
+ | `useAngularSpectrum(ring, instants, plan)` | le plan `(ω, n)` au fil du temps | O(n² · frames) |
509
+
510
+ Deux primitives supplémentaires complètent le modèle : `combineRings(a, b)` joint deux anneaux sur leur plus petit
511
+ commun multiple (un pullback, pas un produit — les paires qui ne peuvent pas partager un instant sont simplement
512
+ absentes), et `rate()` donne à chaque élément sa propre fréquence intrinsèque, afin qu'un anneau puisse dériver par
513
+ rapport à l'environnement. `jetAt`, `windingAt` et `errorIntegralAt` exposent l'état différentiel de cette dérive.
514
+
515
+ ### 10.5 Fenêtres spectrales
516
+
517
+ Les constructions d'anneaux sont une moitié de la surface analytique ; l'autre travaille sur des signaux échantillonnés. Sa primitive la plus réutilisée est la fenêtre, et chaque forme ici possède deux variantes. La variante symétrique divise la trame par `count − 1` ; la variante périodique (DFT paire) la divise par `count`, un échantillon de moins qu'une période complète, de sorte que le taper est continu sous l'extension périodique que suppose la DFT.
518
+
519
+ ```typescript
520
+ useHannWindow(1, 4); // 0.75 symmetric
521
+ useHannPeriodicWindow(1, 4); // 0.5 periodic
522
+ coherentGain(useHannPeriodicWindow, 16); // 0.5
523
+ equivalentNoiseBandwidth(useHannPeriodicWindow, 16); // 1.5
524
+ ```
525
+
526
+ | Forme | Symétrique | Périodique | Notes |
527
+ | :--- | :--- | :--- | :--- |
528
+ | Rectangulaire | `useRectangularWindow` | — | sans taper ; pas de distinction périodique |
529
+ | Hann | `useHannWindow` | `useHannPeriodicWindow` | le défaut de l'analyse à court terme |
530
+ | Hamming | `useHammingWindow` | `useHammingPeriodicWindow` | — |
531
+ | Blackman | `useBlackmanWindow` | `useBlackmanPeriodicWindow` | — |
532
+ | Nuttall | `useNuttallWindow` | `useNuttallPeriodicWindow` | Blackman–Harris à quatre termes |
533
+ | Flat-top | `useFlatTopWindow` | `useFlatTopPeriodicWindow` | la précision d'amplitude avant la résolution |
534
+ | Kaiser | `useKaiserWindow(beta)` | `useKaiserPeriodicWindow(beta)` | `beta` fixe la largeur du lobe principal |
535
+
536
+ Utilisez la variante périodique pour l'analyse et l'overlap-add : à un saut d'une demi-trame, la fenêtre de Hann périodique somme à `1.000000` quelle que soit la longueur, tandis que la variante symétrique ondule de `0.841 dB` à `N = 16` et encore de `0.013 dB` à `N = 1024`. Gardez la variante symétrique pour la conception de filtres, où la trame n'est pas une période du taper.
537
+
538
+ Les fenêtres flat-top renvoient des **poids négatifs** près des extrémités de la trame, parce qu'une fenêtre flat-top est exactement cela. Ils sont renvoyés tels quels plutôt qu'écrêtés, car écrêter perturbe le lobe principal et détruit la faible perte de scalloping qui justifie l'existence de la fenêtre :
539
+
540
+ ```typescript
541
+ useFlatTopWindow(0, 1024); // -0.000421 — negative by definition
542
+ Array.from({ length: 1024 }, (_unused, index) => useFlatTopWindow(index, 1024))
543
+ .filter(weight => weight < 0).length; // 578
544
+ ```
545
+
546
+ ### 10.6 Niveaux, platitude et énergie de bande
547
+
548
+ `decibel` a disparu en tant que fonction unique, parce qu'une amplitude et une puissance ne partagent pas le même facteur. `amplitudeDecibel` applique `20·log10` à un rapport de type tension ; `powerDecibel` applique `10·log10` à une puissance, une énergie, un carré moyen ou une densité spectrale de puissance :
549
+
550
+ ```typescript
551
+ amplitudeDecibel(0.25); // -12.0412
552
+ powerDecibel(0.25); // -6.0206
553
+ ```
554
+
555
+ `Bin.decibel` et `Spectrum.decibelOf` sont des niveaux d'amplitude ; `Spectrum.powerDecibelOf` est le niveau de puissance correspondant. Donner une puissance à la forme amplitude rapporte `20·log10(power)` et double le niveau en décibels.
556
+
557
+ Deux mesures de spectre ont changé de lecture :
558
+
559
+ ```typescript
560
+ const twoBins = new Spectrum(new Sample(2, 1), [new Complex(2, 0), new Complex(1, 0)]);
561
+ twoBins.flatness(); // 0.8 — geometric over arithmetic mean of power
562
+
563
+ const shape = new Spectrum(new Sample(4, 1), [
564
+ new Complex(1, 0), new Complex(2, 0), new Complex(0, 0), new Complex(0, 0),
565
+ ]);
566
+ shape.bandEnergy(new Frequency(0), new Frequency(2)); // 9
567
+ shape.bandEnergy(new Frequency(0), new Frequency(2), "two-sided"); // 5
568
+ ```
569
+
570
+ `flatness()` compare les moyennes géométrique et arithmétique de `power()` sur la moitié unilatérale du spectre, et non la moyenne des magnitudes sur les deux moitiés, et le doublement d'amplitude purement affichage de `oneSided()` n'est délibérément pas appliqué. `bandEnergy()` utilise par défaut la lecture unilatérale, en repliant la moitié conjuguée : DC et Nyquist comptent une fois et chaque bin intérieur compte deux fois ; une bande couvrant tout l'axe satisfait alors Parseval, `Σ|X|² = N·Σx²`. Passez `"two-sided"` pour un spectre complexe, dont les bins sont déjà indépendants.
571
+
572
+ ### 10.7 Analyse à court terme
573
+
574
+ `Sampler.shortTimeFourierTransform` et `Sampler.spectrogram` suivent les conventions `stft` de SciPy : fenêtre de Hann périodique, bordure remplie de zéros aux deux extrémités et queue remplie de zéros.
575
+
576
+ ```typescript
577
+ const sampler = new Sampler(new Sample(64, 1));
578
+ const spectrogram = sampler.spectrogram(time => Math.cos(2 * Math.PI * 4 * time), 16, 8);
579
+
580
+ spectrogram.size(); // 9 frames
581
+ spectrogram.getBoundary(); // "zeros"
582
+ spectrogram.getTime(6); // 0.75 — frame centres, not frame starts
583
+ sampler.spectrogram(time => Math.cos(2 * Math.PI * 4 * time), 16, 8, useHannWindow,
584
+ { boundary: "none", padded: false }).size(); // 7 — complete frames only
585
+ ```
586
+
587
+ L'extension centre la première trame sur le premier échantillon, et le remplissage de queue rend les derniers échantillons atteignables : 64 échantillons avec une trame de 16 et un saut de 8 donnent 9 trames au lieu de 7, et 1000 échantillons avec une trame de 256 et un saut de 128 donnent 9 trames au lieu de 6. Les temps de trame sont des centres — `(k·hop + (frameSize mod 2)/2)/rate` pour une vue étendue, `(k·hop + frameSize/2)/rate` pour une vue non étendue — et `toFrame()` rogne l'extension avant d'inverser.
588
+
589
+ ---
590
+
591
+ <a id="11-utility-helpers"></a>
592
+ ## 11. Fonctions utilitaires
593
+
594
+ ### 11.1 La hiérarchie d'erreurs
595
+
596
+ `structure/error.ts` exporte une hiérarchie de quinze classes pour les erreurs levées *autour* d'un pipeline — par votre propre code, un collecteur personnalisé ou un adaptateur :
597
+
598
+ ```text
599
+ SemanticError
600
+ ├── IllegalOperationError
601
+ ├── ValidationError
602
+ │ ├── IndexError
603
+ │ ├── RangeError (also exported as SemanticRangeError)
604
+ │ └── ConfigurationError
605
+ ├── CapacityError
606
+ ├── StateError
607
+ │ └── IllegalStateError
608
+ ├── ConcurrencyError
609
+ ├── CollectionError
610
+ ├── SerializationError
611
+ ├── NumericError
612
+ │ └── ArithmeticError
613
+ └── PhaseError
614
+ ```
615
+
616
+ Chaque classe fixe `name`, transmet la `cause` native, porte un sac `context` optionnel et se sérialise via `toJSON()` ; chacune possède une garde de marque du même nom.
617
+
618
+ ```typescript
619
+ const error = new CapacityError("LRU capacity exhausted", { context: { capacity: 0 } });
620
+
621
+ error.name; // "CapacityError"
622
+ isCapacityError(error); // true
623
+ isValidationError(error); // false — a capacity failure is not a validation failure
624
+ isSemanticError(error); // true
625
+ error.toJSON(); // { name: "CapacityError", message: "…", context: { capacity: 0 } }
626
+ ```
627
+
628
+ `SemanticRangeError` est la même classe que le `RangeError` exporté, donc `instanceof` tient pour les deux noms ; importer `RangeError` masque le constructeur global pour tout le module, et c'est pourquoi l'alias existe. Aucune des deux classes n'est levée par les vérifications d'arguments de la bibliothèque, qui continuent de lever les `TypeError` et `RangeError` natifs — la hiérarchie est le vocabulaire de la couche au-dessus.
629
+
630
+ ### 11.2 Table des utilitaires
631
+
632
+ | Assistant | Objectif |
633
+ | :--- | :--- |
634
+ | `useCompare(a, b)` | Ordre total entre primitives mixtes |
635
+ | `useToNumber` / `useToBigInt` | Conversion numérique sans perte et vérifiée en plage |
636
+ | `useRandom` | Assistants aléatoires avec ou sans graine |
637
+ | `useTraverse` | Parcours structurel d'objets et de tableaux imbriqués |
638
+ | `useStringify` | Sérialisation JSON sûre face aux cycles |
639
+ | `useHash` / `maskOf` / `register` / `unregister` | Hachage structurel et registre de stratégies de hachage |
640
+ | `invalidate` / `validate` | Gardes qui lèvent une forme d'erreur cohérente |
641
+ | `Optional<T>` | Le remplacement de `null` de la bibliothèque : `Optional.of(x)` / `Optional.empty()` |
642
+
643
+ ### 11.3 `useTraverse`
644
+
645
+ `useTraverse(target, callback)` aplatit n'importe quel objet en ses **feuilles non nulles**. Il descend dans les objets imbriqués, parcourt les valeurs itérables par position, ignore `null` et `undefined` et ne rentre jamais dans un objet déjà visité — un graphe d'objets comportant un cycle se termine donc au lieu de faire déborder la pile.
646
+
647
+ ```typescript
648
+ const source = { id: 7, tags: ["a", "b"], owner: { name: "Ada", note: null } };
649
+ const leaves: string[] = [];
650
+
651
+ useTraverse(source, (key, value, path) => {
652
+ leaves.push(`${path.join(".")} = ${String(value)}`);
653
+ return true;
654
+ });
655
+
656
+ leaves; // ["id = 7", "tags.0 = a", "tags.1 = b", "owner.name = Ada"]
657
+ ```
658
+
659
+ Le rappel s'exécute une fois par feuille et reçoit la clé propre de la feuille (son indice lorsqu'elle provient d'un itérable), la valeur, et le chemin qui y mène. Renvoyer `false` abandonne **tout** le parcours, pas seulement la branche courante :
660
+
661
+ ```typescript
662
+ let visited = 0;
663
+ useTraverse({ a: 1, b: 2, c: 3 }, () => {
664
+ visited += 1;
665
+ return false;
666
+ });
667
+ visited; // 1 — the first leaf already stops the walk
668
+ ```
669
+
670
+ Un cycle est sûr pour la même raison :
671
+
672
+ ```typescript
673
+ const cyclic: any = { name: "root", child: { name: "leaf" } };
674
+ cyclic.child.parent = cyclic;
675
+
676
+ const names: string[] = [];
677
+ useTraverse(cyclic, (_key, value) => {
678
+ if (typeof value === "string") names.push(value);
679
+ return true;
680
+ });
681
+ names; // ["root", "leaf"] — the back edge to `cyclic` is not revisited
682
+ ```
683
+
684
+ **Chaque rappel reçoit son propre instantané de chemin.** Le parcours ne garde en interne qu'un tableau de travail et le copie, au moment où une feuille est remise, dans un nouveau tableau gelé par `Object.freeze`. L'instantané ne change plus ensuite : il peut être conservé tel quel, deux rappels ne partagent jamais un tableau, et relancer le parcours ne perturbe pas les instantanés déjà remis. Son type est `ReadonlyArray`, y écrire est donc une erreur de compilation, et le tableau gelé lève en plus un `TypeError` à l'exécution. Cette sûreté coûte une allocation `O(depth)` par feuille rapportée ; une branche qui ne rapporte rien n'alloue rien de plus.
685
+
686
+ ```typescript
687
+ const paths: Array<ReadonlyArray<PropertyKey>> = [];
688
+ useTraverse({ a: { b: 1 }, c: 2 }, (_key, _value, path) => {
689
+ paths.push(path); // safe to keep as-is
690
+ return true;
691
+ });
692
+ paths[0] === paths[1]; // false — one array per call
693
+ paths.map(path => path.join(".")); // ["a.b", "c"]
694
+ ```
695
+
696
+ `useAttribute` est ce parcours transformé en fabrique : elle rapporte les mêmes feuilles sous forme de paires `{ key, value }`, donc les règles — et le coût — sont identiques.
697
+
698
+ ```typescript
699
+ useAttribute({ a: 1, b: { c: 2 } }).toUnordered().toArray();
700
+ // [{ key: "a", value: 1 }, { key: "c", value: 2 }]
701
+ ```
702
+
703
+ ---
704
+
705
+ <a id="12-performance-and-pitfalls"></a>
706
+ ## 12. Performance et pièges
707
+
708
+ **La paresse a un prix par étage.** Chaque opérateur ajoute un saut de générateur. Pour des boucles numériques
709
+ serrées sur des tableaux en mémoire, une simple boucle `for` battra n'importe quel pipeline ; le pipeline gagne
710
+ lorsque la source est paresseuse, infinie, ou que le pipeline est assez long pour que la discipline d'index paie.
711
+
712
+ **`toOrdered()` trie ; `toUnordered()` ne trie pas.** Si une inversion semble « ne pas avoir marché », le
713
+ collecteur était non ordonné — une réécriture d'index ne devient un ordre que lorsque les index sont normalisés.
714
+
715
+ **`useGenerate` sans `interrupt` ne se termine jamais.** Fournissez toujours un prédicat d'arrêt ou un
716
+ `limit` / `takeWhile` en aval.
717
+
718
+ **`Ring` est un ensemble à ordre d'insertion.** Les éléments sont uniques et conservent l'ordre dans lequel ils ont
719
+ été ajoutés pour la première fois. Un `add` en double renvoie `false`, et un `set` qui en introduirait un lève une
720
+ exception. C'est ce qui permet à une phase d'être une fonction de l'*élément* plutôt que de la position
721
+ qu'il occupe par hasard.
722
+
723
+ **`Spectrum` suppose un signal réel dans ses assistants unilatéraux.** `oneSided()`, `getNyquist()` et
724
+ `centroid()` supposent une symétrie conjuguée. Le spectre spatial d'un anneau de phase est complexe et en
725
+ général non symétrique — un anneau qui parcourt le cercle à rebours place toute son énergie dans le bin
726
+ `n − 1`, au-delà de Nyquist. Lisez les bins directement.
727
+
728
+ **Les collecteurs de structure sont des terminaux, pas des vues.** Chacun vide le pipeline puis construit une structure complète : il coûte donc O(n) en temps et en espace et ne peut pas tourner sur une source infinie. Utilisez-en un à la fin d'un pipeline, jamais à l'intérieur d'une boucle sur un pipeline.
729
+
730
+ **`variance()` et ses compagnons renvoient un `Optional`.** `variance().get()` sur un échantillon trop petit lève une exception ; utilisez `.orElse(0)` ou vérifiez `.isPresent()` quand la taille de l'échantillon ne dépend pas de vous. Il en va de même pour `standardDeviation`, `quantile`, `interquartileRange`, `skewness` et `kurtosis`.
731
+
732
+ **Les fenêtres périodiques sont les fenêtres d'analyse.** `useHannWindow` et les autres formes symétriques sont destinées à la conception de filtres ; une analyse à court terme veut `useHannPeriodicWindow` et ses sœurs, sinon la somme d'overlap-add ondule à chaque jonction de trames.
733
+
734
+ ---
735
+
736
+ <a id="13-phase-rings"></a>
737
+ ## 13. Anneaux de phase : un cycle exact
738
+
739
+ Le chapitre 10 a introduit l'anneau comme modèle du cycle et l'a utilisé pour mesurer un jeu de
740
+ données contre chaque phase. Ce chapitre en donne la surface complète : comment une position devient
741
+ un angle, ce que la fréquence ambiante change et ne change pas, et les instruments qui lisent
742
+ l'anneau contre le temps, contre les données et contre un autre anneau.
743
+
744
+ Un `SynchronousRing` est une liste positionnelle adossée à un anneau circulaire doublement chaîné
745
+ dont la tête et la queue sont jointes. C'est aussi un *ensemble* ordonné par insertion : aucun
746
+ élément n'apparaît deux fois, et une écriture refusée laisse intacts à la fois les liens et les
747
+ échantillons. Ce qui en fait un anneau plutôt qu'une liste, c'est que la phase d'une position est
748
+ une fonction pure de son reste modulo la longueur courante ; l'anneau est donc périodique au bit
749
+ près et son équilibre ne peut pas dériver.
750
+
751
+ ### 13.1 Un anneau est un entier, calculé exactement
752
+
753
+ Un anneau porte `n` éléments, et le nombre d'éléments, la période positionnelle et le nombre de
754
+ phases distinctes sont le même entier. La position `i` porte
755
+
756
+ `phase + span · 2π · (((i mod n) + n) mod n) / n`
757
+
758
+ le reste étant réduit en `bigint` avant de devenir un `number`. C'est toute la raison pour laquelle
759
+ `phaseAt(i)` et `phaseAt(i + n)` sont *identiques* et pas seulement proches : le second appel se
760
+ réduit au même entier, évalue le même produit et produit le même double. Un indice bien au-delà de
761
+ la plage entière exacte de `number` retombe sur la bonne position, et un indice négatif parcourt
762
+ l'anneau à l'envers.
763
+
764
+ | Appel | Retour | Signification |
765
+ | :--- | :--- | :--- |
766
+ | `new SynchronousRing(source, options?)` | anneau | `source` est un itérable quelconque, doublons écartés |
767
+ | `getPhase()` / `getFrequency()` / `getSpan()` | `Angle`, `Frequency`, `number` | les trois paramètres de construction |
768
+ | `size()` | `bigint` | le `n` qui est aussi la période |
769
+ | `phaseAt(index)` | `Optional<Angle>` | phase intrinsèque seule ; ignore temps, taux et fréquence |
770
+ | `elementAt(index)` | `Optional<RingElement>` | élément, phase intrinsèque, fréquence ambiante, phaseur |
771
+ | `frequencyAt(index)` | `Optional<Frequency>` | le taux ambiant, vide pour un anneau vide |
772
+ | `toComplex()` / `toComplex(index)` | `Complex[]` / `Optional<Complex>` | phaseurs intrinsèques |
773
+
774
+ `RingOptions` ajoute deux fonctions par élément : `magnitude(element, index)` donne la longueur du
775
+ phaseur et vaut un par défaut, `rate(element, index)` donne à l'élément sa propre fréquence
776
+ intrinsèque et vaut zéro par défaut, soit un marqueur fixe.
777
+
778
+ ```typescript
779
+ import { SynchronousRing } from "semantic-typescript";
780
+
781
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
782
+
783
+ ring.size(); // 6n
784
+ ring.phaseAt(0n).get().toDegreeValue(); // 0
785
+ ring.phaseAt(1n).get().toDegreeValue(); // 60
786
+ ring.phaseAt(-1n).get().toDegreeValue(); // 300
787
+ ring.phaseAt(6n).get().getValue() === ring.phaseAt(0n).get().getValue(); // true
788
+ ring.phaseAt(10n ** 20n).get().toDegreeValue(); // 240
789
+ ```
790
+
791
+ `phaseAt(-1n)` vaut `300` sur un anneau de six éléments, et `10n ** 20n` se réduit à `4` modulo six,
792
+ donc se lit `240`. Aucun des deux cas n'accumule de produit flottant, ce qui garde l'identité exacte
793
+ quand la représentation de l'indice change.
794
+
795
+ ### 13.2 La fréquence ambiante et l'état à un instant
796
+
797
+ `frequency` est la **vitesse de rotation ambiante** de l'environnement dans lequel l'anneau est
798
+ plongé, pas un pas de phase entre positions. L'environnement avance la phase de chaque élément du
799
+ même angle : il ne peut donc pas changer le motif intrinsèque, ni briser l'équilibre, ni modifier
800
+ un résultat de `phaseAt`. La valeur par défaut est `0 Hz`.
801
+
802
+ `relativeAt(index, time)` soustrait cet angle commun et laisse la coordonnée contre laquelle un
803
+ `PhaseField` est évalué. `viewAt` et `viewsAt` renvoient l'état complet à un instant, y compris le
804
+ `rate` propre de l'élément, et les accesseurs différentiels exposent la dérive :
805
+
806
+ | Appel | Formule | Lit |
807
+ | :--- | :--- | :--- |
808
+ | `relativeAt(index, time)` | `intrinsèque + tauxPropre · t − ambiant · t` | la phase que voit l'environnement |
809
+ | `viewAt(index, time)` / `viewsAt(time)` | idem, plus élément, indice, temps, taux, phaseur | l'état complet |
810
+ | `jetAt(index, time)` | phase à `t`, taux propre, `slip = tauxPropre − ambiant` | l'état différentiel |
811
+ | `windingAt(index, time)` | `trunc(slip · t)` | tours entiers glissés, tronqués vers zéro |
812
+ | `errorIntegralAt(index, time, origin?)` | `∫ phase relative dt`, en tours · secondes | depuis combien de temps un élément avance |
813
+ | `resultant(time?)` | `abs(Σ w · e^{iθ}) / Σ w` | concentration de phase, insensible à la rotation ambiante |
814
+ | `analyse(data, time?)` | comptes et moyennes par référence | les données mesurées contre chaque élément |
815
+ | `scoreAt(index, time, field)` / `scoresAt(time, field)` | le champ échantillonné à la phase relative | un score borné par position |
816
+
817
+ ```typescript
818
+ import { SynchronousRing, useHertz } from "semantic-typescript";
819
+
820
+ const moving = new SynchronousRing(["a", "b", "c", "d", "e", "f"], {
821
+ frequency: useHertz(1),
822
+ });
823
+
824
+ moving.getFrequency().getHertz(); // 1
825
+ moving.phaseAt(1n).get().toDegreeValue(); // 60 — intrinsic
826
+ moving.relativeAt(1n, 0.25).get().toDegreeValue(); // 330 — 60 − 90
827
+ moving.viewAt(1n, 0.25).get().relative.toDegreeValue(); // 330
828
+ moving.jetAt(1n, 0.25).get().slip; // -1
829
+ moving.purity(); // 1 — rotation cannot break it
830
+ ```
831
+
832
+ À `1 Hz`, l'environnement avance d'un quart de tour en `0.25 s` : la phase relative de la position
833
+ un vaut donc `60 − 90 = 330` degrés, tandis que sa phase intrinsèque reste `60`. Le jet rapporte un
834
+ `slip` de `-1` hertz parce que l'élément lui-même ne bouge pas, et `purity()` vaut toujours `1` :
835
+ faire tourner tous les phaseurs du même angle n'est pas une déformation.
836
+
837
+ ### 13.3 Pureté et équilibre
838
+
839
+ `purity()` est la part de l'énergie de l'anneau portée par le mode fondamental :
840
+
841
+ `max(abs(c₁)², abs(c_{n−1})²) / (n · Σ abs(z)²)`
842
+
843
+ Les deux bins fondamentaux sont testés parce qu'un anneau peut parcourir le cercle dans les deux
844
+ sens. Un anneau construit avec `span: 5` sur six éléments avance de `300` degrés, soit `−60`
845
+ degrés : il tourne à l'envers, place toute son énergie dans `c₅` et serait déclaré impur par une
846
+ formule qui ne regarderait que `c₁`. Le coût reste `O(n)` puisque seuls ces deux bins et l'énergie
847
+ totale sont nécessaires.
848
+
849
+ Les cas limites font partie du contrat. Un anneau de moins de deux éléments n'a pas de mode
850
+ fondamental et rapporte `purity() === 0` et `isBalanced() === false`, quel que soit son contenu. Un
851
+ anneau dont tous les phaseurs ont une longueur nulle n'a pas d'énergie et rapporte aussi zéro.
852
+ `isBalanced(epsilon?)` vaut `1 − purity() <= epsilon`, avec `1E-9` comme tolérance par défaut.
853
+
854
+ ```typescript
855
+ import { SynchronousRing } from "semantic-typescript";
856
+
857
+ const forward = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
858
+ const backward = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 5 });
859
+ const weighted = new SynchronousRing(["a", "b", "c", "d"], {
860
+ magnitude: element => element === "a" ? 1 : 0,
861
+ });
862
+
863
+ forward.purity(); // 1
864
+ backward.phaseAt(1n).get().toDegreeValue(); // 300 — it runs backwards
865
+ backward.purity(); // 1 — the energy sits in c₅
866
+ new SynchronousRing(["only"]).purity(); // 0
867
+ new SynchronousRing(["only"]).isBalanced(); // false
868
+ weighted.purity(); // 0.25
869
+ ```
870
+
871
+ ### 13.4 Longueur résultante et composantes symétriques
872
+
873
+ `resultant(time?)` est la longueur résultante moyenne : le module de la somme des phaseurs divisé
874
+ par la somme des modules. Contrairement à `purity`, qui demande si l'anneau est encore les racines
875
+ `n`-ièmes de l'unité, `resultant` demande à quel point les phaseurs sont concentrés maintenant. Un
876
+ anneau équilibré rapporte `0` ; un anneau dont tous les éléments partagent un angle rapporterait
877
+ `1`, mais la sémantique d'ensemble interdit les doublons, si bien que `1` apparaît en pratique
878
+ lorsque `magnitude` annule tous les éléments sauf un.
879
+
880
+ `zeroSequence()` est le phaseur moyen, c'est-à-dire `c₀ / n`. `sequences()` renvoie les `n`
881
+ composantes symétriques, ordre croissant, par une transformée directe. `toSpectrum()` les enveloppe
882
+ dans un `Spectrum` dont le bin `j` rapporte `j` cycles par tour d'anneau : l'axe des fréquences est
883
+ donc l'axe des nombres d'onde.
884
+
885
+ ```typescript
886
+ import { SynchronousRing } from "semantic-typescript";
887
+
888
+ const round = (value: number): number => Number(value.toFixed(4));
889
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
890
+
891
+ round(ring.resultant().get()); // 0
892
+ round(ring.zeroSequence().get().magnitude()); // 0
893
+ ring.sequences().map(component => round(component.magnitude())); // [0, 6, 0, 0, 0, 0]
894
+ ring.toSpectrum().get().size(); // 6
895
+ ring.toSpectrum().get().getResolution().getHertz(); // 1
896
+ ```
897
+
898
+ Les composantes sont complexes et en général **pas conjuguées-symétriques** : un anneau équilibré
899
+ met toute son énergie dans le bin un, un anneau qui tourne à l'envers dans le bin `n − 1`. Les
900
+ helpers unilatéraux de `Spectrum` supposent un signal réel ; lisez les bins directement au lieu de
901
+ les replier.
902
+
903
+ ### 13.5 Analyser des données contre chaque élément
904
+
905
+ `analyse(data, time?)` rattache chaque observation à l'anneau par identité et la mesure contre
906
+ chaque élément comme phase de référence. Les observations que l'anneau ne détient pas sont comptées
907
+ comme `missed` plutôt que devinées, car l'anneau est la seule source de phases. Chaque ligne de
908
+ référence rapporte `count`, la phase relative moyenne `mean` et la longueur résultante moyenne
909
+ `resultant`.
910
+
911
+ Une identité vaut la peine d'être retenue : **`resultant` est le même pour chaque référence**.
912
+ Changer la base ne fait que faire tourner toutes les phases relatives d'une constante, et une
913
+ rotation ne peut pas changer une longueur résultante. Ce qui diffère par référence, c'est le compte
914
+ et la moyenne.
915
+
916
+ ```typescript
917
+ import { SynchronousRing } from "semantic-typescript";
918
+
919
+ const round = (value: number): number => Number(value.toFixed(4));
920
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e"]);
921
+ const report = ring.analyse(["a", "c", "c", "a", "zz", "e"]);
922
+
923
+ report.matched; // 5
924
+ report.missed; // 1
925
+ report.references.map(row => row.count); // [2, 0, 2, 0, 1]
926
+ report.references.map(row => round(row.resultant)); // [0.1453, 0.1453, 0.1453, 0.1453, 0.1453]
927
+ ```
928
+
929
+ L'évaluation est l'autre face de la même pièce. Un `PhaseField` est une série de cosinus bornée
930
+ (chapitre 10.2), et `scoreAt`/`scoresAt` l'échantillonnent à la phase relative : le même anneau peut
931
+ donc être évalué contre un alignement, contre un motif harmonique ou contre n'importe quel champ
932
+ sans que l'anneau change.
933
+
934
+ ```typescript
935
+ import { SynchronousRing, useField } from "semantic-typescript";
936
+
937
+ const round = (value: number): number => Number(value.toFixed(4));
938
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
939
+ const cosine = useField([1]);
940
+
941
+ round(ring.scoreAt(0n, 0, cosine).get()); // 1
942
+ round(ring.scoreAt(1n, 0, cosine).get()); // 0.5
943
+ ring.scoresAt(0, cosine).map(score => round(score)); // [1, 0.5, -0.5, -1, -0.5, 0.5]
944
+ ```
945
+
946
+ ### 13.6 Combiner des anneaux et le spectre angulaire
947
+
948
+ `combineRings(left, right)` joint deux anneaux sur leur plus petit commun multiple, pas sur leur
949
+ produit. L'instant `i` de l'anneau joint lit `i mod m` de l'anneau gauche et `i mod n` de l'anneau
950
+ droit ; l'anneau joint se referme donc après `lcm(m, n)` instants. Les paires qui ne peuvent jamais
951
+ coexister — celles dont les positions diffèrent modulo `gcd(m, n)` — sont simplement absentes :
952
+ c'est pourquoi le résultat est un pullback et non un produit, et `aligns(left, right)` indique si
953
+ une paire donnée est représentable.
954
+
955
+ `useAngularSpectrum(ring, instants, plan)` construit le plan `(ω, n)` : chaque instant apporte une
956
+ trame de phaseurs, la trame `k` du mode `j` est la `j`-ième composante symétrique de cette trame, et
957
+ `cells[frame][order]` est le coefficient. Prendre une colonne donne la série temporelle d'un seul
958
+ mode spatial, et `spectrum(order)` en est le spectre temporel.
959
+
960
+ ```typescript
961
+ import { Sample, SynchronousRing, combineRings, useAngularSpectrum } from "semantic-typescript";
962
+
963
+ const left = new SynchronousRing(["a", "b", "c", "d"]);
964
+ const right = new SynchronousRing(["x", "y", "z", "w", "v", "u"]);
965
+ const joined = combineRings(left, right);
966
+
967
+ joined.period; // 12n
968
+ joined.divisor; // 2n
969
+ joined.ring.size(); // 12n
970
+ joined.aligns(0n, 0n); // true
971
+ joined.aligns(1n, 0n); // false
972
+ joined.ring.get(0n).join("-"); // a-x
973
+
974
+ const angular = useAngularSpectrum(left, [0, 0.25, 0.5, 0.75], new Sample(4, 1));
975
+ angular.order; // 4
976
+ angular.frameCount; // 4
977
+ angular.cells[0][1].magnitude(); // 4
978
+ angular.spectrum(1).get().size(); // 4
979
+ ```
980
+
981
+ ### 13.7 Pièges
982
+
983
+ **L'exactitude est exacte.** `phaseAt(i)` et `phaseAt(i + n)` sont le même double, pas deux valeurs
984
+ à une tolérance près. Un anneau construit en accumulant `2π / n` par élément échouerait à cela ;
985
+ celui-ci réduit d'abord l'indice en `bigint`.
986
+
987
+ **`frequency` n'est pas un pas de phase.** C'est la vitesse de rotation ambiante, nulle par défaut,
988
+ et faire tourner tous les phaseurs du même angle ne peut pas briser le motif intrinsèque. Pour faire
989
+ avancer les positions les unes par rapport aux autres, utilisez `rate`, pas `frequency`.
990
+
991
+ **`purity()` doit regarder les deux bins fondamentaux.** Un anneau qui tourne à l'envers reste les
992
+ racines `n`-ièmes de l'unité ; `c₁` seul le déclarerait impur.
993
+
994
+ **`n < 2` est un cas limite défini.** `purity()` renvoie `0` et `isBalanced()` `false` ; un anneau
995
+ vide renvoie des optionnels vides depuis les méthodes d'échantillonnage.
996
+
997
+ **Un span qui partage un facteur avec `n` effondre l'orbite.** Six éléments avec `span: 2` ne
998
+ visitent que trois phases distinctes, et l'anneau n'est plus équilibré.
999
+
1000
+ **`resultant` est invariant par rotation, pas par le temps.** La rotation ambiante le laisse
1001
+ inchangé ; les `rate` par élément le changent à mesure que l'anneau se déphase.
1002
+
1003
+ **L'anneau est un ensemble, et `analyse` apparie par identité.** Les doublons sont écartés à la
1004
+ construction, et un objet structurellement égal à un élément de l'anneau mais qui n'est pas la même
1005
+ référence est compté comme manqué.
1006
+
1007
+ **`toSpectrum()` est complexe.** Ses bins ne sont pas conjugués-symétriques ; les helpers
1008
+ unilatéraux de `Spectrum` ne s'appliquent pas. Lisez les bins.
1009
+
1010
+ **`windingAt` tronque vers zéro, et `errorIntegralAt` intègre la phase.** Un demi-tour de glissement
1011
+ rapporte `0n` ; l'intégrale d'erreur est en tours · secondes, pas un taux.
1012
+
1013
+ ```typescript
1014
+ import { SynchronousRing, useHertz } from "semantic-typescript";
1015
+
1016
+ const collapsed = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 2 });
1017
+ collapsed.phaseAt(1n).get().toDegreeValue(); // 120
1018
+ collapsed.phaseAt(3n).get().toDegreeValue(); // 0
1019
+ Number(collapsed.purity().toFixed(4)); // 0
1020
+
1021
+ const falling = new SynchronousRing(["a", "b", "c", "d"], {
1022
+ frequency: useHertz(2),
1023
+ rate: () => useHertz(0.5),
1024
+ });
1025
+ falling.jetAt(0n, 2).get().slip; // -1.5
1026
+ falling.windingAt(0n, 1).get(); // -1n
1027
+ falling.windingAt(0n, 0.5).get(); // 0n
1028
+
1029
+ const objects = new SynchronousRing([{ key: 1 }, { key: 2 }]);
1030
+ objects.analyse([{ key: 1 }]).missed; // 1 — identity, not structure
1031
+ ```
1032
+
1033
+ ---
1034
+
1035
+ <a id="14-signal-analysis"></a>
1036
+ ## 14. Analyse du signal : fenêtres, spectres et temps
1037
+
1038
+ Le chapitre 10 traitait un anneau comme un cycle de phases. Ce chapitre en est le pendant temporel :
1039
+ un enregistrement réel qui porte, outre ses échantillons, les deux contextes dont toute formule
1040
+ spectrale a besoin — le plan d'échantillonnage qui transforme un indice de bin en hertz, et la
1041
+ fenêtre qui fixe à la fois le gain cohérent et la bande de bruit équivalente.
1042
+
1043
+ L'objet-valeur est `SynchronousSignalCollectable`, son jumeau mis en file est
1044
+ `AsynchronousSignalCollectable`. Tous deux exposent la même surface `SignalCollectable` :
1045
+ vingt-trois méthodes, chacune déléguant à exactement une fabrique `useSynchronous<Method>` dans
1046
+ `synchronous/collectors`. La séparation est délibérée. La classe est une façade documentée, la
1047
+ fabrique possède l'accumulation, et le constructeur calcule une fois, à cette longueur,
1048
+ `sum(w) / count` et `count * sum(w^2) / sum(w)^2`. Un appelant qui doit se souvenir de « diviser
1049
+ par le gain cohérent » finira par l'oublier, et les 6,02 décibels qui en résultent ressemblent à
1050
+ une mesure plausible.
1051
+
1052
+ ### 14.1 Un signal porte sa propre calibration
1053
+
1054
+ Un signal se construit à partir d'un itérable et d'un taux, d'un `Frame` existant, ou du
1055
+ constructeur avec un `Sample` explicite. La fenêtre par défaut est la fenêtre de Hann
1056
+ périodique ; toute analyse qui n'en nomme pas une l'utilise, et toute analyse accepte un
1057
+ remplacement. Les accesseurs de métadonnées lisent en temps constant un état figé à la
1058
+ construction, de sorte qu'un signal peut circuler sans que le contexte dérive.
1059
+
1060
+ | Appel | Retour | Signification |
1061
+ | :--- | :--- | :--- |
1062
+ | `SynchronousSignalCollectable.useRate(values, rate, window?)` | signal | durée `count / rate` |
1063
+ | `SynchronousSignalCollectable.useFrame(frame, window?)` | signal | conserve le plan du frame |
1064
+ | `new SynchronousSignalCollectable(plan, values, window?)` | signal | le nombre de trames doit égaler le nombre d'échantillons |
1065
+ | `getSample()` / `getValues()` / `getWindow()` | plan, échantillons, fenêtre | le contexte figé |
1066
+ | `getRate()` | `number` | échantillons par seconde |
1067
+ | `getNyquist()` / `getResolution()` | `Frequency` | `rate / 2` et `rate / count` |
1068
+ | `getCoherentGain()` / `getNoiseBandwidth()` | `number` | recalculés à cette longueur exacte |
1069
+ | `size()` / `at(index)` / `toFrame()` / `equals(other)` / `toString()` | — | sémantique de valeur |
1070
+
1071
+ ```typescript
1072
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1073
+
1074
+ const signal = SynchronousSignalCollectable.useRate(
1075
+ Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 64)),
1076
+ 64,
1077
+ );
1078
+
1079
+ signal.size(); // 64n
1080
+ signal.getRate(); // 64
1081
+ signal.getNyquist().getHertz(); // 32
1082
+ signal.getResolution().getHertz(); // 1
1083
+ signal.getCoherentGain(); // 0.5
1084
+ signal.getNoiseBandwidth(); // 1.5
1085
+ ```
1086
+
1087
+ `getResolution()` est l'espacement des bins, non le pouvoir de résolution : il vaut `rate / count`
1088
+ parce que la transformée a `count` bins, et le remplissage par des zéros change l'espacement sans
1089
+ changer ce que la fenêtre peut séparer (section 14.8).
1090
+
1091
+ ### 14.2 Les vingt-trois méthodes
1092
+
1093
+ Les méthodes forment six familles. Sept sont des lectures spectrales, deux des transformées
1094
+ temps–fréquence, quatre des grandeurs analytiques, trois des statistiques de séquence, cinq des
1095
+ descripteurs de forme, et deux renvoient un autre signal.
1096
+
1097
+ | Méthode | Retour | Convention |
1098
+ | :--- | :--- | :--- |
1099
+ | `spectrum(size?, window?)` | `Spectrum` | brut et non normalisé, grille complexe complète |
1100
+ | `oneSided(size?, window?)` | `Bin[]` | moitié conjuguée repliée, DC et Nyquist non doublés |
1101
+ | `amplitude(window?)` | `number[]` | `2·abs(X) / (count · CG)`, DC et Nyquist une seule fois |
1102
+ | `power(window?)` | `number[]` | `A² / 2` par bin intérieur, `A²` en DC et Nyquist |
1103
+ | `decibel(window?)` | `number[]` | `20·log10(amplitude)` |
1104
+ | `powerDecibel(window?)` | `number[]` | `10·log10(puissance)` |
1105
+ | `powerSpectralDensity(frameSize?, hopSize?, window?)` | `SpectralDensity` | moyenne de Welch, `abs(X)² / (rate · Σw²)` |
1106
+ | `shortTimeFourierTransform(frameSize, hopSize, window?)` | `Spectrum[]` | bords étendus, temps au centre des trames |
1107
+ | `spectrogram(frameSize, hopSize, window?)` | `Spectrogram` | les mêmes colonnes avec un axe de temps |
1108
+ | `analyticSignal()` | `Complex[]` | paire de Hilbert `x + i·H(x)` |
1109
+ | `instantaneousPhase()` | `Angle[]` | repliée dans `[0, 2π)` |
1110
+ | `instantaneousFrequency(rate?, gate?)` | `number[]` | différence de phase déroulée, filtrée par l'enveloppe |
1111
+ | `envelope()` | `number[]` | `abs(z[n])`, circulaire aux bords |
1112
+ | `zeroCrossingRate(perSecond?)` | `number` | passages par zéro par paire d'échantillons |
1113
+ | `spectralFlux(frameSize?, hopSize?, window?)` | `number[]` | distance de norme unitaire entre trames |
1114
+ | `autocorrelation(maximumLag?, normalization?)` | `number[]` | coefficient par défaut, `ρ[0] = 1` |
1115
+ | `spectralCentroid(window?)` | `Optional<Frequency>` | fréquence moyenne pondérée par la puissance |
1116
+ | `spectralBandwidth(window?)` | `Optional<Frequency>` | écart-type pondéré par la puissance |
1117
+ | `spectralRolloff(ratio?, window?)` | `Optional<Frequency>` | premier bin atteignant `ratio` de la puissance |
1118
+ | `spectralFlatness(window?)` | `number` | moyenne géométrique sur arithmétique de la puissance |
1119
+ | `signalToNoiseRatio(frameSize?, hopSize?, window?, floorQuantile?)` | `Optional<number>` | plancher médian, `10·log10` |
1120
+ | `detrended(mode?)` | signal | `"none"`, `"constant"` (défaut), `"linear"` |
1121
+ | `windowed(window?)` | signal | applique une fenêtre et l'adopte |
1122
+
1123
+ Les arguments optionnels ne sont pas décoratifs. `frameSize` vaut `min(256, size)` par défaut, le
1124
+ saut une demi-trame arrondie vers le bas, `instantaneousFrequency` filtre à `1E-3` de l'enveloppe
1125
+ de crête, et `spectralRolloff` utilise `0.85`. Chaque défaut est la valeur qu'une implémentation de
1126
+ référence choisit lorsque le paramètre est nommé sans être fourni.
1127
+
1128
+ ### 14.3 Amplitude, puissance et densité
1129
+
1130
+ Trois normalisations sont nécessaires parce que trois questions différentes sont posées.
1131
+
1132
+ `amplitude()` répond « quelle est la taille du sinus sur ce bin » :
1133
+ `2·abs(X[k]) / (count · CG)` pour un bin intérieur et `abs(X[k]) / (count · CG)` en DC et à
1134
+ Nyquist. Le facteur deux replie la moitié conjuguée ; le gain cohérent retire la fenêtre. Omettre
1135
+ l'un ou l'autre fait lire `0.5` à un sinus d'amplitude unité, soit moins 6,02 décibels.
1136
+
1137
+ `power()` répond « quel carré moyen aurait un sinus sur ce bin » : `A² / 2` pour un bin intérieur
1138
+ et `A²` en DC et à Nyquist, car ces deux-là n'oscillent pas. C'est la convention de crête, et elle
1139
+ ne satisfait délibérément pas Parseval : la fenêtre étale chaque raie sur son lobe principal, donc
1140
+ la somme de tous les bins dépasse le carré moyen de la bande de bruit équivalente — `1.5` pour
1141
+ Hann.
1142
+
1143
+ `powerSpectralDensity()` répond « quelle puissance par hertz » : `abs(X)² / (rate · Σw²)`, moyenné
1144
+ sur des segments recouvrants. Seule la somme des poids au carré rend un plancher de bruit blanc
1145
+ non biaisé ; diviser par `count` lit `0.375` pour un bruit unité, diviser par `count · CG²` lit
1146
+ `1.5`.
1147
+
1148
+ ```typescript
1149
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1150
+
1151
+ const round = (value: number): number => Number(value.toFixed(4));
1152
+ const tone = SynchronousSignalCollectable.useRate(
1153
+ Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1154
+ 256,
1155
+ );
1156
+
1157
+ round(tone.amplitude()[32]); // 1
1158
+ round(tone.power()[32]); // 0.5
1159
+ round(tone.power().reduce((sum, power) => sum + power, 0)); // 0.75
1160
+ round(tone.powerSpectralDensity(64n, 32n).totalPower()); // 0.5
1161
+ tone.powerSpectralDensity(64n, 32n).getSegments(); // 7n
1162
+ ```
1163
+
1164
+ Le couple de totaux est le contrôle à retenir : la somme en convention de crête vaut `0.75` tandis
1165
+ que la densité s'intègre à `0.5`, et le rapport est exactement la bande de bruit. La densité est
1166
+ aussi la seule des trois qui survive à un changement de longueur de trame.
1167
+
1168
+ ### 14.4 Niveaux : les deux conventions de décibel
1169
+
1170
+ Une amplitude et une puissance ne partagent pas de facteur, donc pas de logarithme.
1171
+ `amplitudeDecibel` applique `20·log10` et convient à une tension, un déplacement ou une pression
1172
+ acoustique ; `powerDecibel` applique `10·log10` et convient à une puissance, une énergie, un carré
1173
+ moyen ou une densité. Les noms de méthodes du signal suivent la même séparation.
1174
+
1175
+ ```typescript
1176
+ import { amplitudeDecibel, powerDecibel, SynchronousSignalCollectable } from "semantic-typescript";
1177
+
1178
+ const round = (value: number): number => Number(value.toFixed(4));
1179
+ const tone = SynchronousSignalCollectable.useRate(
1180
+ Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1181
+ 256,
1182
+ );
1183
+
1184
+ amplitudeDecibel(0.25); // -12.0412
1185
+ powerDecibel(0.25); // -6.0206
1186
+ round(tone.decibel()[32]); // 0
1187
+ round(tone.powerDecibel()[32]); // -3.0103
1188
+ ```
1189
+
1190
+ Les deux lectures du même son sont correctes : son amplitude vaut un (zéro décibel) et son carré
1191
+ moyen vaut un demi (moins 3,01 décibels). Faire passer une puissance par la convention d'amplitude
1192
+ double le niveau en décibels, transformant le moins 6,02 correct en moins 12,04.
1193
+
1194
+ ### 14.5 Le repli unilatéral
1195
+
1196
+ Un signal réel vérifie `X[count − k] = conj(X[k])`, la moitié des fréquences négatives ne porte
1197
+ donc aucune information indépendante. Le repli double chaque bin intérieur et laisse les deux bins
1198
+ auto-conjugués — DC et, pour une longueur paire, Nyquist — exactement tels que la transformée les a
1199
+ produits. Ces deux-là sont leur propre miroir ; les doubler invente de l'énergie et rapporte
1200
+ exactement 6,02 décibels de trop.
1201
+
1202
+ ```typescript
1203
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1204
+
1205
+ const round = (value: number): number => Number(value.toFixed(4));
1206
+ const edge = SynchronousSignalCollectable.useRate(
1207
+ Array.from({ length: 16 }, (_unused, index) => 2 + Math.cos(Math.PI * index)),
1208
+ 16,
1209
+ );
1210
+ const bins = edge.oneSided();
1211
+
1212
+ bins.length; // 9
1213
+ round(bins[0].magnitude()); // 16
1214
+ round(bins[8].magnitude()); // 8
1215
+ round(edge.amplitude()[0]); // 2
1216
+ round(edge.amplitude()[8]); // 1
1217
+ ```
1218
+
1219
+ Les magnitudes brutes valent `16` et `8` parce que les poids de la fenêtre somment à huit ; diviser
1220
+ par `count · CG = 8` en fait les amplitudes calibrées `2` et `1`. Doubler les bins de bord lirait
1221
+ `4` et `2`, et l'erreur ne lèverait jamais d'exception.
1222
+
1223
+ ### 14.6 Vues temps–fréquence
1224
+
1225
+ `shortTimeFourierTransform` et `spectrogram` découpent l'enregistrement comme la transformée à
1226
+ court terme de référence : une demi-trame de zéros à chaque extrémité, puis une queue complétée par
1227
+ des zéros afin qu'il reste un nombre entier de sauts. La première trame est centrée sur le premier
1228
+ échantillon, la dernière atteint le dernier échantillon, et la trame `m` est centrée à
1229
+ `m · hop / rate` secondes. Ne garder que les trames complètes jetterait jusqu'à `frameSize − 1`
1230
+ échantillons — pour 1000 échantillons, une trame de 256 et un saut de 128, cela fait 104
1231
+ échantillons, environ un dixième de l'enregistrement.
1232
+
1233
+ ```typescript
1234
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1235
+
1236
+ const tone = SynchronousSignalCollectable.useRate(
1237
+ Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 4 * index / 64)),
1238
+ 64,
1239
+ );
1240
+ const spectrogram = tone.spectrogram(16n, 8n);
1241
+
1242
+ spectrogram.size(); // 9
1243
+ spectrogram.getFrameSize(); // 16
1244
+ spectrogram.getBoundary(); // "zeros"
1245
+ spectrogram.getTime(0); // 0
1246
+ spectrogram.getTime(6); // 0.75
1247
+ tone.shortTimeFourierTransform(16n, 8n).length; // 9
1248
+ tone.spectralFlux(16n, 8n).length; // 8
1249
+ ```
1250
+
1251
+ Chaque colonne est un `Spectrum` de `frameSize` bins, exactement ce que `spectrum()` produirait
1252
+ pour la même trame, et `getTime()` rapporte des centres de trame, non des débuts.
1253
+
1254
+ ### 14.7 Fenêtres, gain cohérent et COLA
1255
+
1256
+ Chaque forme a deux variantes. La variante symétrique divise par `count − 1` ; la variante
1257
+ périodique (DFT paire) divise par `count`, un échantillon de moins qu'une période complète, de
1258
+ sorte que la fenêtre est continue sous l'extension périodique que la DFT suppose. L'analyse et
1259
+ l'addition-recouvrement exigent la forme périodique ; la conception de filtres garde la symétrique.
1260
+
1261
+ La différence est mesurable et ne lève jamais d'erreur. À cinquante pour cent de recouvrement, la
1262
+ fenêtre de Hann périodique somme à un en toute position ; la forme symétrique ondule, de `0.84`
1263
+ décibel à seize échantillons, `0.21` à soixante-quatre et encore `0.013` à mille vingt-quatre. La
1264
+ classe expose le gain cohérent et la bande de bruit équivalente de la fenêtre qu'elle détient, si
1265
+ bien que la correction n'est jamais codée en dur : un Hann symétrique lit `0.4375` à huit
1266
+ échantillons, pas `0.5`.
1267
+
1268
+ ```typescript
1269
+ import {
1270
+ coherentGain, equivalentNoiseBandwidth, useFlatTopWindow,
1271
+ useHannPeriodicWindow, useHannWindow,
1272
+ } from "semantic-typescript";
1273
+
1274
+ const round = (value: number): number => Number(value.toFixed(4));
1275
+
1276
+ round(useHannWindow(1, 4)); // 0.75
1277
+ round(useHannPeriodicWindow(1, 4)); // 0.5
1278
+ coherentGain(useHannPeriodicWindow, 16); // 0.5
1279
+ equivalentNoiseBandwidth(useHannPeriodicWindow, 16); // 1.5
1280
+ useFlatTopWindow(0, 1024); // -0.000421
1281
+ Array.from({ length: 1024 }, (_unused, i) => useFlatTopWindow(i, 1024)).filter(w => w < 0).length; // 578
1282
+ ```
1283
+
1284
+ La ligne du flat-top n'est pas une erreur du tableau. Une fenêtre flat-top est partiellement
1285
+ négative par définition — ici 578 poids sur 1024 — et ramener ces poids à zéro perturberait le
1286
+ lobe principal et détruirait la faible perte de scalloping pour laquelle la fenêtre existe. La
1287
+ bibliothèque renvoie la série inchangée.
1288
+
1289
+ ### 14.8 Pièges
1290
+
1291
+ **Le remplissage par des zéros interpole, il ne résout jamais.** Une longueur de transformée
1292
+ supérieure au nombre d'échantillons échantillonne la même DTFT plus densément : la *position* du
1293
+ pic s'affine et l'espacement des bins devient `rate / size`, mais la capacité à séparer deux
1294
+ composantes reste gouvernée par la longueur et la forme de la fenêtre. Un son exactement entre deux
1295
+ bins lit `82.1093` et `80.8598` sur la grille non complétée, et `128` à sa propre fréquence sur la
1296
+ grille complétée.
1297
+
1298
+ ```typescript
1299
+ import { SynchronousSignalCollectable, useRectangularWindow } from "semantic-typescript";
1300
+
1301
+ const round = (value: number): number => Number(value.toFixed(4));
1302
+ const tone = SynchronousSignalCollectable.useRate(
1303
+ Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 100.5 * index / 256)),
1304
+ 256,
1305
+ );
1306
+ const flat = tone.spectrum(256n, useRectangularWindow);
1307
+ const padded = tone.spectrum(1024n, useRectangularWindow);
1308
+
1309
+ flat.getResolution().getHertz(); // 1
1310
+ padded.getResolution().getHertz(); // 0.25
1311
+ round(flat.magnitudeAt(100).get()); // 82.1093
1312
+ round(flat.magnitudeAt(101).get()); // 80.8598
1313
+ round(padded.magnitudeAt(402).get()); // 128
1314
+ padded.frequencyAt(402).get().getHertz(); // 100.5
1315
+ ```
1316
+
1317
+ Deux sons égaux espacés de `0.4` bin restent fusionnés quel que soit le remplissage : avec un
1318
+ enregistrement de 256 points et une fenêtre de Hann, compléter à 8192 ne produit qu'un seul maximum
1319
+ local, pas deux.
1320
+
1321
+ **La transformée de Hilbert est circulaire.** `analyticSignal`, `envelope` et
1322
+ `instantaneousPhase` traitent le premier et le dernier échantillon comme voisins, si bien qu'un
1323
+ enregistrement dont la période ne divise pas la longueur est contaminé aux deux bords. Le milieu de
1324
+ l'enregistrement est fiable, les bords non.
1325
+
1326
+ ```typescript
1327
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1328
+
1329
+ const round = (value: number): number => Number(value.toFixed(4));
1330
+ const edge = SynchronousSignalCollectable.useRate(
1331
+ Array.from({ length: 512 }, (_unused, index) => Math.cos(2 * Math.PI * 8.3 * index / 512)),
1332
+ 512,
1333
+ );
1334
+ const envelope = edge.envelope();
1335
+
1336
+ round(envelope.slice(8, -8).reduce((sum, value) => sum + value, 0) / 496); // 1.0025
1337
+ round(Math.max(...envelope.slice(0, 8))); // 1.2592
1338
+ round(Math.min(...envelope.slice(-8))); // 0.1314
1339
+ ```
1340
+
1341
+ **Une enveloppe qui s'annule ne définit pas de phase.** Là où l'enveloppe s'effondre, la différence
1342
+ de deux angles de bruit d'arrondi peut atteindre un demi-tour, ce qui se lit comme la moitié du
1343
+ taux d'échantillonnage. Le seuil est ce qui garde ce nombre significatif, et le défaut est `1E-3`
1344
+ de l'enveloppe de crête.
1345
+
1346
+ ```typescript
1347
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1348
+
1349
+ const round = (value: number): number => Number(value.toFixed(4));
1350
+ const beat = SynchronousSignalCollectable.useRate(
1351
+ Array.from({ length: 4096 }, (_unused, index) =>
1352
+ Math.cos(2 * Math.PI * 100 * index / 2048) + Math.cos(2 * Math.PI * 102 * index / 2048)),
1353
+ 2048,
1354
+ );
1355
+
1356
+ round(Math.max(...beat.instantaneousFrequency(2048, 0).map(Math.abs))); // 920.218
1357
+ round(Math.max(...beat.instantaneousFrequency().map(Math.abs))); // 101
1358
+ ```
1359
+
1360
+ **Une corrélation non normalisée n'est pas une similarité.** La somme brute `r[k]` croît avec la
1361
+ longueur de l'enregistrement, deux longueurs ne sont donc pas comparables. La règle par défaut
1362
+ `"coefficient"` divise par `r[0]` et garde chaque valeur dans `[-1, 1]` ; `"unbiased"` divise par
1363
+ `count − k` et paie d'une variance qui croît avec le décalage.
1364
+
1365
+ ```typescript
1366
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1367
+
1368
+ const round = (value: number): number => Number(value.toFixed(4));
1369
+ const periodic = SynchronousSignalCollectable.useRate(
1370
+ Array.from({ length: 1024 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 1024)),
1371
+ 1024,
1372
+ );
1373
+ const rho = periodic.autocorrelation(200n);
1374
+
1375
+ round(rho[0]); // 1
1376
+ round(rho[128]); // 0.875
1377
+ round(rho[64]); // -0.9375
1378
+ round(periodic.autocorrelation(200n, "none")[0]); // 512
1379
+ ```
1380
+
1381
+ Une dernière lecture mérite un avertissement : `spectralFlux` normalise chaque trame avant de la
1382
+ comparer, un simple changement de niveau produit donc un flux nul. Sa première et sa dernière
1383
+ valeur sont des artefacts de bord de la frontière de zéros, pas des attaques.
1384
+
1385
+ ---
1386
+
1387
+ <a id="15-license"></a>
1388
+ ## 15. Licence
1389
+
1390
+ MIT © Eloy Kim