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/CHANGELOG.md +587 -574
- package/dist/asynchronous/collectable.js +10 -4
- package/dist/optional.js +0 -1
- package/dist/readonly-map.d.ts +1 -0
- package/dist/readonly-map.js +26 -0
- package/dist/synchronous/collectable.js +2 -1
- package/package.json +1 -1
- package/readme.cn.md +1271 -1271
- package/readme.de.md +1397 -1397
- package/readme.es.md +1381 -1381
- package/readme.fr.md +1390 -1390
- package/readme.jp.md +1300 -1300
- package/readme.kr.md +1297 -1297
- package/readme.md +1442 -1442
- package/readme.ru.md +1378 -1378
- package/readme.tw.md +1271 -1271
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
|