semantic-typescript 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.de.md CHANGED
@@ -1,1397 +1,1397 @@
1
- # **Semantic-TypeScript**
2
-
3
- **Datenfluss, indexiert.** Ihre Daten, präzise unter Kontrolle.
4
-
5
- [English](./readme.md) · [简体中文](./readme.cn.md) · [繁體中文](./readme.tw.md) · [日本語](./readme.jp.md) · [한국어](./readme.kr.md) · Deutsch · [Français](./readme.fr.md) · [Español](./readme.es.md) · [Русский](./readme.ru.md)
6
-
7
- Eine typsichere Stream-Verarbeitungsbibliothek, die **JavaScript-Generatoren**, **Java Stream** und
8
- **indexbasierte Datenadressierung** vereint. Jedes Element trägt einen `bigint`-Index, jede Pipeline ist träge, und
9
- jede Terminaloperation muss ausdrücklich aktiviert werden.
10
-
11
- ```bash
12
- npm install semantic-typescript
13
- ```
14
-
15
- - Keine Laufzeitabhängigkeiten, reines ESM, liefert eigene Typen mit.
16
- - 684 Regressionstests: `npm test`.
17
- - Vollständige Änderungshistorie: [CHANGELOG.md](./CHANGELOG.md).
18
-
19
- ---
20
-
21
- ## Table of Contents
22
-
23
- 1. [Entwurfsphilosophie](#1-design-philosophy)
24
- 2. [Architektur](#2-architecture)
25
- 3. [Schnellstart](#3-quick-start)
26
- 4. [Fabrikfunktionen](#4-factory-functions)
27
- 5. [Transformationsoperatoren](#5-transformation-operators)
28
- 6. [Collectables und Terminaloperationen](#6-collectables-and-terminal-operations)
29
- 7. [Statistik und Fenster](#7-statistics-and-windows)
30
- 8. [Das Indexmodell](#8-the-index-model)
31
- 9. [Das asynchrone Modell](#9-the-asynchronous-model)
32
- 10. [Periodische Analyse: Ringe, Felder und Scores](#10-periodic-analysis-rings-fields-and-scores)
33
- 11. [Hilfsfunktionen](#11-utility-helpers)
34
- 12. [Leistung und Fallstricke](#12-performance-and-pitfalls)
35
- 13. [Phasenringe: ein exakter Zyklus](#13-phase-rings)
36
- 14. [Signalanalyse: Fenster, Spektren und Zeit](#14-signal-analysis)
37
- 15. [Lizenz](#15-license)
38
-
39
- ---
40
-
41
- <a id="1-design-philosophy"></a>
42
- ## 1. Entwurfsphilosophie
43
-
44
- Drei Ideen, eine Pipeline:
45
-
46
- **Index ist Daten.** Jedes Element wird über einen `bigint` adressiert. Umkehrung, Rotation, Mischen
47
- (Shuffling) und Aufteilung sind Index-Arithmetik, keine Pufferoperationen. Nichts wird materialisiert,
48
- bis ein Collectable es anfordert.
49
-
50
- **Trägheit ist die Vorgabe.** Eine Fabrik umschließt einen Generator; ein Operator umschließt einen
51
- weiteren Generator. Kein Element wird angefordert, bis eine Terminaloperation läuft, daher ist eine
52
- unendliche Quelle völlig zulässig, solange etwas Nachgelagertes sie stoppt.
53
-
54
- **Terminals sind explizit.** `map` gibt ein `Semantic` zurück, kein Array. Werte erscheinen erst, wenn
55
- Sie ein Collectable benennen — `toUnordered()`, `toOrdered()`, `toNumericStatistics()`, `toWindow()` —
56
- und jedes davon sagt, was es kostet und welche Reihenfolge es erzeugt.
57
-
58
- Dieselbe Disziplin gilt für die mathematische Hälfte der Bibliothek. Ein `Complex`, ein `Angle` oder
59
- eine `Frequency` ist ein unveränderlicher Wert, dessen Invarianten bei der Konstruktion geprüft
60
- werden; ein `Optional` ersetzt `null`; und ein Phasenring sagt genau, wie viele Elemente er hält,
61
- welche Periode er hat und wie ausgewogen er ist.
62
-
63
- ---
64
-
65
- <a id="2-architecture"></a>
66
- ## 2. Architektur
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
- | Schicht | Modul | Verantwortung |
76
- | :--- | :--- | :--- |
77
- | Quellen | `synchronous/collectors`, `asynchronous/collectors` | Umschließt ein Iterable, einen Generator, ein Promise, einen Blob, ein Event-Target |
78
- | Pipelines | `synchronous/semantic`, `asynchronous/semantic` | Träge Operatoren und das Umschreiben von Indizes |
79
- | Collectables | `synchronous/semantic`, `asynchronous/semantic` | Geordnete, ungeordnete, Statistik-, Fenster- und eigene Terminals |
80
- | Collectors | `synchronous/collector`, `asynchronous/collector` | Die Fold-Primitive, aus denen jedes Terminal aufgebaut ist |
81
- | Mathematik | `mathematics/*` | Winkel, Komplexe, Frequenzen, Spektren, Fenster, Schätzer, Epizyklen, Phasenringe |
82
- | Strukturen | `structure/*` | Listen, Sets, Maps, Bäume, Forste, Ringe, Felder |
83
- | Fehler | `structure/error` | Die typisierte `SemanticError`-Hierarchie und ihre Brand-Guards |
84
-
85
- Die synchrone und die asynchrone Familie teilen ein Vokabular: dieselben Operatornamen, dieselbe
86
- Indexsemantik, dieselben Collectables. Die asynchronen Varianten geben lediglich Promises zurück und
87
- stellen Schreibzugriffe in eine Warteschlange, sodass ein nach einem Schreibzugriff ausgelöster
88
- Lesezugriff ihn stets beobachtet.
89
-
90
- ### 2.1 Was dieses Release verschoben und gebrochen hat
91
-
92
- Drei Module haben ihren Ort oder ihre Form gewechselt, und eine Reihe von Standardwerten hat die Bedeutung geändert. Jedes alte Verhalten bleibt über eine explizite Option erreichbar; nichts wurde ohne Ersatz entfernt.
93
-
94
- | Änderung | Vorher | Jetzt |
95
- | :--- | :--- | :--- |
96
- | Forste | `structure/tree/rootedForest`, `structure/tree/unionFindForest` | `structure/forest/rootedForest`, `structure/forest/unionFindForest`; `tree/index.ts` re-exportiert sie nicht mehr |
97
- | Fehler | ad-hoc native Fehler | eine Hierarchie aus fünfzehn Klassen in `structure/error.ts`, mit je einem Brand-Guard pro Klasse und dem Alias `SemanticRangeError` |
98
- | Schätzer-Optionen | feste Definitionen | `mathematics/quantile.ts` exportiert `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator` und `VarianceOptions` |
99
- | `quantile` | Hyndman–Fan Typ 1 | Typ 7 — R `quantile`, NumPy `linear`, Excel `PERCENTILE.INC` |
100
- | `skewness` / `kurtosis` | `g1` / `g2` | die bias-korrigierten `G1` / `G2` |
101
- | `variance` / `standardDeviation` | `ddof = 0` | `ddof = 1`, in ein `Optional` gehüllt |
102
- | `decibel` | eine `20·log10`-Funktion | entfernt; `amplitudeDecibel` (20·log10) und `powerDecibel` (10·log10) |
103
- | `Spectrum.flatness()` | Beträge über beide Hälften | Leistungen über die einseitige Hälfte |
104
- | `Spectrum.bandEnergy()` | rohe zweiseitige Summe | standardmäßig einseitig, mit Faltung der konjugierten Hälfte |
105
- | `shortTimeFourierTransform` / `spectrogram` | symmetrisches Hann, nur vollständige Frames | periodisches Hann, `boundary: "zeros"`, `padded: true` |
106
-
107
- Die Kapitel 6, 7 und 10 dokumentieren die Ersatzlösungen; die gemessenen Folgen stehen in [CHANGELOG.md](./CHANGELOG.md).
108
-
109
- ---
110
-
111
- <a id="3-quick-start"></a>
112
- ## 3. Schnellstart
113
-
114
- ```typescript
115
- import {
116
- useOf, useFrom, useRange, useText, useStringify, usePromise
117
- } from "semantic-typescript";
118
-
119
- // Numeric aggregation
120
- const sum: number = useOf(10, 20, 30, 40)
121
- .map(n => n * 2)
122
- .toNumericStatistics()
123
- .summate(); // 200
124
-
125
- // BigInt aggregation
126
- const bigSum: bigint = useOf(10n, 20n, 30n, 40n)
127
- .map(n => n * 2n)
128
- .toBigintStatistics()
129
- .summate(); // 200n
130
-
131
- // Reordering is index arithmetic — collect with toOrdered() for it to take effect
132
- const reversed: number[] = useFrom([1, 2, 3, 4, 5]).reverse().toOrdered().toArray(); // [5,4,3,2,1]
133
- const rotated: number[] = useFrom([1, 2, 3, 4, 5]).translate(2).toOrdered().toArray(); // [4,5,1,2,3]
134
-
135
- // Lazy evaluation over a large range
136
- const multiples: bigint[] = useRange(0n, 1_000_000n)
137
- .filter(n => n % 17n === 0n)
138
- .limit(10n)
139
- .toUnordered()
140
- .toArray(); // [0n,17n,…,153n]
141
-
142
- // Unicode-safe string processing (code points, not UTF-16 units)
143
- useText("semantic 👍").toUnordered().forEach(character => console.log(character));
144
-
145
- // Circular-safe serialisation
146
- const cyclic: any = { a: 1 }; cyclic.self = cyclic;
147
- useStringify(cyclic); // '{"a":1}'
148
-
149
- // Terminals hand the elements to real structures, not only to arrays
150
- useOf(3, 1, 4, 1, 5).toCollectable().toOrderedSet().toArray(); // [1,3,4,5]
151
- useFrom([3, 1, 4, 1, 5]).toCollectable().toBinarySearchTree().size(); // 5n
152
-
153
- // Async sources
154
- const total = await usePromise(Promise.resolve(7)).toNumericStatistics().summate(); // 7
155
- ```
156
-
157
- ---
158
-
159
- <a id="4-factory-functions"></a>
160
- ## 4. Fabrikfunktionen
161
-
162
- Die Konstruktion ist stets O(1) — eine Fabrik umschließt lediglich einen Generator. Die Tabelle nennt
163
- die Kosten für den *Verbrauch* von `n` Elementen sowie den zusätzlichen Speicher, den die Quelle selbst
164
- benötigt.
165
-
166
- | Fabrik | Signatur (Ergebnis) | Zeit (n verbrauchen) | Speicher (Quelle) | Anmerkungen |
167
- | :--- | :--- | :--- | :--- | :--- |
168
- | `useOf` | `useOf<E>(...elements) → SynchronousSemantic<E>` | O(n) | O(n) | Indizes sind 0..n−1; das Spread-Argument ist bereits materialisiert |
169
- | `useFrom` | `useFrom<E>(Iterable<E>) → SynchronousSemantic<E>`<br>`useFrom<E>(AsyncIterable<E>) → AsynchronousSemantic<E>` | O(n) | O(1) | Umschließt jedes Iterable; beim Sammeln einmal iteriert |
170
- | `useRange` | `useRange<N>(start, end, step?) → SynchronousSemantic<N>` | O(n) | O(1) | `end` ist exklusiv; `step` muss von Null verschieden sein; `number` und `bigint` funktionieren beide |
171
- | `useFill` | `useFill<E>(element \| supplier, count) → SynchronousSemantic<E>` | O(n) | O(1) | Ein Supplier wird pro Element einmal aufgerufen |
172
- | `useEmpty` | `useEmpty<E>() → SynchronousSemantic<E>` | O(1) | O(1) | Neutralelement für `concat` |
173
- | `useText` | `useText(text, delimiter?)` | O(n) | O(n) für den Text | Iteriert über **Code-Punkte**, sodass Emoji und CJK einzelne Elemente bleiben |
174
- | `useAttribute` | `useAttribute<T>(object)` | O(v + e) | O(d + v) | Durchläuft verschachtelte Objekte rekursiv mit Zyklusschutz |
175
- | `useGenerate` | `useGenerate<E>(supplier, interrupt)` | O(n) × Supplier-Kosten | O(1) | **Muss** ein stoppendes `interrupt` besitzen oder ein nachgelagertes `limit` |
176
- | `useIterate` | `useIterate<E>(Iterable<E>)` / `(AsyncIterable<E>)` | O(n) pro Sammlung | O(1) | Spielt den Iterator bei jeder Sammlung erneut ab |
177
- | `useInterval` | `useInterval(period, delay?) → AsynchronousSemantic<number>` | n × period (Echtzeit) | O(1) | Gibt die verstrichene Zeit aus |
178
- | `usePromise` | `usePromise<T>(promise) → AsynchronousSemantic<T>` | O(1) | O(1) | Ein einzelnes Element am Index `0n` |
179
- | `useBlob` | `useBlob(blob, chunk?) → AsynchronousSemantic<Uint8Array>` | O(blob size) | O(chunk) pro Kopie | Standard-Chunk ist 64 KiB |
180
- | `useSubscription` | `useSubscription(target, subscriber, event) → AsynchronousSemantic<Event>` | ein Element pro Ereignis | O(1) | `limit()` / `takeWhile()` definieren die Abmelde-Grenze |
181
-
182
- ```typescript
183
- useOf(1, 2, 3).toUnordered().toArray(); // [1,2,3]
184
- useFrom(new Set([1, 2, 2])).toUnordered().toArray(); // [1,2]
185
- useRange(0, 5).toUnordered().toArray(); // [0,1,2,3,4]
186
- useRange(10n, 0n, -5n).toUnordered().toArray(); // [10n,5n]
187
- useFill("x", 3n).toUnordered().toArray(); // ["x","x","x"]
188
- useText("A😊B").toUnordered().toArray(); // ["A","😊","B"]
189
- useGenerate(() => 7, (_e, index) => index >= 3n).toUnordered().toArray(); // [7,7,7]
190
- ```
191
-
192
- ---
193
-
194
- <a id="5-transformation-operators"></a>
195
- ## 5. Transformationsoperatoren
196
-
197
- Jeder Operator gibt ein neues `Semantic` zurück; keiner berührt ein Element, bis ein Collectable läuft.
198
- Die Zeit ist der zusätzliche Aufwand **pro Element**; der Speicher ist das, was der Operator während
199
- des Pipeline-Laufs hält.
200
-
201
- | Operator | Zweck | Indexwirkung | Zeit | Speicher |
202
- | :--- | :--- | :--- | :--- | :--- |
203
- | `map(mapper)` | 1:1-Transformation | weitergeleitet | O(1) | O(1) |
204
- | `filter(predicate)` | passende Elemente behalten | weitergeleitet | O(1) | O(1) |
205
- | `flat(mapper)` / `flatMap(mapper)` | 1:N-Entfaltung | neu indiziert ab 0 | O(1) pro Ausgabe | O(1) |
206
- | `distinct(keyExtractor?)` | wiederholte Schlüssel verwerfen, den ersten behalten | weitergeleitet | O(1) im Mittel | O(n) |
207
- | `peek(consumer)` | Seiteneffekt, Element passiert hindurch | weitergeleitet | O(1) | O(1) |
208
- | `limit(count)` | die ersten *n* nehmen | neu indiziert ab 0 | O(1) | O(1) |
209
- | `skip(count)` | die ersten *n* verwerfen | weitergeleitet | O(1) | O(1) |
210
- | `sub(start, end)` | positionsbasierter Ausschnitt `[start, end)` | weitergeleitet | O(1) | O(1) |
211
- | `takeWhile(predicate)` | solange nehmen, wie das Prädikat zutrifft | weitergeleitet | O(1) | O(1) |
212
- | `dropWhile(predicate)` | solange verwerfen, wie das Prädikat zutrifft | neu indiziert ab 0 | O(1) | O(1) |
213
- | `concat(other)` | ein Semantic oder Iterable anhängen | setzt den Index fort | O(1) | O(1) |
214
- | `redirect(redirector)` | einen neuen Index nach Belieben berechnen | **Ihrer** | O(1) | O(1) |
215
- | `reverse()` | `redirect((_e, i) => -i - 1n)` | negiert und verschoben | O(1) | O(1) |
216
- | `translate(offset \| mapper)` | jeden Index verschieben | `i + offset` | O(1) | O(1) |
217
- | `shuffle(mapper?)` | den Index randomisieren | zufällig | O(1) | O(1) |
218
- | `pipe(conversion)` | den Generator an eine eigene Funktion übergeben | nach Belieben | Ihrer | Ihrer |
219
- | `semantic()` | ein Collectable zurück in ein Semantic heben | unverändert | O(1) | O(1) |
220
-
221
- **`reverse`, `translate` und `shuffle` schreiben nur Indizes um.** Nichts wird gepuffert oder rotiert,
222
- bis `toOrdered()` die Indizes modulo der Elementanzahl normalisiert:
223
-
224
- ```typescript
225
- useFrom([1, 2, 3, 4, 5]).reverse().toOrdered().toArray(); // [5,4,3,2,1]
226
- useFrom([1, 2, 3, 4, 5]).translate(2).toOrdered().toArray(); // [4,5,1,2,3] (rotation)
227
- useFrom([1, 2, 3, 4, 5]).translate(7).toOrdered().toArray(); // [4,5,1,2,3] (7 ≡ 2 mod 5)
228
- useFrom([1, 2, 3]).shuffle((_e, i) => -i - 1n).toOrdered().toArray(); // [3,2,1]
229
- ```
230
-
231
- ---
232
-
233
- <a id="6-collectables-and-terminal-operations"></a>
234
- ## 6. Collectables und Terminaloperationen
235
-
236
- ### 6.1 Collectables
237
-
238
- Das Collectable besitzt sowohl die **Reihenfolge** als auch die **Index-Buchhaltung**. `n` ist die
239
- Elementanzahl.
240
-
241
- | Collectable | Struktur | Zeit | Reihenfolge | Verwenden wenn |
242
- | :--- | :--- | :--- | :--- | :--- |
243
- | `toUnordered()` | `Map<bigint, E>` mit Index als Schlüssel | O(n) | Einfügungsreihenfolge | maximaler Durchsatz; rohe Indizes bleiben unangetastet |
244
- | `toOrdered()` | nach Elementindex sortiertes Array | O(n log n) | nach normalisiertem Index | Umkehrung/Rotation muss wirksam werden |
245
- | `toOrdered(comparator)` | nach Ihrem Comparator sortiert | O(n log n) | nach Ihrem Comparator | sortierte Ansichten |
246
- | `toNumericStatistics()` | wertsortierter Puffer | O(n log n) | nach Wert | Statistik über `number` |
247
- | `toBigintStatistics()` | wertsortierter Puffer | O(n log n) | nach Wert | Statistik über `bigint` |
248
- | `toWindow()` | wertsortierter Puffer | O(n log n) | nach Wert | `slide` / `tumble` |
249
- | `toCollectable(mapper)` | was auch immer der Mapper zurückgibt | — | — | eigene Collectables |
250
-
251
- ### 6.2 Terminals
252
-
253
- Jedes Terminal lebt am **Collectable**, nicht am `Semantic`. Genau das bedeutet „Terminals sind explizit" in der Praxis: Eine Pipeline erzeugt nichts, bis ein Collectable benannt wird, und `toCollectable()` ohne Mapper ist die günstigste Benennung.
254
-
255
- | Terminal | Liefert | Zeit | Anmerkungen |
256
- | :--- | :--- | :--- | :--- |
257
- | `toArray()` / `toSet()` | `Array<E>` / `Set<E>` | O(n) | entleert den Puffer; `toSet` dedupliziert nach Identität |
258
- | `toMap(key, value?)` | `Map<K, V>` | O(n) im Mittel | letzter Schreibzugriff gewinnt |
259
- | `count()` / `isEmpty()` | `bigint` / `boolean` | O(1) | der Puffer weiß es bereits |
260
- | `forEach(action)` | `void` | O(n) | `action(element, index)` |
261
- | `join(…)` | `string` | O(n) | `join()`, `join(delimiter)`, `join(prefix, delimiter, suffix)` |
262
- | `reduce(…)` | `Optional<E>` / `E` / `R` | O(n) | leerer Stream → leeres `Optional` |
263
- | `group(classifier)` / `groupBy(key, value)` | `Map<K, E[]>` | O(n) im Mittel | — |
264
- | `partition(count)` | `Array<Array<E>>` | O(n) | Round-Robin nach Index |
265
- | `partitionBy(classifier)` | `Array<Array<E>>` | O(n) | die Bucket-Nummer stammt vom Classifier |
266
- | `anyMatch` / `allMatch` / `nonMatch` | `boolean` | O(n) im schlimmsten Fall, bricht früh ab | — |
267
- | `findAny` / `findFirst` / `findLast` | `Optional<E>` | O(1)–O(n) | positionsbasiert in materialisierter Reihenfolge |
268
- | `findAt(k)` / `findNegativeAt(k)` | `Optional<E>` | O(k) / O(n−k) | zählt von vorne / von hinten |
269
- | `findMaximum(cmp?)` / `findMinimum(cmp?)` | `Optional<E>` | O(n) | Standard ist ein struktureller Vergleich |
270
- | `frequency()` | `Map<E, bigint>` | O(n) im Mittel | nur Statistik-Collectables |
271
- | `write(stream, accumulator?)` | `WritableStream<S>` | O(n) | IO-artiges Terminal |
272
- | `collect(collector)` | `R` | O(n) | der generische Fold hinter jedem obigen Terminal |
273
-
274
- ### 6.3 Struktur-Collectors
275
-
276
- `toArray`, `toSet` und `toMap` beenden eine Pipeline in einem JavaScript-Container. Zwölf weitere Terminals beenden sie in einer der Strukturen der Bibliothek selbst, sodass das Ergebnis die Invarianten trägt, die diese Struktur garantiert, statt derer, die ein Array zufällig hat. Sie stehen an jedem Collectable zur Verfügung, auch am nackten Collectable aus `toCollectable()`.
277
-
278
- | Terminal | Liefert | Reihenfolge | Kosten |
279
- | :--- | :--- | :--- | :--- |
280
- | `toArray()` | `Array<E>` | Durchlaufreihenfolge | O(n) |
281
- | `toSet()` | `Set<E>` | Einfügungsreihenfolge, SameValueZero | O(n) im Mittel |
282
- | `toMap(key, value?)` | `Map<K, V>` | Einfügungsreihenfolge; der letzte Schreibzugriff gewinnt | O(n) im Mittel |
283
- | `toOrderedSet(comparator?)` | `SynchronousOrderedSet<E>` | comparator-aufsteigend | O(n²) im schlimmsten Fall |
284
- | `toUnorderedSet()` | `SynchronousUnorderedSet<E>` | nicht spezifiziert (Hash-Buckets) | O(n) erwartet |
285
- | `toOrderedMap(key, value?)` | `SynchronousOrderedMap<K, V>` | Einfügungsreihenfolge; der letzte Schreibzugriff gewinnt | O(n) erwartet |
286
- | `toUnorderedMap(key, value?)` | `SynchronousUnorderedMap<K, V>` | nicht spezifiziert | O(n) erwartet |
287
- | `toList()` | `SynchronousList<E>` | Durchlaufreihenfolge, Duplikate bleiben | O(n) |
288
- | `toRing(span?)` | `SynchronousRing<E>` | die Durchlaufreihenfolge legt die Ringpositionen fest | O(n) |
289
- | `toLRUSet(capacity)` | `SynchronousUnorderedLRUSet<E>` | nach Aktualität; nur die letzten `capacity` überleben | O(n²) im schlimmsten Fall |
290
- | `toLRUMap(capacity, key, value?)` | `SynchronousUnorderedLRUMap<K, V>` | nach Aktualität | O(n) erwartet |
291
- | `toSkipSet(comparator?)` | `SynchronousSkipSet<E>` | comparator-aufsteigend, navigierbar | O(n²) im schlimmsten Fall |
292
- | `toSkipMap(key, value?)` | `SynchronousSkipMap<K, V>` | nach dem Schlüssel-Comparator, navigierbar | O(n) erwartet |
293
- | `toBinarySearchTree(comparator?)` | `SynchronousBinarySearchTree<E>` | comparator-aufsteigend, höhenbalanciert | O(n log n) |
294
- | `toRootedForest()` | `SynchronousRootedForest<E>` | die Durchlaufreihenfolge wird zur Struktur | O(n) |
295
-
296
- Der Ergebnistyp folgt dem Elementtyp des Collectables selbst, daher braucht kein Terminal ein Typargument: Ein `SynchronousCollectable<E>` liefert direkt `Array<E>`, `SynchronousList<E>` oder `SynchronousOrderedSet<E>`. Es bleiben nur die fachlichen Argumente — der Comparator von `toOrderedSet` / `toSkipSet` / `toBinarySearchTree`, die Kapazität des LRU-Paars, die Spanne von `toRing` und die Schlüssel- und Wert-Extraktoren der Map-Familie.
297
-
298
- Ein Ring ist ein Set, daher behalten wiederholte Elemente ihre erste Position; alle übrigen Strukturen oben behalten Duplikate, sofern ihr eigener Vertrag nichts anderes sagt (`toOrderedSet`, `toUnorderedSet` und `toSkipSet` sind Sets; `toBinarySearchTree` und `toList` nicht).
299
-
300
- ```typescript
301
- const numbers = useOf(3, 1, 4, 1, 5).toCollectable();
302
-
303
- numbers.toOrderedSet().toArray(); // [1,3,4,5]
304
- numbers.toUnorderedSet().size(); // 4n — the duplicate 1 collapses
305
- numbers.toList().toArray(); // [3,1,4,1,5]
306
- numbers.toSkipSet().first().get(); // 1 — navigable by comparator
307
- numbers.toBinarySearchTree().size(); // 5n — duplicates retained
308
- numbers.toRootedForest().getRoots().length; // 1
309
- useOf(1, 2, 3).toCollectable().toRing().phaseAt(3n).get().toDegreeValue(); // 0
310
- useOf(3, 1, 3).toCollectable().toOrderedMap(n => n, n => `v${n}`).get(3).get(); // "v3"
311
- ```
312
-
313
- Die asynchronen Collectables bieten dieselben Terminals und lösen jedes in einem Promise auf; die Aufrufform gewinnt also nur ein `await` hinzu.
314
-
315
- ---
316
-
317
- <a id="7-statistics-and-windows"></a>
318
- ## 7. Statistik und Fenster
319
-
320
- ### 7.1 Die Statistik-Oberfläche
321
-
322
- ```typescript
323
- useOf(3, 1, 4, 1, 5).toNumericStatistics().summate(); // 14
324
- useOf(3, 1, 4, 1, 5).toNumericStatistics().average(); // 2.8
325
- useOf(3, 1, 4, 1, 5).toNumericStatistics().median(); // 3
326
- useOf(3, 1, 4, 1, 5).toNumericStatistics().mode(); // 1
327
- useOf(3, 1, 4, 1, 5).toNumericStatistics().variance().get(); // 3.2 (n − 1)
328
- useOf(3, 1, 4, 1, 5).toNumericStatistics().standardDeviation().get(); // 1.7889 (n − 1)
329
- ```
330
-
331
- Beide Statistik-Familien bieten dieselbe Oberfläche — `summate`, `average`, `median`, `mode`, `variance`, `standardDeviation`, `frequency`, `findMaximum`, `findMinimum` — mit `bigint`-Arithmetik für `toBigintStatistics()`. Die Streuungsmaße und die Quantil-Familie geben `Optional` zurück, weil eine Stichprobe zu klein sein kann, um sie zu definieren; entpacken Sie sie mit `.get()`, `.orElse(…)` oder `.isPresent()`.
332
-
333
- ### 7.2 Schätzer und Standardwerte
334
-
335
- Die Standardwerte sind die Definitionen, die R, NumPy, pandas und Excel verwenden; ein hier berechneter Wert lässt sich also ohne Korrekturfaktor mit diesen Werkzeugen vergleichen.
336
-
337
- | Statistik | Standard | Vorher | Alten Wert zurückholen mit |
338
- | :--- | :--- | :--- | :--- |
339
- | `quantile(probability)` | Hyndman–Fan Typ 7 | Typ 1 | `{ method: "type-1" }` |
340
- | `interquartileRange()` | Hyndman–Fan Typ 7 | Typ 1 | `{ method: "type-1" }` |
341
- | `skewness()` | bias-korrigiertes `G1` | `g1` | `{ estimator: "g1" }` |
342
- | `kurtosis()` | bias-korrigiertes Excess-`G2` | `g2` | `{ estimator: "g2" }` |
343
- | `variance()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
344
- | `standardDeviation()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
345
-
346
- Alle neun Hyndman–Fan-Typen stehen als `"type-1"` … `"type-9"` zur Verfügung, und `{ excess: false }` addiert drei wieder zur Kurtosis hinzu und meldet das nicht-exzessive vierte standardisierte Moment. Die Optionstypen — `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator`, `VarianceOptions` — werden aus `mathematics/quantile.ts` exportiert.
347
-
348
- ```typescript
349
- const sample = useOf(1, 2, 3, 4).toNumericStatistics();
350
-
351
- sample.variance().get(); // 1.6667 (5/3, ddof = 1)
352
- sample.variance({ ddof: 0 }).get(); // 1.25
353
- sample.standardDeviation().get(); // 1.2910
354
- sample.skewness().get(); // 0
355
- sample.kurtosis().get(); // -1.2 (G2)
356
- sample.kurtosis({ estimator: "g2" }).get(); // -1.36
357
- sample.kurtosis({ excess: false }).get(); // 1.8
358
-
359
- const quartiles = useOf(1, 2, 3, 4, 5, 6, 7, 8).toNumericStatistics();
360
- quartiles.quantile(0.75).get(); // 6.25 (type 7)
361
- quartiles.quantile(0.75, { method: "type-1" }).get(); // 6 (nearest rank)
362
- quartiles.interquartileRange().get(); // 3.5
363
- ```
364
-
365
- `G1` und `G2` sind unter drei bzw. vier Beobachtungen undefiniert, und die Stichprobenvarianz sowie die Standardabweichung sind unter `ddof = 1` unter zwei Beobachtungen undefiniert. Das wird als leeres `Optional` gemeldet statt als irreführende Null:
366
-
367
- ```typescript
368
- useOf(1, 2, 3).toNumericStatistics().kurtosis().isEmpty(); // true (G2 needs four observations)
369
- useOf(1, 2).toNumericStatistics().skewness().isEmpty(); // true (G1 needs three)
370
- useOf(1).toNumericStatistics().variance().isEmpty(); // true (ddof = 1 needs two)
371
- useOf(1).toNumericStatistics().variance({ ddof: 0 }).get(); // 0
372
- ```
373
-
374
- Die bigint-Ansichten akzeptieren dieselben Optionen und runden jedes Ergebnis auf ein ganzzahliges `bigint` zurück.
375
-
376
- ### 7.3 Fenster
377
-
378
- Fenster legen die gepufferten Elemente erneut als Pipeline von Pipelines offen, sodass ein Fenster konsumiert werden kann, ohne es zuerst in ein Array zu materialisieren. Beide Formen enthalten ein letztes partielles Fenster.
379
-
380
- ```typescript
381
- useFrom([1, 2, 3, 4]).toWindow().slide(2n, 1n).toUnordered().toArray()
382
- .map(window => window.toUnordered().toArray()); // [[1,2],[2,3],[3,4],[4]]
383
-
384
- useFrom([1, 2, 3, 4]).toWindow().tumble(2n).toUnordered().toArray()
385
- .map(window => window.toUnordered().toArray()); // [[1,2],[3,4]]
386
- ```
387
-
388
- ---
389
-
390
- <a id="8-the-index-model"></a>
391
- ## 8. Das Indexmodell
392
-
393
- Ein Index ist ein `bigint`, der jedem Element anhängt. Operatoren **leiten** ihn **weiter**,
394
- **indizieren** ihn **von Null neu** oder lassen Sie ihn **berechnen**.
395
-
396
- Das geordnete Sammeln normalisiert jeden Index modulo der Elementanzahl und sortiert dann nach dem
397
- Ergebnis. Deshalb ist `translate(7)` auf fünf Elementen gleich `translate(2)`: `7 ≡ 2 (mod 5)`.
398
-
399
- Das ungeordnete Sammeln behält jeden Index, den die Pipeline erzeugt hat, und gibt Elemente in
400
- Ankunftsreihenfolge zurück. Negative Indizes sind zulässig und sortieren nach der Normalisierung
401
- schlicht vor den nicht-negativen.
402
-
403
- ---
404
-
405
- <a id="9-the-asynchronous-model"></a>
406
- ## 9. Das asynchrone Modell
407
-
408
- `AsynchronousSemantic` spiegelt die synchrone Oberfläche und gibt Promises zurück. Schreibzugriffe
409
- werden in eine Warteschlange gestellt, sodass ein nach einem Schreibzugriff ausgelöster Lesezugriff ihn
410
- stets beobachtet.
411
-
412
- ```typescript
413
- const ring = new AsynchronousRing([1, 2]);
414
- const added = ring.add(3); // queued
415
- const sampled = ring.elementAt(2n); // observes the write above
416
- await added;
417
- (await sampled).get().element; // 3
418
- ```
419
-
420
- Fabriken, die von Natur aus asynchron sind — `usePromise`, `useBlob`, `useInterval`,
421
- `useSubscription` — geben direkt `AsynchronousSemantic` zurück, und `useFrom` / `useIterate` stufen
422
- sich selbst hoch, wenn ihnen ein `AsyncIterable` übergeben wird.
423
-
424
- ---
425
-
426
- <a id="10-periodic-analysis-rings-fields-and-scores"></a>
427
- ## 10. Periodische Analyse: Ringe, Felder und Scores
428
-
429
- Die mathematische Hälfte der Bibliothek behandelt einen Zyklus als erstklassiges Objekt und nicht als
430
- Nachschlagetabelle. Drei Konstruktionen wirken zusammen.
431
-
432
- ### 10.1 Ein Ring ist eine ganze Zahl
433
-
434
- Ein `Ring` ist ein **n-facher Ring**: Elementanzahl, Positionsperiode und die Anzahl unterscheidbarer
435
- Phasen sind dieselbe ganze Zahl `n`. Die Phase einer Position wird aus ihrem exakten Rest berechnet,
436
- sodass der Ring bis auf das Bit periodisch ist:
437
-
438
- ```typescript
439
- import { SynchronousRing } from "semantic-typescript";
440
-
441
- const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
442
- ring.phaseAt(0n).get().toDegreeValue(); // 0
443
- ring.phaseAt(1n).get().toDegreeValue(); // 60
444
- ring.phaseAt(6n).get().getValue() === ring.phaseAt(0n).get().getValue(); // true, exactly
445
- ring.purity(); // 1
446
- ring.isBalanced(); // true
447
- ring.sequences().map(z => z.magnitude()); // [0, 6, 0, 0, 0, 0]
448
- ```
449
-
450
- `frequency` ist die **umgebende** Rotationsrate, kein Phasenschritt zwischen Positionen: Die Umgebung
451
- bewegt jeden Phasor um denselben Winkel, kann das intrinsische Muster also nie brechen. `span` legt
452
- fest, wie viele Umdrehungen der Ring einnimmt.
453
-
454
- `purity()` ist der Energieanteil der Grundmode, `max(|c₁|², |c_{n−1}|²) / (n · Σ|z|²)`. Er ist genau
455
- dann `1`, wenn der Ring bis auf Rotation die `n`-ten Einheitswurzeln ist, und er kostet `O(n)`, weil
456
- nur zwei Bins benötigt werden. Beide Richtungen werden geprüft, da ein Ring den Kreis in beide
457
- Richtungen durchlaufen kann.
458
-
459
- ### 10.2 Ein Feld ist ein beschränkter Score
460
-
461
- Ein `PhaseField` bildet eine relative Phase auf einen Score in `[-1, 1]` ab:
462
-
463
- ```typescript
464
- import { useField, useRadian } from "semantic-typescript";
465
-
466
- const cosine = useField([1]); // cos(2πΔ): +1 aligned, −1 opposed
467
- const mixed = useField([0.4, 0.3, 0.3]); // first three harmonics
468
-
469
- cosine.sample(useRadian(0)); // 1
470
- cosine.sample(useRadian(Math.PI)); // -1
471
- ```
472
-
473
- Der Score ist eine abgebrochene Kosinusreihe, `a₀ + Σ wₘ·cos(2πmΔ)`, und der Konstruktor erzwingt
474
- `|a₀| + Σ|wₘ| ≤ 1`. Nach der Dreiecksungleichung kann der Score `[-1, 1]` niemals verlassen — die
475
- Schranke ist eine **Konsequenz der Konstruktion**, kein nachträglich angewendetes Clamping.
476
-
477
- Der Grund, warum das Feld eine Reihe und keine Tabelle ist: Eine Tabelle mit `n` Punkten reproduziert
478
- ihre eigenen Stützstellen exakt, ihr trigonometrisches Interpolant schießt dazwischen jedoch über. Eine
479
- Fünf-Punkt-Tabelle mit den Scores `[+1, +0.5, −1, −0.5, 0]` interpoliert über `[−1.0428, 1.0041]`, was
480
- das Versprechen bereits bricht. Eine beschränkte Reihe kann das nicht.
481
-
482
- ### 10.3 Die Analyse misst Daten gegen jedes Element
483
-
484
- Jedes Ringelement ist eine Referenzphase. `analyse` verortet jede Beobachtung wieder auf dem Ring und
485
- misst sie gegen jede Referenz:
486
-
487
- ```typescript
488
- const ring = new SynchronousRing(["a", "b", "c", "d", "e"]);
489
- const report = ring.analyse(["a", "c", "c", "a", "zz", "e"]);
490
-
491
- report.matched; // 5
492
- report.missed; // 1 ('zz' is not on the ring)
493
- report.references.map(r => r.count); // [2, 0, 2, 0, 1]
494
- ```
495
-
496
- Jede Referenzzeile meldet `count`, die mittlere relative Phase `mean` und die mittlere Resultantenlänge
497
- `resultant`. Eine Identität lohnt sich zu merken: **`resultant` ist für jede Referenz gleich**, weil
498
- eine geänderte Basis alle relativen Phasen nur um eine Konstante rotiert. Pro Referenz unterscheiden
499
- sich der Count und der Mittelwert.
500
-
501
- ```typescript
502
- const ring = new SynchronousRing(["a", "b", "c"]);
503
- ring.scoresAt(0, cosine); // one score per position, in [-1, 1]
504
- ```
505
-
506
- ### 10.4 Verwandte Instrumente
507
-
508
- | Aufruf | Beantwortet | Kosten |
509
- | :--- | :--- | :--- |
510
- | `purity()` | ist der Ring noch die `n`-ten Einheitswurzeln? | O(n) |
511
- | `resultant(t?)` | wie konzentriert sind die Phasoren gerade jetzt? | O(n) |
512
- | `analyse(data)` | wie liegen diese Daten gegen jedes Element? | O(n · m) |
513
- | `toSpectrum()` | der Ring als Spektrum, Wellenzahl auf der Frequenzachse | O(n²) |
514
- | `useAngularSpectrum(ring, instants, plan)` | die `(ω, n)`-Ebene über die Zeit | O(n² · frames) |
515
-
516
- Zwei weitere Primitive runden das Modell ab: `combineRings(a, b)` verbindet zwei Ringe auf ihrem
517
- kleinsten gemeinsamen Vielfachen (ein Pullback, kein Produkt — die Paare, die sich keinen Zeitpunkt
518
- teilen können, fehlen schlicht), und `rate()` gibt jedem Element seine eigene intrinsische Frequenz,
519
- sodass ein Ring gegen die Umgebung driften kann. `jetAt`, `windingAt` und `errorIntegralAt` legen den
520
- differentiellen Zustand dieser Drift offen.
521
-
522
- ### 10.5 Spektrale Fenster
523
-
524
- Die Ringkonstruktionen sind die eine Hälfte der analytischen Oberfläche; die andere arbeitet an abgetasteten Signalen. Ihr meistgenutztes Primitiv ist das Fenster, und jede Form hier hat zwei Varianten. Die symmetrische Variante teilt den Frame durch `count − 1`; die periodische (DFT-gerade) Variante teilt ihn durch `count`, einen Abtastwert weniger als eine volle Periode, sodass die Ausblendung unter der periodischen Fortsetzung, die die DFT annimmt, stetig ist.
525
-
526
- ```typescript
527
- useHannWindow(1, 4); // 0.75 symmetric
528
- useHannPeriodicWindow(1, 4); // 0.5 periodic
529
- coherentGain(useHannPeriodicWindow, 16); // 0.5
530
- equivalentNoiseBandwidth(useHannPeriodicWindow, 16); // 1.5
531
- ```
532
-
533
- | Form | Symmetrisch | Periodisch | Anmerkungen |
534
- | :--- | :--- | :--- | :--- |
535
- | Rechteck | `useRectangularWindow` | — | keine Ausblendung; keine periodische Unterscheidung |
536
- | Hann | `useHannWindow` | `useHannPeriodicWindow` | der Standard der Kurzzeitanalyse |
537
- | Hamming | `useHammingWindow` | `useHammingPeriodicWindow` | — |
538
- | Blackman | `useBlackmanWindow` | `useBlackmanPeriodicWindow` | — |
539
- | Nuttall | `useNuttallWindow` | `useNuttallPeriodicWindow` | viergliedriges Blackman–Harris |
540
- | Flat-Top | `useFlatTopWindow` | `useFlatTopPeriodicWindow` | Amplitudengenauigkeit vor Auflösung |
541
- | Kaiser | `useKaiserWindow(beta)` | `useKaiserPeriodicWindow(beta)` | `beta` bestimmt die Hauptkeulenbreite |
542
-
543
- Verwenden Sie die periodische Variante für Analyse und Overlap-Add: Bei einem Halbframe-Hop summiert das periodische Hann-Fenster bei jeder Länge zu `1.000000`, während die symmetrische Variante bei `N = 16` um `0.841 dB` und bei `N = 1024` noch um `0.013 dB` wellig ist. Für den Filterentwurf bleibt die symmetrische Variante richtig, wo der Frame nicht eine Periode der Ausblendung ist.
544
-
545
- Die Flat-Top-Fenster liefern an den Frame-Enden **negative Gewichte**, weil ein Flat-Top-Fenster genau das ist. Sie werden unverändert zurückgegeben statt abgeschnitten, denn Abschneiden stört die Hauptkeule und zerstört den geringen Scalloping-Verlust, um dessentwillen das Fenster existiert:
546
-
547
- ```typescript
548
- useFlatTopWindow(0, 1024); // -0.000421 — negative by definition
549
- Array.from({ length: 1024 }, (_unused, index) => useFlatTopWindow(index, 1024))
550
- .filter(weight => weight < 0).length; // 578
551
- ```
552
-
553
- ### 10.6 Pegel, Flachheit und Bandenergie
554
-
555
- `decibel` ist als einzelne Funktion verschwunden, weil Amplitude und Leistung keinen gemeinsamen Faktor teilen. `amplitudeDecibel` wendet `20·log10` auf ein spannungsartiges Verhältnis an; `powerDecibel` wendet `10·log10` auf eine Leistung, eine Energie, ein mittleres Quadrat oder eine spektrale Leistungsdichte an:
556
-
557
- ```typescript
558
- amplitudeDecibel(0.25); // -12.0412
559
- powerDecibel(0.25); // -6.0206
560
- ```
561
-
562
- `Bin.decibel` und `Spectrum.decibelOf` sind Amplitudenpegel; `Spectrum.powerDecibelOf` ist der passende Leistungspegel. Eine Leistung an die Amplitudenform zu geben, meldet `20·log10(power)` und verdoppelt den Pegel in Dezibel.
563
-
564
- Zwei Spektrummessungen haben die Lesart gewechselt:
565
-
566
- ```typescript
567
- const twoBins = new Spectrum(new Sample(2, 1), [new Complex(2, 0), new Complex(1, 0)]);
568
- twoBins.flatness(); // 0.8 — geometric over arithmetic mean of power
569
-
570
- const shape = new Spectrum(new Sample(4, 1), [
571
- new Complex(1, 0), new Complex(2, 0), new Complex(0, 0), new Complex(0, 0),
572
- ]);
573
- shape.bandEnergy(new Frequency(0), new Frequency(2)); // 9
574
- shape.bandEnergy(new Frequency(0), new Frequency(2), "two-sided"); // 5
575
- ```
576
-
577
- `flatness()` vergleicht das geometrische und das arithmetische Mittel von `power()` über die einseitige Hälfte des Spektrums, nicht das Betragsmittel über beide Hälften, und die reine Anzeigeverdopplung von `oneSided()` wird bewusst nicht angewandt. `bandEnergy()` nutzt standardmäßig die einseitige Lesart und faltet die konjugierte Hälfte, sodass DC und Nyquist einfach und jeder innere Bin doppelt zählen; ein Band über die gesamte Achse erfüllt dann Parseval, `Σ|X|² = N·Σx²`. Übergeben Sie `"two-sided"` für ein komplexes Spektrum, dessen Bins bereits unabhängig sind.
578
-
579
- ### 10.7 Kurzzeitanalyse
580
-
581
- `Sampler.shortTimeFourierTransform` und `Sampler.spectrogram` folgen den SciPy-`stft`-Konventionen: periodisches Hann-Fenster, eine mit Nullen gefüllte Grenze an beiden Enden und ein mit Nullen gefüllter Schwanz.
582
-
583
- ```typescript
584
- const sampler = new Sampler(new Sample(64, 1));
585
- const spectrogram = sampler.spectrogram(time => Math.cos(2 * Math.PI * 4 * time), 16, 8);
586
-
587
- spectrogram.size(); // 9 frames
588
- spectrogram.getBoundary(); // "zeros"
589
- spectrogram.getTime(6); // 0.75 — frame centres, not frame starts
590
- sampler.spectrogram(time => Math.cos(2 * Math.PI * 4 * time), 16, 8, useHannWindow,
591
- { boundary: "none", padded: false }).size(); // 7 — complete frames only
592
- ```
593
-
594
- Die Erweiterung zentriert den ersten Frame auf den ersten Abtastwert, und die Schwanzauffüllung hält die letzten Abtastwerte erreichbar: 64 Abtastwerte bei Frame 16 und Hop 8 ergeben 9 Frames statt 7, und 1000 Abtastwerte bei Frame 256 und Hop 128 ergeben 9 statt 6. Frame-Zeiten sind Mitten — `(k·hop + (frameSize mod 2)/2)/rate` für eine erweiterte Ansicht, `(k·hop + frameSize/2)/rate` für eine nicht erweiterte — und `toFrame()` schneidet die Erweiterung vor der Inversion ab.
595
-
596
- ---
597
-
598
- <a id="11-utility-helpers"></a>
599
- ## 11. Hilfsfunktionen
600
-
601
- ### 11.1 Die Fehlerhierarchie
602
-
603
- `structure/error.ts` exportiert eine Hierarchie aus fünfzehn Klassen für die Fehler, die *rund um* eine Pipeline entstehen — in Ihrem eigenen Code, einem eigenen Collector oder einem Adapter:
604
-
605
- ```text
606
- SemanticError
607
- ├── IllegalOperationError
608
- ├── ValidationError
609
- │ ├── IndexError
610
- │ ├── RangeError (also exported as SemanticRangeError)
611
- │ └── ConfigurationError
612
- ├── CapacityError
613
- ├── StateError
614
- │ └── IllegalStateError
615
- ├── ConcurrencyError
616
- ├── CollectionError
617
- ├── SerializationError
618
- ├── NumericError
619
- │ └── ArithmeticError
620
- └── PhaseError
621
- ```
622
-
623
- Jede Klasse legt `name` fest, reicht die native `cause` weiter, trägt eine optionale `context`-Tasche und serialisiert über `toJSON()`; jede hat einen gleichnamigen Brand-Guard.
624
-
625
- ```typescript
626
- const error = new CapacityError("LRU capacity exhausted", { context: { capacity: 0 } });
627
-
628
- error.name; // "CapacityError"
629
- isCapacityError(error); // true
630
- isValidationError(error); // false — a capacity failure is not a validation failure
631
- isSemanticError(error); // true
632
- error.toJSON(); // { name: "CapacityError", message: "…", context: { capacity: 0 } }
633
- ```
634
-
635
- `SemanticRangeError` ist dieselbe Klasse wie das exportierte `RangeError`, sodass `instanceof` über beide Namen hinweg gilt; ein Import von `RangeError` verdeckt den globalen Konstruktor für das ganze Modul, und deshalb existiert der Alias. Keine der beiden Klassen wird von den Argumentprüfungen der Bibliothek geworfen, die weiterhin native `TypeError` und `RangeError` auslösen — die Hierarchie ist das Vokabular für die Ebene darüber.
636
-
637
- ### 11.2 Hilfstabelle
638
-
639
- | Hilfsfunktion | Zweck |
640
- | :--- | :--- |
641
- | `useCompare(a, b)` | Totale Ordnung über gemischte Primitive hinweg |
642
- | `useToNumber` / `useToBigInt` | Verlustfreie, bereichsgeprüfte numerische Konvertierung |
643
- | `useRandom` | Zufallshelfer mit oder ohne Seed |
644
- | `useTraverse` | Struktureller Durchlauf verschachtelter Objekte und Arrays |
645
- | `useStringify` | Zyklussichere JSON-Serialisierung |
646
- | `useHash` / `maskOf` / `register` / `unregister` | Strukturelles Hashing und die Registry der Hash-Strategien |
647
- | `invalidate` / `validate` | Guard-Klauseln, die eine konsistente Fehlerform auslösen |
648
- | `Optional<T>` | Der `null`-Ersatz der Bibliothek: `Optional.of(x)` / `Optional.empty()` |
649
-
650
- ### 11.3 `useTraverse`
651
-
652
- `useTraverse(target, callback)` flacht jedes Objekt zu seinen **Nicht-Null-Blättern** ab. Es steigt in verschachtelte Objekte hinab, durchläuft iterierbare Werte nach Position, überspringt `null` und `undefined` und betritt ein bereits besuchtes Objekt nie erneut — ein Objektgraph mit einem Zyklus endet also, statt den Stack zu sprengen.
653
-
654
- ```typescript
655
- const source = { id: 7, tags: ["a", "b"], owner: { name: "Ada", note: null } };
656
- const leaves: string[] = [];
657
-
658
- useTraverse(source, (key, value, path) => {
659
- leaves.push(`${path.join(".")} = ${String(value)}`);
660
- return true;
661
- });
662
-
663
- leaves; // ["id = 7", "tags.0 = a", "tags.1 = b", "owner.name = Ada"]
664
- ```
665
-
666
- Der Callback läuft einmal pro Blatt und erhält den eigenen Schlüssel des Blatts (bei einem iterierbaren Wert dessen Index), den Wert und den Pfad dorthin. Die Rückgabe `false` gibt die **gesamte** Traversierung auf, nicht nur den aktuellen Zweig:
667
-
668
- ```typescript
669
- let visited = 0;
670
- useTraverse({ a: 1, b: 2, c: 3 }, () => {
671
- visited += 1;
672
- return false;
673
- });
674
- visited; // 1 — the first leaf already stops the walk
675
- ```
676
-
677
- Aus demselben Grund ist ein Zyklus ungefährlich:
678
-
679
- ```typescript
680
- const cyclic: any = { name: "root", child: { name: "leaf" } };
681
- cyclic.child.parent = cyclic;
682
-
683
- const names: string[] = [];
684
- useTraverse(cyclic, (_key, value) => {
685
- if (typeof value === "string") names.push(value);
686
- return true;
687
- });
688
- names; // ["root", "leaf"] — the back edge to `cyclic` is not revisited
689
- ```
690
-
691
- **Jeder Callback erhält seinen eigenen Pfad-Schnappschuss.** Der Durchlauf hält intern nur ein Arbeitsarray und kopiert es in dem Moment, in dem ein Blatt übergeben wird, in ein neues, mit `Object.freeze` eingefrorenes Array. Der Schnappschuss ändert sich danach nicht mehr, kann also unverändert behalten werden; zwei Callbacks teilen sich nie ein Array, und ein erneuter Durchlauf stört bereits ausgegebene Schnappschüsse nicht. Sein Typ ist `ReadonlyArray`, ein Schreibzugriff ist daher ein Compile-Fehler, und auf dem eingefrorenen Array wirft er zur Laufzeit zusätzlich einen `TypeError`. Diese Sicherheit kostet eine `O(depth)`-Allokation pro gemeldetem Blatt; ein Zweig, der nichts meldet, allokiert nichts zusätzlich.
692
-
693
- ```typescript
694
- const paths: Array<ReadonlyArray<PropertyKey>> = [];
695
- useTraverse({ a: { b: 1 }, c: 2 }, (_key, _value, path) => {
696
- paths.push(path); // safe to keep as-is
697
- return true;
698
- });
699
- paths[0] === paths[1]; // false — one array per call
700
- paths.map(path => path.join(".")); // ["a.b", "c"]
701
- ```
702
-
703
- `useAttribute` ist dieser Durchlauf als Fabrik: Sie meldet dieselben Blätter als `{ key, value }`-Paare, also sind Regeln und Kosten identisch.
704
-
705
- ```typescript
706
- useAttribute({ a: 1, b: { c: 2 } }).toUnordered().toArray();
707
- // [{ key: "a", value: 1 }, { key: "c", value: 2 }]
708
- ```
709
-
710
- ---
711
-
712
- <a id="12-performance-and-pitfalls"></a>
713
- ## 12. Leistung und Fallstricke
714
-
715
- **Trägheit hat einen Preis pro Stufe.** Jeder Operator fügt einen Generator-Hop hinzu. Für enge
716
- numerische Schleifen über In-Memory-Arrays schlägt eine schlichte `for`-Schleife jede Pipeline; die
717
- Pipeline gewinnt, wenn die Quelle träge oder unendlich ist oder die Pipeline lang genug ist, damit sich
718
- die Index-Disziplin auszahlt.
719
-
720
- **`toOrdered()` sortiert; `toUnordered()` nicht.** Wenn eine Umkehrung „nicht funktioniert" zu haben
721
- scheint, war der Collector ungeordnet — das Umschreiben von Indizes wird erst dann zu einer Reihenfolge,
722
- wenn die Indizes normalisiert werden.
723
-
724
- **`useGenerate` ohne `interrupt` endet nie.** Geben Sie stets ein stoppendes Prädikat oder ein
725
- nachgelagertes `limit` / `takeWhile` an.
726
-
727
- **`Ring` ist ein einfügungsgeordnetes Set.** Elemente sind eindeutig und behalten die Reihenfolge, in
728
- der sie zuerst hinzugefügt wurden. Ein doppeltes `add` gibt `false` zurück, und ein `set`, das einen
729
- Doppelgänger einführen würde, wirft. Genau das erlaubt es, eine Phase als Funktion des *Elements* zu
730
- behandeln und nicht der Position, die es gerade zufällig einnimmt.
731
-
732
- **`Spectrum` setzt in seinen einseitigen Helfern ein reelles Signal voraus.** `oneSided()`,
733
- `getNyquist()` und `centroid()` unterstellen konjugierte Symmetrie. Das räumliche Spektrum eines
734
- Phasenrings ist komplex und im Allgemeinen nicht symmetrisch — ein Ring, der den Kreis rückwärts
735
- durchläuft, legt seine gesamte Energie in Bin `n − 1`, jenseits von Nyquist. Lesen Sie die Bins direkt.
736
-
737
- **Struktur-Collectors sind Terminals, keine Ansichten.** Jeder leert die Pipeline und baut dann eine vollständige Struktur auf; er kostet also O(n) Zeit und O(n) Speicher und läuft nicht auf einer unendlichen Quelle. Verwenden Sie einen am Ende einer Pipeline, niemals innerhalb einer Schleife über eine Pipeline.
738
-
739
- **`variance()` und seine Verwandten geben ein `Optional` zurück.** `variance().get()` auf einer zu kleinen Stichprobe wirft; verwenden Sie `.orElse(0)` oder prüfen Sie `.isPresent()`, wenn die Stichprobengröße nicht in Ihrer Hand liegt. Dasselbe gilt für `standardDeviation`, `quantile`, `interquartileRange`, `skewness` und `kurtosis`.
740
-
741
- **Die periodischen Fenster sind die Analysefenster.** `useHannWindow` und die anderen symmetrischen Formen sind für den Filterentwurf gedacht; eine Kurzzeitanalyse will `useHannPeriodicWindow` und seine Geschwister, sonst ist die Overlap-Add-Summe an jeder Fuge der Frames wellig.
742
-
743
- ---
744
-
745
- <a id="13-phase-rings"></a>
746
- ## 13. Phasenringe: ein exakter Zyklus
747
-
748
- Kapitel 10 führte den Ring als Modell eines Zyklus ein und nutzte ihn, um einen Datensatz gegen jede
749
- Phase zu messen. Dieses Kapitel ist die vollständige Oberfläche: wie aus einer Position ein Winkel
750
- wird, was die Umgebungsfrequenz ändert und was nicht, und die Instrumente, die den Ring gegen die
751
- Zeit, gegen Daten und gegen einen anderen Ring lesen.
752
-
753
- Ein `SynchronousRing` ist eine Positionsliste, unterlegt von einem kreisförmigen doppelt
754
- verketteten Ring, dessen Kopf und Schwanz verbunden sind. Er ist zugleich eine einfügungsgeordnete
755
- *Menge*: kein Element erscheint zweimal, und ein abgelehnter Schreibvorgang lässt sowohl die
756
- Verkettung als auch die Abtastwerte unberührt. Zum Ring statt zur bloßen Liste macht ihn, dass die
757
- Phase einer Position eine reine Funktion ihres Restes modulo der aktuellen Länge ist; deshalb ist
758
- der Ring bitgenau periodisch, und seine Balance kann nicht driften.
759
-
760
- ### 13.1 Ein Ring ist eine ganze Zahl, exakt berechnet
761
-
762
- Ein Ring trägt `n` Elemente, und Elementanzahl, Positionsperiode und Anzahl verschiedener Phasen
763
- sind dieselbe ganze Zahl. Position `i` trägt
764
-
765
- `phase + span · 2π · (((i mod n) + n) mod n) / n`
766
-
767
- wobei der Rest in `bigint` reduziert wird, bevor er je eine `number` wird. Genau deshalb sind
768
- `phaseAt(i)` und `phaseAt(i + n)` *identisch* und nicht bloß ähnlich: Der zweite Aufruf reduziert
769
- auf dieselbe ganze Zahl, wertet dasselbe Produkt aus und erzeugt dasselbe Double. Ein Index weit
770
- jenseits des exakten Ganzzahlbereichs von `number` landet noch auf der richtigen Position, und ein
771
- negativer Index läuft den Ring rückwärts.
772
-
773
- | Aufruf | Rückgabe | Bedeutung |
774
- | :--- | :--- | :--- |
775
- | `new SynchronousRing(source, options?)` | Ring | `source` ist ein beliebiges Iterable, Duplikate entfallen |
776
- | `getPhase()` / `getFrequency()` / `getSpan()` | `Angle`, `Frequency`, `number` | die drei Konstruktionsparameter |
777
- | `size()` | `bigint` | das `n`, das zugleich die Periode ist |
778
- | `phaseAt(index)` | `Optional<Angle>` | nur die intrinsische Phase; ignoriert Zeit, Rate und Frequenz |
779
- | `elementAt(index)` | `Optional<RingElement>` | Element, intrinsische Phase, Umgebungsfrequenz, Zeiger |
780
- | `frequencyAt(index)` | `Optional<Frequency>` | die Umgebungsrate, leer bei leerem Ring |
781
- | `toComplex()` / `toComplex(index)` | `Complex[]` / `Optional<Complex>` | intrinsische Zeiger |
782
-
783
- `RingOptions` ergänzt zwei Funktionen pro Element: `magnitude(element, index)` liefert die
784
- Zeigerlänge und ist standardmäßig eins, `rate(element, index)` gibt dem Element seine eigene
785
- intrinsische Frequenz und ist standardmäßig null, also eine feste Markierung.
786
-
787
- ```typescript
788
- import { SynchronousRing } from "semantic-typescript";
789
-
790
- const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
791
-
792
- ring.size(); // 6n
793
- ring.phaseAt(0n).get().toDegreeValue(); // 0
794
- ring.phaseAt(1n).get().toDegreeValue(); // 60
795
- ring.phaseAt(-1n).get().toDegreeValue(); // 300
796
- ring.phaseAt(6n).get().getValue() === ring.phaseAt(0n).get().getValue(); // true
797
- ring.phaseAt(10n ** 20n).get().toDegreeValue(); // 240
798
- ```
799
-
800
- `phaseAt(-1n)` ist bei einem Ring aus sechs Elementen `300`, und `10n ** 20n` reduziert modulo
801
- sechs auf `4`, liest sich also als `240`. Keiner der Fälle akkumuliert ein Fließkommaprodukt; das
802
- hält die Identität exakt, auch wenn sich die Indexdarstellung ändert.
803
-
804
- ### 13.2 Die Umgebungsfrequenz und der Zustand zu einem Zeitpunkt
805
-
806
- `frequency` ist die **Umgebungsrotationsrate** der Umgebung, in die der Ring eingebettet ist, kein
807
- Phasenschritt zwischen Positionen. Die Umgebung verschiebt die Phase jedes Elements um denselben
808
- Winkel, kann also das intrinsische Muster nicht ändern, die Balance nicht brechen und kein
809
- `phaseAt`-Ergebnis verändern. Der Standard ist `0 Hz`.
810
-
811
- `relativeAt(index, time)` zieht diesen gemeinsamen Winkel ab und hinterlässt die Koordinate, gegen
812
- die ein `PhaseField` bewertet wird. `viewAt` und `viewsAt` liefern den vollen Zustand zu einem
813
- Zeitpunkt samt der eigenen `rate` des Elements, und die Differential-Zugriffe legen die Drift frei:
814
-
815
- | Aufruf | Formel | Liest |
816
- | :--- | :--- | :--- |
817
- | `relativeAt(index, time)` | `intrinsisch + eigeneRate · t − Umgebung · t` | die Phase, die die Umgebung sieht |
818
- | `viewAt(index, time)` / `viewsAt(time)` | dasselbe plus Element, Index, Zeit, Rate, Zeiger | der volle Zustand |
819
- | `jetAt(index, time)` | Phase bei `t`, eigene Rate, `slip = eigeneRate − Umgebung` | der Differentialzustand |
820
- | `windingAt(index, time)` | `trunc(slip · t)` | ganze Umdrehungen Schlupf, gegen null abgeschnitten |
821
- | `errorIntegralAt(index, time, origin?)` | `∫ relative Phase dt`, in Umdrehungen · Sekunden | wie lange ein Element voraus war |
822
- | `resultant(time?)` | `abs(Σ w · e^{iθ}) / Σ w` | Phasenkonzentration, von Umgebungsrotation unberührt |
823
- | `analyse(data, time?)` | Zählungen und Mittel je Referenz | Daten gegen jedes Element gemessen |
824
- | `scoreAt(index, time, field)` / `scoresAt(time, field)` | das Feld an der relativen Phase abgetastet | ein beschränkter Score je Position |
825
-
826
- ```typescript
827
- import { SynchronousRing, useHertz } from "semantic-typescript";
828
-
829
- const moving = new SynchronousRing(["a", "b", "c", "d", "e", "f"], {
830
- frequency: useHertz(1),
831
- });
832
-
833
- moving.getFrequency().getHertz(); // 1
834
- moving.phaseAt(1n).get().toDegreeValue(); // 60 — intrinsic
835
- moving.relativeAt(1n, 0.25).get().toDegreeValue(); // 330 — 60 − 90
836
- moving.viewAt(1n, 0.25).get().relative.toDegreeValue(); // 330
837
- moving.jetAt(1n, 0.25).get().slip; // -1
838
- moving.purity(); // 1 — rotation cannot break it
839
- ```
840
-
841
- Bei `1 Hz` rückt die Umgebung in `0.25 s` um eine Vierteldrehung vor, die relative Phase von
842
- Position eins ist also `60 − 90 = 330` Grad, während ihre intrinsische Phase `60` bleibt. Der Jet
843
- meldet `slip` von `-1` Hertz, weil das Element selbst ruht, und `purity()` ist weiterhin `1`: Alle
844
- Zeiger um denselben Winkel zu drehen ist keine Verformung.
845
-
846
- ### 13.3 Reinheit und Balance
847
-
848
- `purity()` ist der Energieanteil des Rings, den die Grundmode trägt:
849
-
850
- `max(abs(c₁)², abs(c_{n−1})²) / (n · Σ abs(z)²)`
851
-
852
- Beide Grundbins werden geprüft, weil ein Ring den Kreis in beide Richtungen durchlaufen kann. Ein
853
- Ring mit `span: 5` über sechs Elemente schreitet um `300` Grad, also `−60` Grad: Er läuft rückwärts,
854
- legt seine ganze Energie in `c₅` und würde von einer Formel, die nur `c₁` betrachtet, als unrein
855
- gemeldet. Die Kosten bleiben `O(n)`, weil nur diese beiden Bins und die Gesamtenergie gebraucht
856
- werden.
857
-
858
- Die Randfälle gehören zum Vertrag. Ein Ring mit weniger als zwei Elementen hat keine Grundmode und
859
- meldet `purity() === 0` und `isBalanced() === false`, unabhängig vom Inhalt. Ein Ring, dessen
860
- Zeiger alle die Länge null haben, hat keine Energie und meldet ebenfalls null. `isBalanced(epsilon?)`
861
- ist `1 − purity() <= epsilon`, mit `1E-9` als Standardtoleranz.
862
-
863
- ```typescript
864
- import { SynchronousRing } from "semantic-typescript";
865
-
866
- const forward = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
867
- const backward = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 5 });
868
- const weighted = new SynchronousRing(["a", "b", "c", "d"], {
869
- magnitude: element => element === "a" ? 1 : 0,
870
- });
871
-
872
- forward.purity(); // 1
873
- backward.phaseAt(1n).get().toDegreeValue(); // 300 — it runs backwards
874
- backward.purity(); // 1 — the energy sits in c₅
875
- new SynchronousRing(["only"]).purity(); // 0
876
- new SynchronousRing(["only"]).isBalanced(); // false
877
- weighted.purity(); // 0.25
878
- ```
879
-
880
- ### 13.4 Resultierende Länge und die Symmetriekomponenten
881
-
882
- `resultant(time?)` ist die mittlere resultierende Länge: der Betrag der Zeigersumme geteilt durch
883
- die Summe der Beträge. Anders als `purity`, das fragt, ob der Ring noch die `n`-ten Einheitswurzeln
884
- ist, fragt `resultant`, wie konzentriert die Zeiger gerade sind. Ein balancierter Ring meldet `0`;
885
- ein Ring, dessen Elemente alle einen Winkel teilen, meldete `1`, aber die Mengensemantik verbietet
886
- Duplikate, sodass `1` praktisch erscheint, wenn `magnitude` alle bis auf ein Element nullt.
887
-
888
- `zeroSequence()` ist der mittlere Zeiger, also `c₀ / n`. `sequences()` liefert alle `n`
889
- Symmetriekomponenten, niedrigste Ordnung zuerst, über eine direkte Transformation. `toSpectrum()`
890
- packt sie in ein `Spectrum`, dessen Bin `j` `j` Zyklen pro Ringumdrehung meldet, sodass die
891
- Frequenzachse die Wellenzahlachse ist.
892
-
893
- ```typescript
894
- import { SynchronousRing } from "semantic-typescript";
895
-
896
- const round = (value: number): number => Number(value.toFixed(4));
897
- const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
898
-
899
- round(ring.resultant().get()); // 0
900
- round(ring.zeroSequence().get().magnitude()); // 0
901
- ring.sequences().map(component => round(component.magnitude())); // [0, 6, 0, 0, 0, 0]
902
- ring.toSpectrum().get().size(); // 6
903
- ring.toSpectrum().get().getResolution().getHertz(); // 1
904
- ```
905
-
906
- Die Komponenten sind komplex und im Allgemeinen **nicht konjugiert-symmetrisch**: Ein balancierter
907
- Ring legt seine ganze Energie in Bin eins, ein rückwärts laufender in Bin `n − 1`. Die einseitigen
908
- Hilfsfunktionen von `Spectrum` setzen ein reelles Signal voraus; lies die Bins direkt, statt sie zu
909
- falten.
910
-
911
- ### 13.5 Daten gegen jedes Element analysieren
912
-
913
- `analyse(data, time?)` ordnet jede Beobachtung über Identität dem Ring zu und misst sie gegen jedes
914
- Element als Referenzphase. Beobachtungen, die der Ring nicht hält, werden als `missed` gezählt statt
915
- geraten, denn der Ring ist die einzige Quelle von Phasen. Jede Referenzzeile meldet `count`, die
916
- mittlere relative Phase `mean` und die mittlere resultierende Länge `resultant`.
917
-
918
- Eine Identität lohnt sich zu merken: **`resultant` ist für jede Referenz gleich**. Eine andere
919
- Basis dreht alle relativen Phasen um eine Konstante, und eine Drehung kann eine resultierende Länge
920
- nicht ändern. Was sich je Referenz unterscheidet, sind Zählung und Mittel.
921
-
922
- ```typescript
923
- import { SynchronousRing } from "semantic-typescript";
924
-
925
- const round = (value: number): number => Number(value.toFixed(4));
926
- const ring = new SynchronousRing(["a", "b", "c", "d", "e"]);
927
- const report = ring.analyse(["a", "c", "c", "a", "zz", "e"]);
928
-
929
- report.matched; // 5
930
- report.missed; // 1
931
- report.references.map(row => row.count); // [2, 0, 2, 0, 1]
932
- report.references.map(row => round(row.resultant)); // [0.1453, 0.1453, 0.1453, 0.1453, 0.1453]
933
- ```
934
-
935
- Bewertung ist die andere Seite derselben Medaille. Ein `PhaseField` ist eine beschränkte
936
- Kosinusreihe (Kapitel 10.2), und `scoreAt`/`scoresAt` tasten sie an der relativen Phase ab, sodass
937
- derselbe Ring gegen Ausrichtung, gegen ein harmonisches Muster oder gegen ein beliebiges Feld
938
- bewertet werden kann, ohne den Ring zu ändern.
939
-
940
- ```typescript
941
- import { SynchronousRing, useField } from "semantic-typescript";
942
-
943
- const round = (value: number): number => Number(value.toFixed(4));
944
- const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
945
- const cosine = useField([1]);
946
-
947
- round(ring.scoreAt(0n, 0, cosine).get()); // 1
948
- round(ring.scoreAt(1n, 0, cosine).get()); // 0.5
949
- ring.scoresAt(0, cosine).map(score => round(score)); // [1, 0.5, -0.5, -1, -0.5, 0.5]
950
- ```
951
-
952
- ### 13.6 Ringe kombinieren und das Winkelspektrum
953
-
954
- `combineRings(left, right)` verbindet zwei Ringe über ihr kleinstes gemeinsames Vielfaches, nicht
955
- über ihr Produkt. Instant `i` des gemeinsamen Rings liest `i mod m` des linken und `i mod n` des
956
- rechten Rings, sodass der gemeinsame Ring nach `lcm(m, n)` Instants schließt. Die Paare, die nie
957
- gemeinsam auftreten können — deren Positionen modulo `gcd(m, n)` nicht übereinstimmen —, fehlen
958
- einfach; deshalb ist das Ergebnis ein Pullback und kein Produkt, und `aligns(left, right)` meldet,
959
- ob ein gegebenes Paar darstellbar ist.
960
-
961
- `useAngularSpectrum(ring, instants, plan)` baut die `(ω, n)`-Ebene: Jeder Instant trägt einen Frame
962
- von Zeigern bei, Frame `k` der Mode `j` ist die `j`-te Symmetriekomponente dieses Frames, und
963
- `cells[frame][order]` ist der Koeffizient. Eine Spalte zu nehmen ergibt die Zeitreihe einer
964
- einzigen räumlichen Mode, und `spectrum(order)` ist ihr zeitliches Spektrum.
965
-
966
- ```typescript
967
- import { Sample, SynchronousRing, combineRings, useAngularSpectrum } from "semantic-typescript";
968
-
969
- const left = new SynchronousRing(["a", "b", "c", "d"]);
970
- const right = new SynchronousRing(["x", "y", "z", "w", "v", "u"]);
971
- const joined = combineRings(left, right);
972
-
973
- joined.period; // 12n
974
- joined.divisor; // 2n
975
- joined.ring.size(); // 12n
976
- joined.aligns(0n, 0n); // true
977
- joined.aligns(1n, 0n); // false
978
- joined.ring.get(0n).join("-"); // a-x
979
-
980
- const angular = useAngularSpectrum(left, [0, 0.25, 0.5, 0.75], new Sample(4, 1));
981
- angular.order; // 4
982
- angular.frameCount; // 4
983
- angular.cells[0][1].magnitude(); // 4
984
- angular.spectrum(1).get().size(); // 4
985
- ```
986
-
987
- ### 13.7 Fallstricke
988
-
989
- **Exaktheit ist exakt.** `phaseAt(i)` und `phaseAt(i + n)` sind dasselbe Double, nicht zwei Werte
990
- innerhalb einer Toleranz. Ein Ring, der pro Element `2π / n` akkumuliert, würde das verfehlen;
991
- dieser reduziert den Index zuerst in `bigint`.
992
-
993
- **`frequency` ist kein Phasenschritt.** Es ist die Umgebungsrotationsrate, standardmäßig null, und
994
- alle Zeiger um denselben Winkel zu drehen kann das intrinsische Muster nicht brechen. Wenn sich
995
- Positionen relativ zueinander bewegen sollen, nutze `rate`, nicht `frequency`.
996
-
997
- **`purity()` muss beide Grundbins betrachten.** Ein rückwärts laufender Ring ist noch immer die
998
- `n`-ten Einheitswurzeln; nur `c₁` würde ihn für unrein erklären.
999
-
1000
- **`n < 2` ist ein definierter Randfall.** `purity()` liefert `0` und `isBalanced()` `false`; ein
1001
- leerer Ring liefert leere Optionals aus den Abtastmethoden.
1002
-
1003
- **Ein Span, der einen Faktor mit `n` teilt, kollabiert die Umlaufbahn.** Sechs Elemente mit
1004
- `span: 2` besuchen nur drei verschiedene Phasen, und der Ring ist nicht mehr balanciert.
1005
-
1006
- **`resultant` ist rotationsinvariant, nicht zeitinvariant.** Die Umgebungsrotation lässt ihn
1007
- unverändert; Raten pro Element ändern ihn, während der Ring aus der Phase läuft.
1008
-
1009
- **Der Ring ist eine Menge, und `analyse` ordnet über Identität zu.** Duplikate entfallen bei der
1010
- Konstruktion, und ein Objekt, das einem Ringelement strukturell gleicht, aber nicht dieselbe
1011
- Referenz ist, zählt als verfehlt.
1012
-
1013
- **`toSpectrum()` ist komplex.** Seine Bins sind nicht konjugiert-symmetrisch, die einseitigen
1014
- Hilfsfunktionen von `Spectrum` greifen also nicht. Lies die Bins.
1015
-
1016
- **`windingAt` schneidet gegen null ab, und `errorIntegralAt` integriert die Phase.** Ein halber
1017
- Umlauf Schlupf meldet `0n`; das Fehlerintegral ist in Umdrehungen · Sekunden, keine Rate.
1018
-
1019
- ```typescript
1020
- import { SynchronousRing, useHertz } from "semantic-typescript";
1021
-
1022
- const collapsed = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 2 });
1023
- collapsed.phaseAt(1n).get().toDegreeValue(); // 120
1024
- collapsed.phaseAt(3n).get().toDegreeValue(); // 0
1025
- Number(collapsed.purity().toFixed(4)); // 0
1026
-
1027
- const falling = new SynchronousRing(["a", "b", "c", "d"], {
1028
- frequency: useHertz(2),
1029
- rate: () => useHertz(0.5),
1030
- });
1031
- falling.jetAt(0n, 2).get().slip; // -1.5
1032
- falling.windingAt(0n, 1).get(); // -1n
1033
- falling.windingAt(0n, 0.5).get(); // 0n
1034
-
1035
- const objects = new SynchronousRing([{ key: 1 }, { key: 2 }]);
1036
- objects.analyse([{ key: 1 }]).missed; // 1 — identity, not structure
1037
- ```
1038
-
1039
- ---
1040
-
1041
- <a id="14-signal-analysis"></a>
1042
- ## 14. Signalanalyse: Fenster, Spektren und Zeit
1043
-
1044
- Kapitel 10 behandelte einen Ring als Zyklus von Phasen. Dieses Kapitel ist das zeitliche
1045
- Gegenstück: eine reellwertige Aufzeichnung, die neben ihren Abtastwerten den beiden Kontext
1046
- mitführt, den jede Spektralformel braucht — den Abtastplan, der einen Bin-Index in Hertz
1047
- verwandelt, und die Fensterfunktion, die sowohl den kohärenten Gewinn als auch die
1048
- Rauschbandbreite festlegt.
1049
-
1050
- Das Wertobjekt ist `SynchronousSignalCollectable`, sein eingereihtes Gegenstück ist
1051
- `AsynchronousSignalCollectable`. Beide bieten dieselbe `SignalCollectable`-Oberfläche:
1052
- dreiundzwanzig Methoden, jede delegiert an genau eine `useSynchronous<Method>`-Fabrik in
1053
- `synchronous/collectors`. Die Trennung ist Absicht. Die Klasse ist eine dokumentierte Fassade,
1054
- die Fabrik besitzt die Akkumulation, und der Konstruktor berechnet `sum(w) / count` und
1055
- `count * sum(w^2) / sum(w)^2` einmal, bei dieser Länge. Wer sich merken muss, „durch den
1056
- kohärenten Gewinn zu teilen", vergisst es irgendwann, und die daraus folgenden 6,02 Dezibel
1057
- sehen nach einer plausiblen Messung aus.
1058
-
1059
- ### 14.1 Ein Signal trägt seine eigene Kalibrierung
1060
-
1061
- Ein Signal entsteht aus einem Iterable und einer Rate, aus einem vorhandenen `Frame` oder über
1062
- den Konstruktor mit einem expliziten `Sample`. Das Standardfenster ist das periodische
1063
- Hann-Fenster; jede Analyse ohne eigenes Fenster verwendet es, und jede Analyse akzeptiert eine
1064
- Überschreibung. Die Metadaten-Zugriffe lesen Zustand, der beim Bauen festgeschrieben wurde, in
1065
- konstanter Zeit, sodass ein Signal weitergereicht werden kann, ohne dass der Kontext driftet.
1066
-
1067
- | Aufruf | Rückgabe | Bedeutung |
1068
- | :--- | :--- | :--- |
1069
- | `SynchronousSignalCollectable.useRate(values, rate, window?)` | Signal | Dauer `count / rate` |
1070
- | `SynchronousSignalCollectable.useFrame(frame, window?)` | Signal | behält den Plan des Frames |
1071
- | `new SynchronousSignalCollectable(plan, values, window?)` | Signal | Frame-Zahl muss der Abtastwertzahl entsprechen |
1072
- | `getSample()` / `getValues()` / `getWindow()` | Plan, Werte, Fenster | der eingefrorene Kontext |
1073
- | `getRate()` | `number` | Abtastwerte pro Sekunde |
1074
- | `getNyquist()` / `getResolution()` | `Frequency` | `rate / 2` und `rate / count` |
1075
- | `getCoherentGain()` / `getNoiseBandwidth()` | `number` | bei genau dieser Länge neu berechnet |
1076
- | `size()` / `at(index)` / `toFrame()` / `equals(other)` / `toString()` | — | Wertsemantik |
1077
-
1078
- ```typescript
1079
- import { SynchronousSignalCollectable } from "semantic-typescript";
1080
-
1081
- const signal = SynchronousSignalCollectable.useRate(
1082
- Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 64)),
1083
- 64,
1084
- );
1085
-
1086
- signal.size(); // 64n
1087
- signal.getRate(); // 64
1088
- signal.getNyquist().getHertz(); // 32
1089
- signal.getResolution().getHertz(); // 1
1090
- signal.getCoherentGain(); // 0.5
1091
- signal.getNoiseBandwidth(); // 1.5
1092
- ```
1093
-
1094
- `getResolution()` ist der Bin-Abstand, nicht das Auflösungsvermögen: Er beträgt `rate / count`,
1095
- weil die Transformation `count` Bins hat, und Nullen-Auffüllen ändert den Abstand, nicht das,
1096
- was das Fenster trennen kann (Abschnitt 14.8).
1097
-
1098
- ### 14.2 Die dreiundzwanzig Methoden
1099
-
1100
- Die Methoden bilden sechs Familien. Sieben sind Spektralmessungen, zwei sind Zeit-Frequenz-
1101
- Transformationen, vier sind analytische Größen, drei sind Folgestatistiken, fünf sind
1102
- Formbeschreibungen, und zwei liefern ein weiteres Signal.
1103
-
1104
- | Methode | Rückgabe | Konvention |
1105
- | :--- | :--- | :--- |
1106
- | `spectrum(size?, window?)` | `Spectrum` | roh und unnormiert, volles komplexes Raster |
1107
- | `oneSided(size?, window?)` | `Bin[]` | konjugierte Hälfte gefaltet, DC und Nyquist nicht verdoppelt |
1108
- | `amplitude(window?)` | `number[]` | `2·abs(X) / (count · CG)`, DC und Nyquist einmal |
1109
- | `power(window?)` | `number[]` | `A² / 2` je innerem Bin, `A²` bei DC und Nyquist |
1110
- | `decibel(window?)` | `number[]` | `20·log10(Amplitude)` |
1111
- | `powerDecibel(window?)` | `number[]` | `10·log10(Leistung)` |
1112
- | `powerSpectralDensity(frameSize?, hopSize?, window?)` | `SpectralDensity` | Welch-Mittel, `abs(X)² / (rate · Σw²)` |
1113
- | `shortTimeFourierTransform(frameSize, hopSize, window?)` | `Spectrum[]` | Ränder erweitert, Zeiten als Frame-Mitten |
1114
- | `spectrogram(frameSize, hopSize, window?)` | `Spectrogram` | dieselben Spalten mit Zeitachse |
1115
- | `analyticSignal()` | `Complex[]` | Hilbert-Paar `x + i·H(x)` |
1116
- | `instantaneousPhase()` | `Angle[]` | in `[0, 2π)` gefaltet |
1117
- | `instantaneousFrequency(rate?, gate?)` | `number[]` | entfaltete Phasendifferenz, auf die Hüllkurve gegated |
1118
- | `envelope()` | `number[]` | `abs(z[n])`, an den Rändern zirkulär |
1119
- | `zeroCrossingRate(perSecond?)` | `number` | Nulldurchgänge pro benachbartem Wertepaar |
1120
- | `spectralFlux(frameSize?, hopSize?, window?)` | `number[]` | Betragsabstand normierter Frames |
1121
- | `autocorrelation(maximumLag?, normalization?)` | `number[]` | standardmäßig Koeffizient, `ρ[0] = 1` |
1122
- | `spectralCentroid(window?)` | `Optional<Frequency>` | leistungsgewichtete Mittelfrequenz |
1123
- | `spectralBandwidth(window?)` | `Optional<Frequency>` | leistungsgewichtete Standardabweichung |
1124
- | `spectralRolloff(ratio?, window?)` | `Optional<Frequency>` | erster Bin, der `ratio` der Leistung erreicht |
1125
- | `spectralFlatness(window?)` | `number` | geometrisches zu arithmetischem Mittel der Leistung |
1126
- | `signalToNoiseRatio(frameSize?, hopSize?, window?, floorQuantile?)` | `Optional<number>` | Median-Rauschboden, `10·log10` |
1127
- | `detrended(mode?)` | Signal | `"none"`, `"constant"` (Standard), `"linear"` |
1128
- | `windowed(window?)` | Signal | wendet ein Fenster an und übernimmt es |
1129
-
1130
- Die optionalen Argumente sind keine Zierde. `frameSize` ist standardmäßig `min(256, size)`, der
1131
- Hop eine halbe Fensterlänge abgerundet, `instantaneousFrequency` gated bei `1E-3` der
1132
- Spitzenhüllkurve, und `spectralRolloff` nutzt `0.85`. Jeder Standard ist der Wert, den eine
1133
- Referenzimplementierung wählt, wenn der Parameter benannt, aber nicht übergeben wird.
1134
-
1135
- ### 14.3 Amplitude, Leistung und Dichte
1136
-
1137
- Drei Normierungen sind nötig, weil drei verschiedene Fragen gestellt werden.
1138
-
1139
- `amplitude()` beantwortet „wie groß ist der Sinus auf diesem Bin": `2·abs(X[k]) / (count · CG)`
1140
- für einen inneren Bin und `abs(X[k]) / (count · CG)` bei DC und Nyquist. Der Faktor zwei faltet
1141
- die konjugierte Hälfte, der kohärente Gewinn entfernt das Fenster. Fehlt eines von beiden, liest
1142
- ein Sinus mit Einheitsamplitude `0.5`, also minus 6,02 Dezibel.
1143
-
1144
- `power()` beantwortet „welches mittlere Quadrat hätte ein Sinus auf diesem Bin": `A² / 2` für
1145
- einen inneren Bin und `A²` bei DC und Nyquist, weil diese beiden nicht schwingen. Das ist die
1146
- Peak-Konvention, und sie erfüllt bewusst nicht Parseval: Ein Fenster verteilt jede Linie über
1147
- seine Hauptkeule, sodass die Summe aller Bins das mittlere Quadrat um die äquivalente
1148
- Rauschbandbreite übersteigt — bei Hann um `1.5`.
1149
-
1150
- `powerSpectralDensity()` beantwortet „wie viel Leistung pro Hertz": `abs(X)² / (rate · Σw²)`,
1151
- gemittelt über überlappende Segmente. Nur die Summe der quadrierten Gewichte hält einen
1152
- Weißrauschboden unverzerrt; Division durch `count` liest `0.375` für Einheitsrauschen, Division
1153
- durch `count · CG²` liest `1.5`.
1154
-
1155
- ```typescript
1156
- import { SynchronousSignalCollectable } from "semantic-typescript";
1157
-
1158
- const round = (value: number): number => Number(value.toFixed(4));
1159
- const tone = SynchronousSignalCollectable.useRate(
1160
- Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1161
- 256,
1162
- );
1163
-
1164
- round(tone.amplitude()[32]); // 1
1165
- round(tone.power()[32]); // 0.5
1166
- round(tone.power().reduce((sum, power) => sum + power, 0)); // 0.75
1167
- round(tone.powerSpectralDensity(64n, 32n).totalPower()); // 0.5
1168
- tone.powerSpectralDensity(64n, 32n).getSegments(); // 7n
1169
- ```
1170
-
1171
- Das Paar der Summen ist die Prüfung, die man sich merken sollte: Die Peak-Summe ist `0.75`,
1172
- während die Dichte zu `0.5` integriert, und das Verhältnis ist genau die Rauschbandbreite. Nur
1173
- die Dichte übersteht außerdem eine Änderung der Fensterlänge.
1174
-
1175
- ### 14.4 Pegel: die beiden Dezibel-Konventionen
1176
-
1177
- Amplitude und Leistung teilen keinen Faktor, also auch keinen Logarithmus. `amplitudeDecibel`
1178
- wendet `20·log10` an und ist für Spannung, Auslenkung oder Schalldruck richtig; `powerDecibel`
1179
- wendet `10·log10` an und ist für Leistung, Energie, mittleres Quadrat oder Dichte richtig. Die
1180
- Methodennamen des Signals folgen derselben Trennung.
1181
-
1182
- ```typescript
1183
- import { amplitudeDecibel, powerDecibel, SynchronousSignalCollectable } from "semantic-typescript";
1184
-
1185
- const round = (value: number): number => Number(value.toFixed(4));
1186
- const tone = SynchronousSignalCollectable.useRate(
1187
- Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1188
- 256,
1189
- );
1190
-
1191
- amplitudeDecibel(0.25); // -12.0412
1192
- powerDecibel(0.25); // -6.0206
1193
- round(tone.decibel()[32]); // 0
1194
- round(tone.powerDecibel()[32]); // -3.0103
1195
- ```
1196
-
1197
- Beide Ablesungen desselben Tons sind korrekt: Seine Amplitude ist eins (null Dezibel), sein
1198
- mittleres Quadrat ist ein Halb (minus 3,01 Dezibel). Eine Leistung durch die Amplitude-Konvention
1199
- zu schicken verdoppelt den Pegel in Dezibel und macht aus den korrekten minus 6,02 die minus
1200
- 12,04.
1201
-
1202
- ### 14.5 Die einseitige Faltung
1203
-
1204
- Ein reelles Signal erfüllt `X[count − k] = conj(X[k])`, die negative Frequenzhälfte trägt also
1205
- keine unabhängige Information. Die Faltung verdoppelt jeden inneren Bin und lässt die beiden
1206
- selbstkonjugierten Bins — DC und, bei gerader Länge, Nyquist — genau so, wie die Transformation
1207
- sie erzeugt hat. Diese beiden sind ihr eigenes Spiegelbild; Verdoppeln erfindet Energie und
1208
- meldet exakt 6,02 Dezibel zu hoch.
1209
-
1210
- ```typescript
1211
- import { SynchronousSignalCollectable } from "semantic-typescript";
1212
-
1213
- const round = (value: number): number => Number(value.toFixed(4));
1214
- const edge = SynchronousSignalCollectable.useRate(
1215
- Array.from({ length: 16 }, (_unused, index) => 2 + Math.cos(Math.PI * index)),
1216
- 16,
1217
- );
1218
- const bins = edge.oneSided();
1219
-
1220
- bins.length; // 9
1221
- round(bins[0].magnitude()); // 16
1222
- round(bins[8].magnitude()); // 8
1223
- round(edge.amplitude()[0]); // 2
1224
- round(edge.amplitude()[8]); // 1
1225
- ```
1226
-
1227
- Die rohen Beträge sind `16` und `8`, weil die Gewichte des Fensters sich zu acht summieren;
1228
- Division durch `count · CG = 8` macht daraus die kalibrierten Amplituden `2` und `1`. Würde man
1229
- die Randbins verdoppeln, läse man `4` und `2`, und der Fehler würde nie eine Ausnahme auslösen.
1230
-
1231
- ### 14.6 Zeit-Frequenz-Ansichten
1232
-
1233
- `shortTimeFourierTransform` und `spectrogram` rahmen die Aufzeichnung so wie die referenzierte
1234
- Kurzzeit-Transformation: an beiden Enden eine halbe Fensterlänge Nullen, dann ein mit Nullen
1235
- aufgefüllter Schwanz, sodass eine ganze Zahl von Hops bleibt. Der erste Frame ist auf den ersten
1236
- Abtastwert zentriert, der letzte erreicht den letzten Abtastwert, und Frame `m` ist bei
1237
- `m · hop / rate` Sekunden zentriert. Nur vollständige Frames zu behalten würde bis zu
1238
- `frameSize − 1` Werte verwerfen — bei 1000 Werten, Fenster 256 und Hop 128 sind das 104 Werte,
1239
- etwa ein Zehntel der Aufzeichnung.
1240
-
1241
- ```typescript
1242
- import { SynchronousSignalCollectable } from "semantic-typescript";
1243
-
1244
- const tone = SynchronousSignalCollectable.useRate(
1245
- Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 4 * index / 64)),
1246
- 64,
1247
- );
1248
- const spectrogram = tone.spectrogram(16n, 8n);
1249
-
1250
- spectrogram.size(); // 9
1251
- spectrogram.getFrameSize(); // 16
1252
- spectrogram.getBoundary(); // "zeros"
1253
- spectrogram.getTime(0); // 0
1254
- spectrogram.getTime(6); // 0.75
1255
- tone.shortTimeFourierTransform(16n, 8n).length; // 9
1256
- tone.spectralFlux(16n, 8n).length; // 8
1257
- ```
1258
-
1259
- Jede Spalte ist ein `Spectrum` über `frameSize` Bins, genau wie es `spectrum()` für denselben
1260
- Frame liefern würde, und `getTime()` meldet Frame-Mitten statt Frame-Anfänge.
1261
-
1262
- ### 14.7 Fenster, kohärenter Gewinn und COLA
1263
-
1264
- Jede Form hat zwei Varianten. Die symmetrische Variante teilt durch `count − 1`; die periodische
1265
- (DFT-gerade) Variante teilt durch `count`, einen Abtastwert weniger als eine volle Periode, sodass
1266
- die Fensterfunktion unter der periodischen Fortsetzung, die die DFT annimmt, stetig ist. Analyse
1267
- und Überlappungs-Addition brauchen die periodische Form; der Filterentwurf behält die
1268
- symmetrische.
1269
-
1270
- Der Unterschied ist messbar und löst nie einen Fehler aus. Bei fünfzig Prozent Überlappung
1271
- summiert sich das periodische Hann-Fenster an jeder Position zu eins; die symmetrische Form
1272
- welligt, um `0.84` Dezibel bei sechzehn Werten, `0.21` bei vierundsechzig und immer noch `0.013`
1273
- bei eintausendvierundzwanzig. Die Klasse legt den kohärenten Gewinn und die äquivalente
1274
- Rauschbandbreite des Fensters offen, das sie hält, sodass die Korrektur nie fest verdrahtet ist:
1275
- Ein symmetrisches Hann liest `0.4375` bei acht Werten, nicht `0.5`.
1276
-
1277
- ```typescript
1278
- import {
1279
- coherentGain, equivalentNoiseBandwidth, useFlatTopWindow,
1280
- useHannPeriodicWindow, useHannWindow,
1281
- } from "semantic-typescript";
1282
-
1283
- const round = (value: number): number => Number(value.toFixed(4));
1284
-
1285
- round(useHannWindow(1, 4)); // 0.75
1286
- round(useHannPeriodicWindow(1, 4)); // 0.5
1287
- coherentGain(useHannPeriodicWindow, 16); // 0.5
1288
- equivalentNoiseBandwidth(useHannPeriodicWindow, 16); // 1.5
1289
- useFlatTopWindow(0, 1024); // -0.000421
1290
- Array.from({ length: 1024 }, (_unused, i) => useFlatTopWindow(i, 1024)).filter(w => w < 0).length; // 578
1291
- ```
1292
-
1293
- Die Flat-Top-Zeile ist kein Fehler in der Tabelle. Ein Flat-Top-Fenster ist per Definition
1294
- teilweise negativ — hier sind 578 von 1024 Gewichten negativ —, und das Abschneiden auf null
1295
- würde die Hauptkeule stören und den geringen Scalloping-Verlust zerstören, für den das Fenster
1296
- existiert. Die Bibliothek gibt die Reihe unverändert zurück.
1297
-
1298
- ### 14.8 Fallstricke
1299
-
1300
- **Nullen-Auffüllen interpoliert, es löst nie auf.** Eine Transformationslänge über der
1301
- Abtastwertzahl tastet dieselbe DTFT dichter ab: Die Peak-*Position* wird feiner und der
1302
- Bin-Abstand wird `rate / size`, aber ob zwei Komponenten trennbar sind, bestimmen weiterhin
1303
- Fensterlänge und -form. Ein Ton genau zwischen zwei Bins liest `82.1093` und `80.8598` auf dem
1304
- ungefüllten Raster und `128` bei seiner eigenen Frequenz auf dem gefüllten.
1305
-
1306
- ```typescript
1307
- import { SynchronousSignalCollectable, useRectangularWindow } from "semantic-typescript";
1308
-
1309
- const round = (value: number): number => Number(value.toFixed(4));
1310
- const tone = SynchronousSignalCollectable.useRate(
1311
- Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 100.5 * index / 256)),
1312
- 256,
1313
- );
1314
- const flat = tone.spectrum(256n, useRectangularWindow);
1315
- const padded = tone.spectrum(1024n, useRectangularWindow);
1316
-
1317
- flat.getResolution().getHertz(); // 1
1318
- padded.getResolution().getHertz(); // 0.25
1319
- round(flat.magnitudeAt(100).get()); // 82.1093
1320
- round(flat.magnitudeAt(101).get()); // 80.8598
1321
- round(padded.magnitudeAt(402).get()); // 128
1322
- padded.frequencyAt(402).get().getHertz(); // 100.5
1323
- ```
1324
-
1325
- Zwei gleich starke Töne im Abstand von `0.4` Bins bleiben bei jeder Auffüllung verschmolzen: Mit
1326
- 256 Werten und einem Hann-Fenster erzeugt Auffüllen auf 8192 nur ein lokales Maximum, nicht zwei.
1327
-
1328
- **Die Hilbert-Transformation ist zirkulär.** `analyticSignal`, `envelope` und
1329
- `instantaneousPhase` behandeln den ersten und den letzten Abtastwert als Nachbarn, sodass eine
1330
- Aufzeichnung, deren Periode ihre Länge nicht teilt, an beiden Enden verunreinigt ist. Die Mitte
1331
- ist verlässlich, die Enden nicht.
1332
-
1333
- ```typescript
1334
- import { SynchronousSignalCollectable } from "semantic-typescript";
1335
-
1336
- const round = (value: number): number => Number(value.toFixed(4));
1337
- const edge = SynchronousSignalCollectable.useRate(
1338
- Array.from({ length: 512 }, (_unused, index) => Math.cos(2 * Math.PI * 8.3 * index / 512)),
1339
- 512,
1340
- );
1341
- const envelope = edge.envelope();
1342
-
1343
- round(envelope.slice(8, -8).reduce((sum, value) => sum + value, 0) / 496); // 1.0025
1344
- round(Math.max(...envelope.slice(0, 8))); // 1.2592
1345
- round(Math.min(...envelope.slice(-8))); // 0.1314
1346
- ```
1347
-
1348
- **Eine verschwindende Hüllkurve definiert keine Phase.** Wo die Hüllkurve zusammenbricht, kann
1349
- die Differenz zweier Rundungsrauschwinkel eine halbe Umdrehung betragen, was als halbe
1350
- Abtastrate gelesen wird. Das Gate ist das, was die Zahl sinnvoll hält, und der Standard ist
1351
- `1E-3` der Spitzenhüllkurve.
1352
-
1353
- ```typescript
1354
- import { SynchronousSignalCollectable } from "semantic-typescript";
1355
-
1356
- const round = (value: number): number => Number(value.toFixed(4));
1357
- const beat = SynchronousSignalCollectable.useRate(
1358
- Array.from({ length: 4096 }, (_unused, index) =>
1359
- Math.cos(2 * Math.PI * 100 * index / 2048) + Math.cos(2 * Math.PI * 102 * index / 2048)),
1360
- 2048,
1361
- );
1362
-
1363
- round(Math.max(...beat.instantaneousFrequency(2048, 0).map(Math.abs))); // 920.218
1364
- round(Math.max(...beat.instantaneousFrequency().map(Math.abs))); // 101
1365
- ```
1366
-
1367
- **Eine unnormierte Korrelation ist keine Ähnlichkeit.** Die rohe Summe `r[k]` wächst mit der
1368
- Aufzeichnungslänge, zwei Längen sind also nicht vergleichbar. Die Standardregel
1369
- `"coefficient"` teilt durch `r[0]` und hält jeden Wert in `[-1, 1]`; `"unbiased"` teilt durch
1370
- `count − k` und zahlt mit einer Varianz, die mit dem Lag wächst.
1371
-
1372
- ```typescript
1373
- import { SynchronousSignalCollectable } from "semantic-typescript";
1374
-
1375
- const round = (value: number): number => Number(value.toFixed(4));
1376
- const periodic = SynchronousSignalCollectable.useRate(
1377
- Array.from({ length: 1024 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 1024)),
1378
- 1024,
1379
- );
1380
- const rho = periodic.autocorrelation(200n);
1381
-
1382
- round(rho[0]); // 1
1383
- round(rho[128]); // 0.875
1384
- round(rho[64]); // -0.9375
1385
- round(periodic.autocorrelation(200n, "none")[0]); // 512
1386
- ```
1387
-
1388
- Eine weitere Ablesung verdient eine Warnung: `spectralFlux` normiert jeden Frame vor dem
1389
- Vergleich, eine reine Pegeländerung erzeugt also null Fluss. Sein erster und letzter Wert sind
1390
- Randeffekte der Null-Grenze, keine Einsätze.
1391
-
1392
- ---
1393
-
1394
- <a id="15-license"></a>
1395
- ## 15. Lizenz
1396
-
1397
- MIT © Eloy Kim
1
+ # **Semantic-TypeScript**
2
+
3
+ **Datenfluss, indexiert.** Ihre Daten, präzise unter Kontrolle.
4
+
5
+ [English](./readme.md) · [简体中文](./readme.cn.md) · [繁體中文](./readme.tw.md) · [日本語](./readme.jp.md) · [한국어](./readme.kr.md) · Deutsch · [Français](./readme.fr.md) · [Español](./readme.es.md) · [Русский](./readme.ru.md)
6
+
7
+ Eine typsichere Stream-Verarbeitungsbibliothek, die **JavaScript-Generatoren**, **Java Stream** und
8
+ **indexbasierte Datenadressierung** vereint. Jedes Element trägt einen `bigint`-Index, jede Pipeline ist träge, und
9
+ jede Terminaloperation muss ausdrücklich aktiviert werden.
10
+
11
+ ```bash
12
+ npm install semantic-typescript
13
+ ```
14
+
15
+ - Keine Laufzeitabhängigkeiten, reines ESM, liefert eigene Typen mit.
16
+ - 684 Regressionstests: `npm test`.
17
+ - Vollständige Änderungshistorie: [CHANGELOG.md](./CHANGELOG.md).
18
+
19
+ ---
20
+
21
+ ## Table of Contents
22
+
23
+ 1. [Entwurfsphilosophie](#1-design-philosophy)
24
+ 2. [Architektur](#2-architecture)
25
+ 3. [Schnellstart](#3-quick-start)
26
+ 4. [Fabrikfunktionen](#4-factory-functions)
27
+ 5. [Transformationsoperatoren](#5-transformation-operators)
28
+ 6. [Collectables und Terminaloperationen](#6-collectables-and-terminal-operations)
29
+ 7. [Statistik und Fenster](#7-statistics-and-windows)
30
+ 8. [Das Indexmodell](#8-the-index-model)
31
+ 9. [Das asynchrone Modell](#9-the-asynchronous-model)
32
+ 10. [Periodische Analyse: Ringe, Felder und Scores](#10-periodic-analysis-rings-fields-and-scores)
33
+ 11. [Hilfsfunktionen](#11-utility-helpers)
34
+ 12. [Leistung und Fallstricke](#12-performance-and-pitfalls)
35
+ 13. [Phasenringe: ein exakter Zyklus](#13-phase-rings)
36
+ 14. [Signalanalyse: Fenster, Spektren und Zeit](#14-signal-analysis)
37
+ 15. [Lizenz](#15-license)
38
+
39
+ ---
40
+
41
+ <a id="1-design-philosophy"></a>
42
+ ## 1. Entwurfsphilosophie
43
+
44
+ Drei Ideen, eine Pipeline:
45
+
46
+ **Index ist Daten.** Jedes Element wird über einen `bigint` adressiert. Umkehrung, Rotation, Mischen
47
+ (Shuffling) und Aufteilung sind Index-Arithmetik, keine Pufferoperationen. Nichts wird materialisiert,
48
+ bis ein Collectable es anfordert.
49
+
50
+ **Trägheit ist die Vorgabe.** Eine Fabrik umschließt einen Generator; ein Operator umschließt einen
51
+ weiteren Generator. Kein Element wird angefordert, bis eine Terminaloperation läuft, daher ist eine
52
+ unendliche Quelle völlig zulässig, solange etwas Nachgelagertes sie stoppt.
53
+
54
+ **Terminals sind explizit.** `map` gibt ein `Semantic` zurück, kein Array. Werte erscheinen erst, wenn
55
+ Sie ein Collectable benennen — `toUnordered()`, `toOrdered()`, `toNumericStatistics()`, `toWindow()` —
56
+ und jedes davon sagt, was es kostet und welche Reihenfolge es erzeugt.
57
+
58
+ Dieselbe Disziplin gilt für die mathematische Hälfte der Bibliothek. Ein `Complex`, ein `Angle` oder
59
+ eine `Frequency` ist ein unveränderlicher Wert, dessen Invarianten bei der Konstruktion geprüft
60
+ werden; ein `Optional` ersetzt `null`; und ein Phasenring sagt genau, wie viele Elemente er hält,
61
+ welche Periode er hat und wie ausgewogen er ist.
62
+
63
+ ---
64
+
65
+ <a id="2-architecture"></a>
66
+ ## 2. Architektur
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
+ | Schicht | Modul | Verantwortung |
76
+ | :--- | :--- | :--- |
77
+ | Quellen | `synchronous/collectors`, `asynchronous/collectors` | Umschließt ein Iterable, einen Generator, ein Promise, einen Blob, ein Event-Target |
78
+ | Pipelines | `synchronous/semantic`, `asynchronous/semantic` | Träge Operatoren und das Umschreiben von Indizes |
79
+ | Collectables | `synchronous/semantic`, `asynchronous/semantic` | Geordnete, ungeordnete, Statistik-, Fenster- und eigene Terminals |
80
+ | Collectors | `synchronous/collector`, `asynchronous/collector` | Die Fold-Primitive, aus denen jedes Terminal aufgebaut ist |
81
+ | Mathematik | `mathematics/*` | Winkel, Komplexe, Frequenzen, Spektren, Fenster, Schätzer, Epizyklen, Phasenringe |
82
+ | Strukturen | `structure/*` | Listen, Sets, Maps, Bäume, Forste, Ringe, Felder |
83
+ | Fehler | `structure/error` | Die typisierte `SemanticError`-Hierarchie und ihre Brand-Guards |
84
+
85
+ Die synchrone und die asynchrone Familie teilen ein Vokabular: dieselben Operatornamen, dieselbe
86
+ Indexsemantik, dieselben Collectables. Die asynchronen Varianten geben lediglich Promises zurück und
87
+ stellen Schreibzugriffe in eine Warteschlange, sodass ein nach einem Schreibzugriff ausgelöster
88
+ Lesezugriff ihn stets beobachtet.
89
+
90
+ ### 2.1 Was dieses Release verschoben und gebrochen hat
91
+
92
+ Drei Module haben ihren Ort oder ihre Form gewechselt, und eine Reihe von Standardwerten hat die Bedeutung geändert. Jedes alte Verhalten bleibt über eine explizite Option erreichbar; nichts wurde ohne Ersatz entfernt.
93
+
94
+ | Änderung | Vorher | Jetzt |
95
+ | :--- | :--- | :--- |
96
+ | Forste | `structure/tree/rootedForest`, `structure/tree/unionFindForest` | `structure/forest/rootedForest`, `structure/forest/unionFindForest`; `tree/index.ts` re-exportiert sie nicht mehr |
97
+ | Fehler | ad-hoc native Fehler | eine Hierarchie aus fünfzehn Klassen in `structure/error.ts`, mit je einem Brand-Guard pro Klasse und dem Alias `SemanticRangeError` |
98
+ | Schätzer-Optionen | feste Definitionen | `mathematics/quantile.ts` exportiert `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator` und `VarianceOptions` |
99
+ | `quantile` | Hyndman–Fan Typ 1 | Typ 7 — R `quantile`, NumPy `linear`, Excel `PERCENTILE.INC` |
100
+ | `skewness` / `kurtosis` | `g1` / `g2` | die bias-korrigierten `G1` / `G2` |
101
+ | `variance` / `standardDeviation` | `ddof = 0` | `ddof = 1`, in ein `Optional` gehüllt |
102
+ | `decibel` | eine `20·log10`-Funktion | entfernt; `amplitudeDecibel` (20·log10) und `powerDecibel` (10·log10) |
103
+ | `Spectrum.flatness()` | Beträge über beide Hälften | Leistungen über die einseitige Hälfte |
104
+ | `Spectrum.bandEnergy()` | rohe zweiseitige Summe | standardmäßig einseitig, mit Faltung der konjugierten Hälfte |
105
+ | `shortTimeFourierTransform` / `spectrogram` | symmetrisches Hann, nur vollständige Frames | periodisches Hann, `boundary: "zeros"`, `padded: true` |
106
+
107
+ Die Kapitel 6, 7 und 10 dokumentieren die Ersatzlösungen; die gemessenen Folgen stehen in [CHANGELOG.md](./CHANGELOG.md).
108
+
109
+ ---
110
+
111
+ <a id="3-quick-start"></a>
112
+ ## 3. Schnellstart
113
+
114
+ ```typescript
115
+ import {
116
+ useOf, useFrom, useRange, useText, useStringify, usePromise
117
+ } from "semantic-typescript";
118
+
119
+ // Numeric aggregation
120
+ const sum: number = useOf(10, 20, 30, 40)
121
+ .map(n => n * 2)
122
+ .toNumericStatistics()
123
+ .summate(); // 200
124
+
125
+ // BigInt aggregation
126
+ const bigSum: bigint = useOf(10n, 20n, 30n, 40n)
127
+ .map(n => n * 2n)
128
+ .toBigintStatistics()
129
+ .summate(); // 200n
130
+
131
+ // Reordering is index arithmetic — collect with toOrdered() for it to take effect
132
+ const reversed: number[] = useFrom([1, 2, 3, 4, 5]).reverse().toOrdered().toArray(); // [5,4,3,2,1]
133
+ const rotated: number[] = useFrom([1, 2, 3, 4, 5]).translate(2).toOrdered().toArray(); // [4,5,1,2,3]
134
+
135
+ // Lazy evaluation over a large range
136
+ const multiples: bigint[] = useRange(0n, 1_000_000n)
137
+ .filter(n => n % 17n === 0n)
138
+ .limit(10n)
139
+ .toUnordered()
140
+ .toArray(); // [0n,17n,…,153n]
141
+
142
+ // Unicode-safe string processing (code points, not UTF-16 units)
143
+ useText("semantic 👍").toUnordered().forEach(character => console.log(character));
144
+
145
+ // Circular-safe serialisation
146
+ const cyclic: any = { a: 1 }; cyclic.self = cyclic;
147
+ useStringify(cyclic); // '{"a":1}'
148
+
149
+ // Terminals hand the elements to real structures, not only to arrays
150
+ useOf(3, 1, 4, 1, 5).toCollectable().toOrderedSet().toArray(); // [1,3,4,5]
151
+ useFrom([3, 1, 4, 1, 5]).toCollectable().toBinarySearchTree().size(); // 5n
152
+
153
+ // Async sources
154
+ const total = await usePromise(Promise.resolve(7)).toNumericStatistics().summate(); // 7
155
+ ```
156
+
157
+ ---
158
+
159
+ <a id="4-factory-functions"></a>
160
+ ## 4. Fabrikfunktionen
161
+
162
+ Die Konstruktion ist stets O(1) — eine Fabrik umschließt lediglich einen Generator. Die Tabelle nennt
163
+ die Kosten für den *Verbrauch* von `n` Elementen sowie den zusätzlichen Speicher, den die Quelle selbst
164
+ benötigt.
165
+
166
+ | Fabrik | Signatur (Ergebnis) | Zeit (n verbrauchen) | Speicher (Quelle) | Anmerkungen |
167
+ | :--- | :--- | :--- | :--- | :--- |
168
+ | `useOf` | `useOf<E>(...elements) → SynchronousSemantic<E>` | O(n) | O(n) | Indizes sind 0..n−1; das Spread-Argument ist bereits materialisiert |
169
+ | `useFrom` | `useFrom<E>(Iterable<E>) → SynchronousSemantic<E>`<br>`useFrom<E>(AsyncIterable<E>) → AsynchronousSemantic<E>` | O(n) | O(1) | Umschließt jedes Iterable; beim Sammeln einmal iteriert |
170
+ | `useRange` | `useRange<N>(start, end, step?) → SynchronousSemantic<N>` | O(n) | O(1) | `end` ist exklusiv; `step` muss von Null verschieden sein; `number` und `bigint` funktionieren beide |
171
+ | `useFill` | `useFill<E>(element \| supplier, count) → SynchronousSemantic<E>` | O(n) | O(1) | Ein Supplier wird pro Element einmal aufgerufen |
172
+ | `useEmpty` | `useEmpty<E>() → SynchronousSemantic<E>` | O(1) | O(1) | Neutralelement für `concat` |
173
+ | `useText` | `useText(text, delimiter?)` | O(n) | O(n) für den Text | Iteriert über **Code-Punkte**, sodass Emoji und CJK einzelne Elemente bleiben |
174
+ | `useAttribute` | `useAttribute<T>(object)` | O(v + e) | O(d + v) | Durchläuft verschachtelte Objekte rekursiv mit Zyklusschutz |
175
+ | `useGenerate` | `useGenerate<E>(supplier, interrupt)` | O(n) × Supplier-Kosten | O(1) | **Muss** ein stoppendes `interrupt` besitzen oder ein nachgelagertes `limit` |
176
+ | `useIterate` | `useIterate<E>(Iterable<E>)` / `(AsyncIterable<E>)` | O(n) pro Sammlung | O(1) | Spielt den Iterator bei jeder Sammlung erneut ab |
177
+ | `useInterval` | `useInterval(period, delay?) → AsynchronousSemantic<number>` | n × period (Echtzeit) | O(1) | Gibt die verstrichene Zeit aus |
178
+ | `usePromise` | `usePromise<T>(promise) → AsynchronousSemantic<T>` | O(1) | O(1) | Ein einzelnes Element am Index `0n` |
179
+ | `useBlob` | `useBlob(blob, chunk?) → AsynchronousSemantic<Uint8Array>` | O(blob size) | O(chunk) pro Kopie | Standard-Chunk ist 64 KiB |
180
+ | `useSubscription` | `useSubscription(target, subscriber, event) → AsynchronousSemantic<Event>` | ein Element pro Ereignis | O(1) | `limit()` / `takeWhile()` definieren die Abmelde-Grenze |
181
+
182
+ ```typescript
183
+ useOf(1, 2, 3).toUnordered().toArray(); // [1,2,3]
184
+ useFrom(new Set([1, 2, 2])).toUnordered().toArray(); // [1,2]
185
+ useRange(0, 5).toUnordered().toArray(); // [0,1,2,3,4]
186
+ useRange(10n, 0n, -5n).toUnordered().toArray(); // [10n,5n]
187
+ useFill("x", 3n).toUnordered().toArray(); // ["x","x","x"]
188
+ useText("A😊B").toUnordered().toArray(); // ["A","😊","B"]
189
+ useGenerate(() => 7, (_e, index) => index >= 3n).toUnordered().toArray(); // [7,7,7]
190
+ ```
191
+
192
+ ---
193
+
194
+ <a id="5-transformation-operators"></a>
195
+ ## 5. Transformationsoperatoren
196
+
197
+ Jeder Operator gibt ein neues `Semantic` zurück; keiner berührt ein Element, bis ein Collectable läuft.
198
+ Die Zeit ist der zusätzliche Aufwand **pro Element**; der Speicher ist das, was der Operator während
199
+ des Pipeline-Laufs hält.
200
+
201
+ | Operator | Zweck | Indexwirkung | Zeit | Speicher |
202
+ | :--- | :--- | :--- | :--- | :--- |
203
+ | `map(mapper)` | 1:1-Transformation | weitergeleitet | O(1) | O(1) |
204
+ | `filter(predicate)` | passende Elemente behalten | weitergeleitet | O(1) | O(1) |
205
+ | `flat(mapper)` / `flatMap(mapper)` | 1:N-Entfaltung | neu indiziert ab 0 | O(1) pro Ausgabe | O(1) |
206
+ | `distinct(keyExtractor?)` | wiederholte Schlüssel verwerfen, den ersten behalten | weitergeleitet | O(1) im Mittel | O(n) |
207
+ | `peek(consumer)` | Seiteneffekt, Element passiert hindurch | weitergeleitet | O(1) | O(1) |
208
+ | `limit(count)` | die ersten *n* nehmen | neu indiziert ab 0 | O(1) | O(1) |
209
+ | `skip(count)` | die ersten *n* verwerfen | weitergeleitet | O(1) | O(1) |
210
+ | `sub(start, end)` | positionsbasierter Ausschnitt `[start, end)` | weitergeleitet | O(1) | O(1) |
211
+ | `takeWhile(predicate)` | solange nehmen, wie das Prädikat zutrifft | weitergeleitet | O(1) | O(1) |
212
+ | `dropWhile(predicate)` | solange verwerfen, wie das Prädikat zutrifft | neu indiziert ab 0 | O(1) | O(1) |
213
+ | `concat(other)` | ein Semantic oder Iterable anhängen | setzt den Index fort | O(1) | O(1) |
214
+ | `redirect(redirector)` | einen neuen Index nach Belieben berechnen | **Ihrer** | O(1) | O(1) |
215
+ | `reverse()` | `redirect((_e, i) => -i - 1n)` | negiert und verschoben | O(1) | O(1) |
216
+ | `translate(offset \| mapper)` | jeden Index verschieben | `i + offset` | O(1) | O(1) |
217
+ | `shuffle(mapper?)` | den Index randomisieren | zufällig | O(1) | O(1) |
218
+ | `pipe(conversion)` | den Generator an eine eigene Funktion übergeben | nach Belieben | Ihrer | Ihrer |
219
+ | `semantic()` | ein Collectable zurück in ein Semantic heben | unverändert | O(1) | O(1) |
220
+
221
+ **`reverse`, `translate` und `shuffle` schreiben nur Indizes um.** Nichts wird gepuffert oder rotiert,
222
+ bis `toOrdered()` die Indizes modulo der Elementanzahl normalisiert:
223
+
224
+ ```typescript
225
+ useFrom([1, 2, 3, 4, 5]).reverse().toOrdered().toArray(); // [5,4,3,2,1]
226
+ useFrom([1, 2, 3, 4, 5]).translate(2).toOrdered().toArray(); // [4,5,1,2,3] (rotation)
227
+ useFrom([1, 2, 3, 4, 5]).translate(7).toOrdered().toArray(); // [4,5,1,2,3] (7 ≡ 2 mod 5)
228
+ useFrom([1, 2, 3]).shuffle((_e, i) => -i - 1n).toOrdered().toArray(); // [3,2,1]
229
+ ```
230
+
231
+ ---
232
+
233
+ <a id="6-collectables-and-terminal-operations"></a>
234
+ ## 6. Collectables und Terminaloperationen
235
+
236
+ ### 6.1 Collectables
237
+
238
+ Das Collectable besitzt sowohl die **Reihenfolge** als auch die **Index-Buchhaltung**. `n` ist die
239
+ Elementanzahl.
240
+
241
+ | Collectable | Struktur | Zeit | Reihenfolge | Verwenden wenn |
242
+ | :--- | :--- | :--- | :--- | :--- |
243
+ | `toUnordered()` | `Map<bigint, E>` mit Index als Schlüssel | O(n) | Einfügungsreihenfolge | maximaler Durchsatz; rohe Indizes bleiben unangetastet |
244
+ | `toOrdered()` | nach Elementindex sortiertes Array | O(n log n) | nach normalisiertem Index | Umkehrung/Rotation muss wirksam werden |
245
+ | `toOrdered(comparator)` | nach Ihrem Comparator sortiert | O(n log n) | nach Ihrem Comparator | sortierte Ansichten |
246
+ | `toNumericStatistics()` | wertsortierter Puffer | O(n log n) | nach Wert | Statistik über `number` |
247
+ | `toBigintStatistics()` | wertsortierter Puffer | O(n log n) | nach Wert | Statistik über `bigint` |
248
+ | `toWindow()` | wertsortierter Puffer | O(n log n) | nach Wert | `slide` / `tumble` |
249
+ | `toCollectable(mapper)` | was auch immer der Mapper zurückgibt | — | — | eigene Collectables |
250
+
251
+ ### 6.2 Terminals
252
+
253
+ Jedes Terminal lebt am **Collectable**, nicht am `Semantic`. Genau das bedeutet „Terminals sind explizit" in der Praxis: Eine Pipeline erzeugt nichts, bis ein Collectable benannt wird, und `toCollectable()` ohne Mapper ist die günstigste Benennung.
254
+
255
+ | Terminal | Liefert | Zeit | Anmerkungen |
256
+ | :--- | :--- | :--- | :--- |
257
+ | `toArray()` / `toSet()` | `Array<E>` / `Set<E>` | O(n) | entleert den Puffer; `toSet` dedupliziert nach Identität |
258
+ | `toMap(key, value?)` | `Map<K, V>` | O(n) im Mittel | letzter Schreibzugriff gewinnt |
259
+ | `count()` / `isEmpty()` | `bigint` / `boolean` | O(1) | der Puffer weiß es bereits |
260
+ | `forEach(action)` | `void` | O(n) | `action(element, index)` |
261
+ | `join(…)` | `string` | O(n) | `join()`, `join(delimiter)`, `join(prefix, delimiter, suffix)` |
262
+ | `reduce(…)` | `Optional<E>` / `E` / `R` | O(n) | leerer Stream → leeres `Optional` |
263
+ | `group(classifier)` / `groupBy(key, value)` | `Map<K, E[]>` | O(n) im Mittel | — |
264
+ | `partition(count)` | `Array<Array<E>>` | O(n) | Round-Robin nach Index |
265
+ | `partitionBy(classifier)` | `Array<Array<E>>` | O(n) | die Bucket-Nummer stammt vom Classifier |
266
+ | `anyMatch` / `allMatch` / `nonMatch` | `boolean` | O(n) im schlimmsten Fall, bricht früh ab | — |
267
+ | `findAny` / `findFirst` / `findLast` | `Optional<E>` | O(1)–O(n) | positionsbasiert in materialisierter Reihenfolge |
268
+ | `findAt(k)` / `findNegativeAt(k)` | `Optional<E>` | O(k) / O(n−k) | zählt von vorne / von hinten |
269
+ | `findMaximum(cmp?)` / `findMinimum(cmp?)` | `Optional<E>` | O(n) | Standard ist ein struktureller Vergleich |
270
+ | `frequency()` | `Map<E, bigint>` | O(n) im Mittel | nur Statistik-Collectables |
271
+ | `write(stream, accumulator?)` | `WritableStream<S>` | O(n) | IO-artiges Terminal |
272
+ | `collect(collector)` | `R` | O(n) | der generische Fold hinter jedem obigen Terminal |
273
+
274
+ ### 6.3 Struktur-Collectors
275
+
276
+ `toArray`, `toSet` und `toMap` beenden eine Pipeline in einem JavaScript-Container. Zwölf weitere Terminals beenden sie in einer der Strukturen der Bibliothek selbst, sodass das Ergebnis die Invarianten trägt, die diese Struktur garantiert, statt derer, die ein Array zufällig hat. Sie stehen an jedem Collectable zur Verfügung, auch am nackten Collectable aus `toCollectable()`.
277
+
278
+ | Terminal | Liefert | Reihenfolge | Kosten |
279
+ | :--- | :--- | :--- | :--- |
280
+ | `toArray()` | `Array<E>` | Durchlaufreihenfolge | O(n) |
281
+ | `toSet()` | `Set<E>` | Einfügungsreihenfolge, SameValueZero | O(n) im Mittel |
282
+ | `toMap(key, value?)` | `Map<K, V>` | Einfügungsreihenfolge; der letzte Schreibzugriff gewinnt | O(n) im Mittel |
283
+ | `toOrderedSet(comparator?)` | `SynchronousOrderedSet<E>` | comparator-aufsteigend | O(n²) im schlimmsten Fall |
284
+ | `toUnorderedSet()` | `SynchronousUnorderedSet<E>` | nicht spezifiziert (Hash-Buckets) | O(n) erwartet |
285
+ | `toOrderedMap(key, value?)` | `SynchronousOrderedMap<K, V>` | Einfügungsreihenfolge; der letzte Schreibzugriff gewinnt | O(n) erwartet |
286
+ | `toUnorderedMap(key, value?)` | `SynchronousUnorderedMap<K, V>` | nicht spezifiziert | O(n) erwartet |
287
+ | `toList()` | `SynchronousList<E>` | Durchlaufreihenfolge, Duplikate bleiben | O(n) |
288
+ | `toRing(span?)` | `SynchronousRing<E>` | die Durchlaufreihenfolge legt die Ringpositionen fest | O(n) |
289
+ | `toLRUSet(capacity)` | `SynchronousUnorderedLRUSet<E>` | nach Aktualität; nur die letzten `capacity` überleben | O(n²) im schlimmsten Fall |
290
+ | `toLRUMap(capacity, key, value?)` | `SynchronousUnorderedLRUMap<K, V>` | nach Aktualität | O(n) erwartet |
291
+ | `toSkipSet(comparator?)` | `SynchronousSkipSet<E>` | comparator-aufsteigend, navigierbar | O(n²) im schlimmsten Fall |
292
+ | `toSkipMap(key, value?)` | `SynchronousSkipMap<K, V>` | nach dem Schlüssel-Comparator, navigierbar | O(n) erwartet |
293
+ | `toBinarySearchTree(comparator?)` | `SynchronousBinarySearchTree<E>` | comparator-aufsteigend, höhenbalanciert | O(n log n) |
294
+ | `toRootedForest()` | `SynchronousRootedForest<E>` | die Durchlaufreihenfolge wird zur Struktur | O(n) |
295
+
296
+ Der Ergebnistyp folgt dem Elementtyp des Collectables selbst, daher braucht kein Terminal ein Typargument: Ein `SynchronousCollectable<E>` liefert direkt `Array<E>`, `SynchronousList<E>` oder `SynchronousOrderedSet<E>`. Es bleiben nur die fachlichen Argumente — der Comparator von `toOrderedSet` / `toSkipSet` / `toBinarySearchTree`, die Kapazität des LRU-Paars, die Spanne von `toRing` und die Schlüssel- und Wert-Extraktoren der Map-Familie.
297
+
298
+ Ein Ring ist ein Set, daher behalten wiederholte Elemente ihre erste Position; alle übrigen Strukturen oben behalten Duplikate, sofern ihr eigener Vertrag nichts anderes sagt (`toOrderedSet`, `toUnorderedSet` und `toSkipSet` sind Sets; `toBinarySearchTree` und `toList` nicht).
299
+
300
+ ```typescript
301
+ const numbers = useOf(3, 1, 4, 1, 5).toCollectable();
302
+
303
+ numbers.toOrderedSet().toArray(); // [1,3,4,5]
304
+ numbers.toUnorderedSet().size(); // 4n — the duplicate 1 collapses
305
+ numbers.toList().toArray(); // [3,1,4,1,5]
306
+ numbers.toSkipSet().first().get(); // 1 — navigable by comparator
307
+ numbers.toBinarySearchTree().size(); // 5n — duplicates retained
308
+ numbers.toRootedForest().getRoots().length; // 1
309
+ useOf(1, 2, 3).toCollectable().toRing().phaseAt(3n).get().toDegreeValue(); // 0
310
+ useOf(3, 1, 3).toCollectable().toOrderedMap(n => n, n => `v${n}`).get(3).get(); // "v3"
311
+ ```
312
+
313
+ Die asynchronen Collectables bieten dieselben Terminals und lösen jedes in einem Promise auf; die Aufrufform gewinnt also nur ein `await` hinzu.
314
+
315
+ ---
316
+
317
+ <a id="7-statistics-and-windows"></a>
318
+ ## 7. Statistik und Fenster
319
+
320
+ ### 7.1 Die Statistik-Oberfläche
321
+
322
+ ```typescript
323
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().summate(); // 14
324
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().average(); // 2.8
325
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().median(); // 3
326
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().mode(); // 1
327
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().variance().get(); // 3.2 (n − 1)
328
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().standardDeviation().get(); // 1.7889 (n − 1)
329
+ ```
330
+
331
+ Beide Statistik-Familien bieten dieselbe Oberfläche — `summate`, `average`, `median`, `mode`, `variance`, `standardDeviation`, `frequency`, `findMaximum`, `findMinimum` — mit `bigint`-Arithmetik für `toBigintStatistics()`. Die Streuungsmaße und die Quantil-Familie geben `Optional` zurück, weil eine Stichprobe zu klein sein kann, um sie zu definieren; entpacken Sie sie mit `.get()`, `.orElse(…)` oder `.isPresent()`.
332
+
333
+ ### 7.2 Schätzer und Standardwerte
334
+
335
+ Die Standardwerte sind die Definitionen, die R, NumPy, pandas und Excel verwenden; ein hier berechneter Wert lässt sich also ohne Korrekturfaktor mit diesen Werkzeugen vergleichen.
336
+
337
+ | Statistik | Standard | Vorher | Alten Wert zurückholen mit |
338
+ | :--- | :--- | :--- | :--- |
339
+ | `quantile(probability)` | Hyndman–Fan Typ 7 | Typ 1 | `{ method: "type-1" }` |
340
+ | `interquartileRange()` | Hyndman–Fan Typ 7 | Typ 1 | `{ method: "type-1" }` |
341
+ | `skewness()` | bias-korrigiertes `G1` | `g1` | `{ estimator: "g1" }` |
342
+ | `kurtosis()` | bias-korrigiertes Excess-`G2` | `g2` | `{ estimator: "g2" }` |
343
+ | `variance()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
344
+ | `standardDeviation()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
345
+
346
+ Alle neun Hyndman–Fan-Typen stehen als `"type-1"` … `"type-9"` zur Verfügung, und `{ excess: false }` addiert drei wieder zur Kurtosis hinzu und meldet das nicht-exzessive vierte standardisierte Moment. Die Optionstypen — `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator`, `VarianceOptions` — werden aus `mathematics/quantile.ts` exportiert.
347
+
348
+ ```typescript
349
+ const sample = useOf(1, 2, 3, 4).toNumericStatistics();
350
+
351
+ sample.variance().get(); // 1.6667 (5/3, ddof = 1)
352
+ sample.variance({ ddof: 0 }).get(); // 1.25
353
+ sample.standardDeviation().get(); // 1.2910
354
+ sample.skewness().get(); // 0
355
+ sample.kurtosis().get(); // -1.2 (G2)
356
+ sample.kurtosis({ estimator: "g2" }).get(); // -1.36
357
+ sample.kurtosis({ excess: false }).get(); // 1.8
358
+
359
+ const quartiles = useOf(1, 2, 3, 4, 5, 6, 7, 8).toNumericStatistics();
360
+ quartiles.quantile(0.75).get(); // 6.25 (type 7)
361
+ quartiles.quantile(0.75, { method: "type-1" }).get(); // 6 (nearest rank)
362
+ quartiles.interquartileRange().get(); // 3.5
363
+ ```
364
+
365
+ `G1` und `G2` sind unter drei bzw. vier Beobachtungen undefiniert, und die Stichprobenvarianz sowie die Standardabweichung sind unter `ddof = 1` unter zwei Beobachtungen undefiniert. Das wird als leeres `Optional` gemeldet statt als irreführende Null:
366
+
367
+ ```typescript
368
+ useOf(1, 2, 3).toNumericStatistics().kurtosis().isEmpty(); // true (G2 needs four observations)
369
+ useOf(1, 2).toNumericStatistics().skewness().isEmpty(); // true (G1 needs three)
370
+ useOf(1).toNumericStatistics().variance().isEmpty(); // true (ddof = 1 needs two)
371
+ useOf(1).toNumericStatistics().variance({ ddof: 0 }).get(); // 0
372
+ ```
373
+
374
+ Die bigint-Ansichten akzeptieren dieselben Optionen und runden jedes Ergebnis auf ein ganzzahliges `bigint` zurück.
375
+
376
+ ### 7.3 Fenster
377
+
378
+ Fenster legen die gepufferten Elemente erneut als Pipeline von Pipelines offen, sodass ein Fenster konsumiert werden kann, ohne es zuerst in ein Array zu materialisieren. Beide Formen enthalten ein letztes partielles Fenster.
379
+
380
+ ```typescript
381
+ useFrom([1, 2, 3, 4]).toWindow().slide(2n, 1n).toUnordered().toArray()
382
+ .map(window => window.toUnordered().toArray()); // [[1,2],[2,3],[3,4],[4]]
383
+
384
+ useFrom([1, 2, 3, 4]).toWindow().tumble(2n).toUnordered().toArray()
385
+ .map(window => window.toUnordered().toArray()); // [[1,2],[3,4]]
386
+ ```
387
+
388
+ ---
389
+
390
+ <a id="8-the-index-model"></a>
391
+ ## 8. Das Indexmodell
392
+
393
+ Ein Index ist ein `bigint`, der jedem Element anhängt. Operatoren **leiten** ihn **weiter**,
394
+ **indizieren** ihn **von Null neu** oder lassen Sie ihn **berechnen**.
395
+
396
+ Das geordnete Sammeln normalisiert jeden Index modulo der Elementanzahl und sortiert dann nach dem
397
+ Ergebnis. Deshalb ist `translate(7)` auf fünf Elementen gleich `translate(2)`: `7 ≡ 2 (mod 5)`.
398
+
399
+ Das ungeordnete Sammeln behält jeden Index, den die Pipeline erzeugt hat, und gibt Elemente in
400
+ Ankunftsreihenfolge zurück. Negative Indizes sind zulässig und sortieren nach der Normalisierung
401
+ schlicht vor den nicht-negativen.
402
+
403
+ ---
404
+
405
+ <a id="9-the-asynchronous-model"></a>
406
+ ## 9. Das asynchrone Modell
407
+
408
+ `AsynchronousSemantic` spiegelt die synchrone Oberfläche und gibt Promises zurück. Schreibzugriffe
409
+ werden in eine Warteschlange gestellt, sodass ein nach einem Schreibzugriff ausgelöster Lesezugriff ihn
410
+ stets beobachtet.
411
+
412
+ ```typescript
413
+ const ring = new AsynchronousRing([1, 2]);
414
+ const added = ring.add(3); // queued
415
+ const sampled = ring.elementAt(2n); // observes the write above
416
+ await added;
417
+ (await sampled).get().element; // 3
418
+ ```
419
+
420
+ Fabriken, die von Natur aus asynchron sind — `usePromise`, `useBlob`, `useInterval`,
421
+ `useSubscription` — geben direkt `AsynchronousSemantic` zurück, und `useFrom` / `useIterate` stufen
422
+ sich selbst hoch, wenn ihnen ein `AsyncIterable` übergeben wird.
423
+
424
+ ---
425
+
426
+ <a id="10-periodic-analysis-rings-fields-and-scores"></a>
427
+ ## 10. Periodische Analyse: Ringe, Felder und Scores
428
+
429
+ Die mathematische Hälfte der Bibliothek behandelt einen Zyklus als erstklassiges Objekt und nicht als
430
+ Nachschlagetabelle. Drei Konstruktionen wirken zusammen.
431
+
432
+ ### 10.1 Ein Ring ist eine ganze Zahl
433
+
434
+ Ein `Ring` ist ein **n-facher Ring**: Elementanzahl, Positionsperiode und die Anzahl unterscheidbarer
435
+ Phasen sind dieselbe ganze Zahl `n`. Die Phase einer Position wird aus ihrem exakten Rest berechnet,
436
+ sodass der Ring bis auf das Bit periodisch ist:
437
+
438
+ ```typescript
439
+ import { SynchronousRing } from "semantic-typescript";
440
+
441
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
442
+ ring.phaseAt(0n).get().toDegreeValue(); // 0
443
+ ring.phaseAt(1n).get().toDegreeValue(); // 60
444
+ ring.phaseAt(6n).get().getValue() === ring.phaseAt(0n).get().getValue(); // true, exactly
445
+ ring.purity(); // 1
446
+ ring.isBalanced(); // true
447
+ ring.sequences().map(z => z.magnitude()); // [0, 6, 0, 0, 0, 0]
448
+ ```
449
+
450
+ `frequency` ist die **umgebende** Rotationsrate, kein Phasenschritt zwischen Positionen: Die Umgebung
451
+ bewegt jeden Phasor um denselben Winkel, kann das intrinsische Muster also nie brechen. `span` legt
452
+ fest, wie viele Umdrehungen der Ring einnimmt.
453
+
454
+ `purity()` ist der Energieanteil der Grundmode, `max(|c₁|², |c_{n−1}|²) / (n · Σ|z|²)`. Er ist genau
455
+ dann `1`, wenn der Ring bis auf Rotation die `n`-ten Einheitswurzeln ist, und er kostet `O(n)`, weil
456
+ nur zwei Bins benötigt werden. Beide Richtungen werden geprüft, da ein Ring den Kreis in beide
457
+ Richtungen durchlaufen kann.
458
+
459
+ ### 10.2 Ein Feld ist ein beschränkter Score
460
+
461
+ Ein `PhaseField` bildet eine relative Phase auf einen Score in `[-1, 1]` ab:
462
+
463
+ ```typescript
464
+ import { useField, useRadian } from "semantic-typescript";
465
+
466
+ const cosine = useField([1]); // cos(2πΔ): +1 aligned, −1 opposed
467
+ const mixed = useField([0.4, 0.3, 0.3]); // first three harmonics
468
+
469
+ cosine.sample(useRadian(0)); // 1
470
+ cosine.sample(useRadian(Math.PI)); // -1
471
+ ```
472
+
473
+ Der Score ist eine abgebrochene Kosinusreihe, `a₀ + Σ wₘ·cos(2πmΔ)`, und der Konstruktor erzwingt
474
+ `|a₀| + Σ|wₘ| ≤ 1`. Nach der Dreiecksungleichung kann der Score `[-1, 1]` niemals verlassen — die
475
+ Schranke ist eine **Konsequenz der Konstruktion**, kein nachträglich angewendetes Clamping.
476
+
477
+ Der Grund, warum das Feld eine Reihe und keine Tabelle ist: Eine Tabelle mit `n` Punkten reproduziert
478
+ ihre eigenen Stützstellen exakt, ihr trigonometrisches Interpolant schießt dazwischen jedoch über. Eine
479
+ Fünf-Punkt-Tabelle mit den Scores `[+1, +0.5, −1, −0.5, 0]` interpoliert über `[−1.0428, 1.0041]`, was
480
+ das Versprechen bereits bricht. Eine beschränkte Reihe kann das nicht.
481
+
482
+ ### 10.3 Die Analyse misst Daten gegen jedes Element
483
+
484
+ Jedes Ringelement ist eine Referenzphase. `analyse` verortet jede Beobachtung wieder auf dem Ring und
485
+ misst sie gegen jede Referenz:
486
+
487
+ ```typescript
488
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e"]);
489
+ const report = ring.analyse(["a", "c", "c", "a", "zz", "e"]);
490
+
491
+ report.matched; // 5
492
+ report.missed; // 1 ('zz' is not on the ring)
493
+ report.references.map(r => r.count); // [2, 0, 2, 0, 1]
494
+ ```
495
+
496
+ Jede Referenzzeile meldet `count`, die mittlere relative Phase `mean` und die mittlere Resultantenlänge
497
+ `resultant`. Eine Identität lohnt sich zu merken: **`resultant` ist für jede Referenz gleich**, weil
498
+ eine geänderte Basis alle relativen Phasen nur um eine Konstante rotiert. Pro Referenz unterscheiden
499
+ sich der Count und der Mittelwert.
500
+
501
+ ```typescript
502
+ const ring = new SynchronousRing(["a", "b", "c"]);
503
+ ring.scoresAt(0, cosine); // one score per position, in [-1, 1]
504
+ ```
505
+
506
+ ### 10.4 Verwandte Instrumente
507
+
508
+ | Aufruf | Beantwortet | Kosten |
509
+ | :--- | :--- | :--- |
510
+ | `purity()` | ist der Ring noch die `n`-ten Einheitswurzeln? | O(n) |
511
+ | `resultant(t?)` | wie konzentriert sind die Phasoren gerade jetzt? | O(n) |
512
+ | `analyse(data)` | wie liegen diese Daten gegen jedes Element? | O(n · m) |
513
+ | `toSpectrum()` | der Ring als Spektrum, Wellenzahl auf der Frequenzachse | O(n²) |
514
+ | `useAngularSpectrum(ring, instants, plan)` | die `(ω, n)`-Ebene über die Zeit | O(n² · frames) |
515
+
516
+ Zwei weitere Primitive runden das Modell ab: `combineRings(a, b)` verbindet zwei Ringe auf ihrem
517
+ kleinsten gemeinsamen Vielfachen (ein Pullback, kein Produkt — die Paare, die sich keinen Zeitpunkt
518
+ teilen können, fehlen schlicht), und `rate()` gibt jedem Element seine eigene intrinsische Frequenz,
519
+ sodass ein Ring gegen die Umgebung driften kann. `jetAt`, `windingAt` und `errorIntegralAt` legen den
520
+ differentiellen Zustand dieser Drift offen.
521
+
522
+ ### 10.5 Spektrale Fenster
523
+
524
+ Die Ringkonstruktionen sind die eine Hälfte der analytischen Oberfläche; die andere arbeitet an abgetasteten Signalen. Ihr meistgenutztes Primitiv ist das Fenster, und jede Form hier hat zwei Varianten. Die symmetrische Variante teilt den Frame durch `count − 1`; die periodische (DFT-gerade) Variante teilt ihn durch `count`, einen Abtastwert weniger als eine volle Periode, sodass die Ausblendung unter der periodischen Fortsetzung, die die DFT annimmt, stetig ist.
525
+
526
+ ```typescript
527
+ useHannWindow(1, 4); // 0.75 symmetric
528
+ useHannPeriodicWindow(1, 4); // 0.5 periodic
529
+ coherentGain(useHannPeriodicWindow, 16); // 0.5
530
+ equivalentNoiseBandwidth(useHannPeriodicWindow, 16); // 1.5
531
+ ```
532
+
533
+ | Form | Symmetrisch | Periodisch | Anmerkungen |
534
+ | :--- | :--- | :--- | :--- |
535
+ | Rechteck | `useRectangularWindow` | — | keine Ausblendung; keine periodische Unterscheidung |
536
+ | Hann | `useHannWindow` | `useHannPeriodicWindow` | der Standard der Kurzzeitanalyse |
537
+ | Hamming | `useHammingWindow` | `useHammingPeriodicWindow` | — |
538
+ | Blackman | `useBlackmanWindow` | `useBlackmanPeriodicWindow` | — |
539
+ | Nuttall | `useNuttallWindow` | `useNuttallPeriodicWindow` | viergliedriges Blackman–Harris |
540
+ | Flat-Top | `useFlatTopWindow` | `useFlatTopPeriodicWindow` | Amplitudengenauigkeit vor Auflösung |
541
+ | Kaiser | `useKaiserWindow(beta)` | `useKaiserPeriodicWindow(beta)` | `beta` bestimmt die Hauptkeulenbreite |
542
+
543
+ Verwenden Sie die periodische Variante für Analyse und Overlap-Add: Bei einem Halbframe-Hop summiert das periodische Hann-Fenster bei jeder Länge zu `1.000000`, während die symmetrische Variante bei `N = 16` um `0.841 dB` und bei `N = 1024` noch um `0.013 dB` wellig ist. Für den Filterentwurf bleibt die symmetrische Variante richtig, wo der Frame nicht eine Periode der Ausblendung ist.
544
+
545
+ Die Flat-Top-Fenster liefern an den Frame-Enden **negative Gewichte**, weil ein Flat-Top-Fenster genau das ist. Sie werden unverändert zurückgegeben statt abgeschnitten, denn Abschneiden stört die Hauptkeule und zerstört den geringen Scalloping-Verlust, um dessentwillen das Fenster existiert:
546
+
547
+ ```typescript
548
+ useFlatTopWindow(0, 1024); // -0.000421 — negative by definition
549
+ Array.from({ length: 1024 }, (_unused, index) => useFlatTopWindow(index, 1024))
550
+ .filter(weight => weight < 0).length; // 578
551
+ ```
552
+
553
+ ### 10.6 Pegel, Flachheit und Bandenergie
554
+
555
+ `decibel` ist als einzelne Funktion verschwunden, weil Amplitude und Leistung keinen gemeinsamen Faktor teilen. `amplitudeDecibel` wendet `20·log10` auf ein spannungsartiges Verhältnis an; `powerDecibel` wendet `10·log10` auf eine Leistung, eine Energie, ein mittleres Quadrat oder eine spektrale Leistungsdichte an:
556
+
557
+ ```typescript
558
+ amplitudeDecibel(0.25); // -12.0412
559
+ powerDecibel(0.25); // -6.0206
560
+ ```
561
+
562
+ `Bin.decibel` und `Spectrum.decibelOf` sind Amplitudenpegel; `Spectrum.powerDecibelOf` ist der passende Leistungspegel. Eine Leistung an die Amplitudenform zu geben, meldet `20·log10(power)` und verdoppelt den Pegel in Dezibel.
563
+
564
+ Zwei Spektrummessungen haben die Lesart gewechselt:
565
+
566
+ ```typescript
567
+ const twoBins = new Spectrum(new Sample(2, 1), [new Complex(2, 0), new Complex(1, 0)]);
568
+ twoBins.flatness(); // 0.8 — geometric over arithmetic mean of power
569
+
570
+ const shape = new Spectrum(new Sample(4, 1), [
571
+ new Complex(1, 0), new Complex(2, 0), new Complex(0, 0), new Complex(0, 0),
572
+ ]);
573
+ shape.bandEnergy(new Frequency(0), new Frequency(2)); // 9
574
+ shape.bandEnergy(new Frequency(0), new Frequency(2), "two-sided"); // 5
575
+ ```
576
+
577
+ `flatness()` vergleicht das geometrische und das arithmetische Mittel von `power()` über die einseitige Hälfte des Spektrums, nicht das Betragsmittel über beide Hälften, und die reine Anzeigeverdopplung von `oneSided()` wird bewusst nicht angewandt. `bandEnergy()` nutzt standardmäßig die einseitige Lesart und faltet die konjugierte Hälfte, sodass DC und Nyquist einfach und jeder innere Bin doppelt zählen; ein Band über die gesamte Achse erfüllt dann Parseval, `Σ|X|² = N·Σx²`. Übergeben Sie `"two-sided"` für ein komplexes Spektrum, dessen Bins bereits unabhängig sind.
578
+
579
+ ### 10.7 Kurzzeitanalyse
580
+
581
+ `Sampler.shortTimeFourierTransform` und `Sampler.spectrogram` folgen den SciPy-`stft`-Konventionen: periodisches Hann-Fenster, eine mit Nullen gefüllte Grenze an beiden Enden und ein mit Nullen gefüllter Schwanz.
582
+
583
+ ```typescript
584
+ const sampler = new Sampler(new Sample(64, 1));
585
+ const spectrogram = sampler.spectrogram(time => Math.cos(2 * Math.PI * 4 * time), 16, 8);
586
+
587
+ spectrogram.size(); // 9 frames
588
+ spectrogram.getBoundary(); // "zeros"
589
+ spectrogram.getTime(6); // 0.75 — frame centres, not frame starts
590
+ sampler.spectrogram(time => Math.cos(2 * Math.PI * 4 * time), 16, 8, useHannWindow,
591
+ { boundary: "none", padded: false }).size(); // 7 — complete frames only
592
+ ```
593
+
594
+ Die Erweiterung zentriert den ersten Frame auf den ersten Abtastwert, und die Schwanzauffüllung hält die letzten Abtastwerte erreichbar: 64 Abtastwerte bei Frame 16 und Hop 8 ergeben 9 Frames statt 7, und 1000 Abtastwerte bei Frame 256 und Hop 128 ergeben 9 statt 6. Frame-Zeiten sind Mitten — `(k·hop + (frameSize mod 2)/2)/rate` für eine erweiterte Ansicht, `(k·hop + frameSize/2)/rate` für eine nicht erweiterte — und `toFrame()` schneidet die Erweiterung vor der Inversion ab.
595
+
596
+ ---
597
+
598
+ <a id="11-utility-helpers"></a>
599
+ ## 11. Hilfsfunktionen
600
+
601
+ ### 11.1 Die Fehlerhierarchie
602
+
603
+ `structure/error.ts` exportiert eine Hierarchie aus fünfzehn Klassen für die Fehler, die *rund um* eine Pipeline entstehen — in Ihrem eigenen Code, einem eigenen Collector oder einem Adapter:
604
+
605
+ ```text
606
+ SemanticError
607
+ ├── IllegalOperationError
608
+ ├── ValidationError
609
+ │ ├── IndexError
610
+ │ ├── RangeError (also exported as SemanticRangeError)
611
+ │ └── ConfigurationError
612
+ ├── CapacityError
613
+ ├── StateError
614
+ │ └── IllegalStateError
615
+ ├── ConcurrencyError
616
+ ├── CollectionError
617
+ ├── SerializationError
618
+ ├── NumericError
619
+ │ └── ArithmeticError
620
+ └── PhaseError
621
+ ```
622
+
623
+ Jede Klasse legt `name` fest, reicht die native `cause` weiter, trägt eine optionale `context`-Tasche und serialisiert über `toJSON()`; jede hat einen gleichnamigen Brand-Guard.
624
+
625
+ ```typescript
626
+ const error = new CapacityError("LRU capacity exhausted", { context: { capacity: 0 } });
627
+
628
+ error.name; // "CapacityError"
629
+ isCapacityError(error); // true
630
+ isValidationError(error); // false — a capacity failure is not a validation failure
631
+ isSemanticError(error); // true
632
+ error.toJSON(); // { name: "CapacityError", message: "…", context: { capacity: 0 } }
633
+ ```
634
+
635
+ `SemanticRangeError` ist dieselbe Klasse wie das exportierte `RangeError`, sodass `instanceof` über beide Namen hinweg gilt; ein Import von `RangeError` verdeckt den globalen Konstruktor für das ganze Modul, und deshalb existiert der Alias. Keine der beiden Klassen wird von den Argumentprüfungen der Bibliothek geworfen, die weiterhin native `TypeError` und `RangeError` auslösen — die Hierarchie ist das Vokabular für die Ebene darüber.
636
+
637
+ ### 11.2 Hilfstabelle
638
+
639
+ | Hilfsfunktion | Zweck |
640
+ | :--- | :--- |
641
+ | `useCompare(a, b)` | Totale Ordnung über gemischte Primitive hinweg |
642
+ | `useToNumber` / `useToBigInt` | Verlustfreie, bereichsgeprüfte numerische Konvertierung |
643
+ | `useRandom` | Zufallshelfer mit oder ohne Seed |
644
+ | `useTraverse` | Struktureller Durchlauf verschachtelter Objekte und Arrays |
645
+ | `useStringify` | Zyklussichere JSON-Serialisierung |
646
+ | `useHash` / `maskOf` / `register` / `unregister` | Strukturelles Hashing und die Registry der Hash-Strategien |
647
+ | `invalidate` / `validate` | Guard-Klauseln, die eine konsistente Fehlerform auslösen |
648
+ | `Optional<T>` | Der `null`-Ersatz der Bibliothek: `Optional.of(x)` / `Optional.empty()` |
649
+
650
+ ### 11.3 `useTraverse`
651
+
652
+ `useTraverse(target, callback)` flacht jedes Objekt zu seinen **Nicht-Null-Blättern** ab. Es steigt in verschachtelte Objekte hinab, durchläuft iterierbare Werte nach Position, überspringt `null` und `undefined` und betritt ein bereits besuchtes Objekt nie erneut — ein Objektgraph mit einem Zyklus endet also, statt den Stack zu sprengen.
653
+
654
+ ```typescript
655
+ const source = { id: 7, tags: ["a", "b"], owner: { name: "Ada", note: null } };
656
+ const leaves: string[] = [];
657
+
658
+ useTraverse(source, (key, value, path) => {
659
+ leaves.push(`${path.join(".")} = ${String(value)}`);
660
+ return true;
661
+ });
662
+
663
+ leaves; // ["id = 7", "tags.0 = a", "tags.1 = b", "owner.name = Ada"]
664
+ ```
665
+
666
+ Der Callback läuft einmal pro Blatt und erhält den eigenen Schlüssel des Blatts (bei einem iterierbaren Wert dessen Index), den Wert und den Pfad dorthin. Die Rückgabe `false` gibt die **gesamte** Traversierung auf, nicht nur den aktuellen Zweig:
667
+
668
+ ```typescript
669
+ let visited = 0;
670
+ useTraverse({ a: 1, b: 2, c: 3 }, () => {
671
+ visited += 1;
672
+ return false;
673
+ });
674
+ visited; // 1 — the first leaf already stops the walk
675
+ ```
676
+
677
+ Aus demselben Grund ist ein Zyklus ungefährlich:
678
+
679
+ ```typescript
680
+ const cyclic: any = { name: "root", child: { name: "leaf" } };
681
+ cyclic.child.parent = cyclic;
682
+
683
+ const names: string[] = [];
684
+ useTraverse(cyclic, (_key, value) => {
685
+ if (typeof value === "string") names.push(value);
686
+ return true;
687
+ });
688
+ names; // ["root", "leaf"] — the back edge to `cyclic` is not revisited
689
+ ```
690
+
691
+ **Jeder Callback erhält seinen eigenen Pfad-Schnappschuss.** Der Durchlauf hält intern nur ein Arbeitsarray und kopiert es in dem Moment, in dem ein Blatt übergeben wird, in ein neues, mit `Object.freeze` eingefrorenes Array. Der Schnappschuss ändert sich danach nicht mehr, kann also unverändert behalten werden; zwei Callbacks teilen sich nie ein Array, und ein erneuter Durchlauf stört bereits ausgegebene Schnappschüsse nicht. Sein Typ ist `ReadonlyArray`, ein Schreibzugriff ist daher ein Compile-Fehler, und auf dem eingefrorenen Array wirft er zur Laufzeit zusätzlich einen `TypeError`. Diese Sicherheit kostet eine `O(depth)`-Allokation pro gemeldetem Blatt; ein Zweig, der nichts meldet, allokiert nichts zusätzlich.
692
+
693
+ ```typescript
694
+ const paths: Array<ReadonlyArray<PropertyKey>> = [];
695
+ useTraverse({ a: { b: 1 }, c: 2 }, (_key, _value, path) => {
696
+ paths.push(path); // safe to keep as-is
697
+ return true;
698
+ });
699
+ paths[0] === paths[1]; // false — one array per call
700
+ paths.map(path => path.join(".")); // ["a.b", "c"]
701
+ ```
702
+
703
+ `useAttribute` ist dieser Durchlauf als Fabrik: Sie meldet dieselben Blätter als `{ key, value }`-Paare, also sind Regeln und Kosten identisch.
704
+
705
+ ```typescript
706
+ useAttribute({ a: 1, b: { c: 2 } }).toUnordered().toArray();
707
+ // [{ key: "a", value: 1 }, { key: "c", value: 2 }]
708
+ ```
709
+
710
+ ---
711
+
712
+ <a id="12-performance-and-pitfalls"></a>
713
+ ## 12. Leistung und Fallstricke
714
+
715
+ **Trägheit hat einen Preis pro Stufe.** Jeder Operator fügt einen Generator-Hop hinzu. Für enge
716
+ numerische Schleifen über In-Memory-Arrays schlägt eine schlichte `for`-Schleife jede Pipeline; die
717
+ Pipeline gewinnt, wenn die Quelle träge oder unendlich ist oder die Pipeline lang genug ist, damit sich
718
+ die Index-Disziplin auszahlt.
719
+
720
+ **`toOrdered()` sortiert; `toUnordered()` nicht.** Wenn eine Umkehrung „nicht funktioniert" zu haben
721
+ scheint, war der Collector ungeordnet — das Umschreiben von Indizes wird erst dann zu einer Reihenfolge,
722
+ wenn die Indizes normalisiert werden.
723
+
724
+ **`useGenerate` ohne `interrupt` endet nie.** Geben Sie stets ein stoppendes Prädikat oder ein
725
+ nachgelagertes `limit` / `takeWhile` an.
726
+
727
+ **`Ring` ist ein einfügungsgeordnetes Set.** Elemente sind eindeutig und behalten die Reihenfolge, in
728
+ der sie zuerst hinzugefügt wurden. Ein doppeltes `add` gibt `false` zurück, und ein `set`, das einen
729
+ Doppelgänger einführen würde, wirft. Genau das erlaubt es, eine Phase als Funktion des *Elements* zu
730
+ behandeln und nicht der Position, die es gerade zufällig einnimmt.
731
+
732
+ **`Spectrum` setzt in seinen einseitigen Helfern ein reelles Signal voraus.** `oneSided()`,
733
+ `getNyquist()` und `centroid()` unterstellen konjugierte Symmetrie. Das räumliche Spektrum eines
734
+ Phasenrings ist komplex und im Allgemeinen nicht symmetrisch — ein Ring, der den Kreis rückwärts
735
+ durchläuft, legt seine gesamte Energie in Bin `n − 1`, jenseits von Nyquist. Lesen Sie die Bins direkt.
736
+
737
+ **Struktur-Collectors sind Terminals, keine Ansichten.** Jeder leert die Pipeline und baut dann eine vollständige Struktur auf; er kostet also O(n) Zeit und O(n) Speicher und läuft nicht auf einer unendlichen Quelle. Verwenden Sie einen am Ende einer Pipeline, niemals innerhalb einer Schleife über eine Pipeline.
738
+
739
+ **`variance()` und seine Verwandten geben ein `Optional` zurück.** `variance().get()` auf einer zu kleinen Stichprobe wirft; verwenden Sie `.orElse(0)` oder prüfen Sie `.isPresent()`, wenn die Stichprobengröße nicht in Ihrer Hand liegt. Dasselbe gilt für `standardDeviation`, `quantile`, `interquartileRange`, `skewness` und `kurtosis`.
740
+
741
+ **Die periodischen Fenster sind die Analysefenster.** `useHannWindow` und die anderen symmetrischen Formen sind für den Filterentwurf gedacht; eine Kurzzeitanalyse will `useHannPeriodicWindow` und seine Geschwister, sonst ist die Overlap-Add-Summe an jeder Fuge der Frames wellig.
742
+
743
+ ---
744
+
745
+ <a id="13-phase-rings"></a>
746
+ ## 13. Phasenringe: ein exakter Zyklus
747
+
748
+ Kapitel 10 führte den Ring als Modell eines Zyklus ein und nutzte ihn, um einen Datensatz gegen jede
749
+ Phase zu messen. Dieses Kapitel ist die vollständige Oberfläche: wie aus einer Position ein Winkel
750
+ wird, was die Umgebungsfrequenz ändert und was nicht, und die Instrumente, die den Ring gegen die
751
+ Zeit, gegen Daten und gegen einen anderen Ring lesen.
752
+
753
+ Ein `SynchronousRing` ist eine Positionsliste, unterlegt von einem kreisförmigen doppelt
754
+ verketteten Ring, dessen Kopf und Schwanz verbunden sind. Er ist zugleich eine einfügungsgeordnete
755
+ *Menge*: kein Element erscheint zweimal, und ein abgelehnter Schreibvorgang lässt sowohl die
756
+ Verkettung als auch die Abtastwerte unberührt. Zum Ring statt zur bloßen Liste macht ihn, dass die
757
+ Phase einer Position eine reine Funktion ihres Restes modulo der aktuellen Länge ist; deshalb ist
758
+ der Ring bitgenau periodisch, und seine Balance kann nicht driften.
759
+
760
+ ### 13.1 Ein Ring ist eine ganze Zahl, exakt berechnet
761
+
762
+ Ein Ring trägt `n` Elemente, und Elementanzahl, Positionsperiode und Anzahl verschiedener Phasen
763
+ sind dieselbe ganze Zahl. Position `i` trägt
764
+
765
+ `phase + span · 2π · (((i mod n) + n) mod n) / n`
766
+
767
+ wobei der Rest in `bigint` reduziert wird, bevor er je eine `number` wird. Genau deshalb sind
768
+ `phaseAt(i)` und `phaseAt(i + n)` *identisch* und nicht bloß ähnlich: Der zweite Aufruf reduziert
769
+ auf dieselbe ganze Zahl, wertet dasselbe Produkt aus und erzeugt dasselbe Double. Ein Index weit
770
+ jenseits des exakten Ganzzahlbereichs von `number` landet noch auf der richtigen Position, und ein
771
+ negativer Index läuft den Ring rückwärts.
772
+
773
+ | Aufruf | Rückgabe | Bedeutung |
774
+ | :--- | :--- | :--- |
775
+ | `new SynchronousRing(source, options?)` | Ring | `source` ist ein beliebiges Iterable, Duplikate entfallen |
776
+ | `getPhase()` / `getFrequency()` / `getSpan()` | `Angle`, `Frequency`, `number` | die drei Konstruktionsparameter |
777
+ | `size()` | `bigint` | das `n`, das zugleich die Periode ist |
778
+ | `phaseAt(index)` | `Optional<Angle>` | nur die intrinsische Phase; ignoriert Zeit, Rate und Frequenz |
779
+ | `elementAt(index)` | `Optional<RingElement>` | Element, intrinsische Phase, Umgebungsfrequenz, Zeiger |
780
+ | `frequencyAt(index)` | `Optional<Frequency>` | die Umgebungsrate, leer bei leerem Ring |
781
+ | `toComplex()` / `toComplex(index)` | `Complex[]` / `Optional<Complex>` | intrinsische Zeiger |
782
+
783
+ `RingOptions` ergänzt zwei Funktionen pro Element: `magnitude(element, index)` liefert die
784
+ Zeigerlänge und ist standardmäßig eins, `rate(element, index)` gibt dem Element seine eigene
785
+ intrinsische Frequenz und ist standardmäßig null, also eine feste Markierung.
786
+
787
+ ```typescript
788
+ import { SynchronousRing } from "semantic-typescript";
789
+
790
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
791
+
792
+ ring.size(); // 6n
793
+ ring.phaseAt(0n).get().toDegreeValue(); // 0
794
+ ring.phaseAt(1n).get().toDegreeValue(); // 60
795
+ ring.phaseAt(-1n).get().toDegreeValue(); // 300
796
+ ring.phaseAt(6n).get().getValue() === ring.phaseAt(0n).get().getValue(); // true
797
+ ring.phaseAt(10n ** 20n).get().toDegreeValue(); // 240
798
+ ```
799
+
800
+ `phaseAt(-1n)` ist bei einem Ring aus sechs Elementen `300`, und `10n ** 20n` reduziert modulo
801
+ sechs auf `4`, liest sich also als `240`. Keiner der Fälle akkumuliert ein Fließkommaprodukt; das
802
+ hält die Identität exakt, auch wenn sich die Indexdarstellung ändert.
803
+
804
+ ### 13.2 Die Umgebungsfrequenz und der Zustand zu einem Zeitpunkt
805
+
806
+ `frequency` ist die **Umgebungsrotationsrate** der Umgebung, in die der Ring eingebettet ist, kein
807
+ Phasenschritt zwischen Positionen. Die Umgebung verschiebt die Phase jedes Elements um denselben
808
+ Winkel, kann also das intrinsische Muster nicht ändern, die Balance nicht brechen und kein
809
+ `phaseAt`-Ergebnis verändern. Der Standard ist `0 Hz`.
810
+
811
+ `relativeAt(index, time)` zieht diesen gemeinsamen Winkel ab und hinterlässt die Koordinate, gegen
812
+ die ein `PhaseField` bewertet wird. `viewAt` und `viewsAt` liefern den vollen Zustand zu einem
813
+ Zeitpunkt samt der eigenen `rate` des Elements, und die Differential-Zugriffe legen die Drift frei:
814
+
815
+ | Aufruf | Formel | Liest |
816
+ | :--- | :--- | :--- |
817
+ | `relativeAt(index, time)` | `intrinsisch + eigeneRate · t − Umgebung · t` | die Phase, die die Umgebung sieht |
818
+ | `viewAt(index, time)` / `viewsAt(time)` | dasselbe plus Element, Index, Zeit, Rate, Zeiger | der volle Zustand |
819
+ | `jetAt(index, time)` | Phase bei `t`, eigene Rate, `slip = eigeneRate − Umgebung` | der Differentialzustand |
820
+ | `windingAt(index, time)` | `trunc(slip · t)` | ganze Umdrehungen Schlupf, gegen null abgeschnitten |
821
+ | `errorIntegralAt(index, time, origin?)` | `∫ relative Phase dt`, in Umdrehungen · Sekunden | wie lange ein Element voraus war |
822
+ | `resultant(time?)` | `abs(Σ w · e^{iθ}) / Σ w` | Phasenkonzentration, von Umgebungsrotation unberührt |
823
+ | `analyse(data, time?)` | Zählungen und Mittel je Referenz | Daten gegen jedes Element gemessen |
824
+ | `scoreAt(index, time, field)` / `scoresAt(time, field)` | das Feld an der relativen Phase abgetastet | ein beschränkter Score je Position |
825
+
826
+ ```typescript
827
+ import { SynchronousRing, useHertz } from "semantic-typescript";
828
+
829
+ const moving = new SynchronousRing(["a", "b", "c", "d", "e", "f"], {
830
+ frequency: useHertz(1),
831
+ });
832
+
833
+ moving.getFrequency().getHertz(); // 1
834
+ moving.phaseAt(1n).get().toDegreeValue(); // 60 — intrinsic
835
+ moving.relativeAt(1n, 0.25).get().toDegreeValue(); // 330 — 60 − 90
836
+ moving.viewAt(1n, 0.25).get().relative.toDegreeValue(); // 330
837
+ moving.jetAt(1n, 0.25).get().slip; // -1
838
+ moving.purity(); // 1 — rotation cannot break it
839
+ ```
840
+
841
+ Bei `1 Hz` rückt die Umgebung in `0.25 s` um eine Vierteldrehung vor, die relative Phase von
842
+ Position eins ist also `60 − 90 = 330` Grad, während ihre intrinsische Phase `60` bleibt. Der Jet
843
+ meldet `slip` von `-1` Hertz, weil das Element selbst ruht, und `purity()` ist weiterhin `1`: Alle
844
+ Zeiger um denselben Winkel zu drehen ist keine Verformung.
845
+
846
+ ### 13.3 Reinheit und Balance
847
+
848
+ `purity()` ist der Energieanteil des Rings, den die Grundmode trägt:
849
+
850
+ `max(abs(c₁)², abs(c_{n−1})²) / (n · Σ abs(z)²)`
851
+
852
+ Beide Grundbins werden geprüft, weil ein Ring den Kreis in beide Richtungen durchlaufen kann. Ein
853
+ Ring mit `span: 5` über sechs Elemente schreitet um `300` Grad, also `−60` Grad: Er läuft rückwärts,
854
+ legt seine ganze Energie in `c₅` und würde von einer Formel, die nur `c₁` betrachtet, als unrein
855
+ gemeldet. Die Kosten bleiben `O(n)`, weil nur diese beiden Bins und die Gesamtenergie gebraucht
856
+ werden.
857
+
858
+ Die Randfälle gehören zum Vertrag. Ein Ring mit weniger als zwei Elementen hat keine Grundmode und
859
+ meldet `purity() === 0` und `isBalanced() === false`, unabhängig vom Inhalt. Ein Ring, dessen
860
+ Zeiger alle die Länge null haben, hat keine Energie und meldet ebenfalls null. `isBalanced(epsilon?)`
861
+ ist `1 − purity() <= epsilon`, mit `1E-9` als Standardtoleranz.
862
+
863
+ ```typescript
864
+ import { SynchronousRing } from "semantic-typescript";
865
+
866
+ const forward = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
867
+ const backward = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 5 });
868
+ const weighted = new SynchronousRing(["a", "b", "c", "d"], {
869
+ magnitude: element => element === "a" ? 1 : 0,
870
+ });
871
+
872
+ forward.purity(); // 1
873
+ backward.phaseAt(1n).get().toDegreeValue(); // 300 — it runs backwards
874
+ backward.purity(); // 1 — the energy sits in c₅
875
+ new SynchronousRing(["only"]).purity(); // 0
876
+ new SynchronousRing(["only"]).isBalanced(); // false
877
+ weighted.purity(); // 0.25
878
+ ```
879
+
880
+ ### 13.4 Resultierende Länge und die Symmetriekomponenten
881
+
882
+ `resultant(time?)` ist die mittlere resultierende Länge: der Betrag der Zeigersumme geteilt durch
883
+ die Summe der Beträge. Anders als `purity`, das fragt, ob der Ring noch die `n`-ten Einheitswurzeln
884
+ ist, fragt `resultant`, wie konzentriert die Zeiger gerade sind. Ein balancierter Ring meldet `0`;
885
+ ein Ring, dessen Elemente alle einen Winkel teilen, meldete `1`, aber die Mengensemantik verbietet
886
+ Duplikate, sodass `1` praktisch erscheint, wenn `magnitude` alle bis auf ein Element nullt.
887
+
888
+ `zeroSequence()` ist der mittlere Zeiger, also `c₀ / n`. `sequences()` liefert alle `n`
889
+ Symmetriekomponenten, niedrigste Ordnung zuerst, über eine direkte Transformation. `toSpectrum()`
890
+ packt sie in ein `Spectrum`, dessen Bin `j` `j` Zyklen pro Ringumdrehung meldet, sodass die
891
+ Frequenzachse die Wellenzahlachse ist.
892
+
893
+ ```typescript
894
+ import { SynchronousRing } from "semantic-typescript";
895
+
896
+ const round = (value: number): number => Number(value.toFixed(4));
897
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
898
+
899
+ round(ring.resultant().get()); // 0
900
+ round(ring.zeroSequence().get().magnitude()); // 0
901
+ ring.sequences().map(component => round(component.magnitude())); // [0, 6, 0, 0, 0, 0]
902
+ ring.toSpectrum().get().size(); // 6
903
+ ring.toSpectrum().get().getResolution().getHertz(); // 1
904
+ ```
905
+
906
+ Die Komponenten sind komplex und im Allgemeinen **nicht konjugiert-symmetrisch**: Ein balancierter
907
+ Ring legt seine ganze Energie in Bin eins, ein rückwärts laufender in Bin `n − 1`. Die einseitigen
908
+ Hilfsfunktionen von `Spectrum` setzen ein reelles Signal voraus; lies die Bins direkt, statt sie zu
909
+ falten.
910
+
911
+ ### 13.5 Daten gegen jedes Element analysieren
912
+
913
+ `analyse(data, time?)` ordnet jede Beobachtung über Identität dem Ring zu und misst sie gegen jedes
914
+ Element als Referenzphase. Beobachtungen, die der Ring nicht hält, werden als `missed` gezählt statt
915
+ geraten, denn der Ring ist die einzige Quelle von Phasen. Jede Referenzzeile meldet `count`, die
916
+ mittlere relative Phase `mean` und die mittlere resultierende Länge `resultant`.
917
+
918
+ Eine Identität lohnt sich zu merken: **`resultant` ist für jede Referenz gleich**. Eine andere
919
+ Basis dreht alle relativen Phasen um eine Konstante, und eine Drehung kann eine resultierende Länge
920
+ nicht ändern. Was sich je Referenz unterscheidet, sind Zählung und Mittel.
921
+
922
+ ```typescript
923
+ import { SynchronousRing } from "semantic-typescript";
924
+
925
+ const round = (value: number): number => Number(value.toFixed(4));
926
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e"]);
927
+ const report = ring.analyse(["a", "c", "c", "a", "zz", "e"]);
928
+
929
+ report.matched; // 5
930
+ report.missed; // 1
931
+ report.references.map(row => row.count); // [2, 0, 2, 0, 1]
932
+ report.references.map(row => round(row.resultant)); // [0.1453, 0.1453, 0.1453, 0.1453, 0.1453]
933
+ ```
934
+
935
+ Bewertung ist die andere Seite derselben Medaille. Ein `PhaseField` ist eine beschränkte
936
+ Kosinusreihe (Kapitel 10.2), und `scoreAt`/`scoresAt` tasten sie an der relativen Phase ab, sodass
937
+ derselbe Ring gegen Ausrichtung, gegen ein harmonisches Muster oder gegen ein beliebiges Feld
938
+ bewertet werden kann, ohne den Ring zu ändern.
939
+
940
+ ```typescript
941
+ import { SynchronousRing, useField } from "semantic-typescript";
942
+
943
+ const round = (value: number): number => Number(value.toFixed(4));
944
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
945
+ const cosine = useField([1]);
946
+
947
+ round(ring.scoreAt(0n, 0, cosine).get()); // 1
948
+ round(ring.scoreAt(1n, 0, cosine).get()); // 0.5
949
+ ring.scoresAt(0, cosine).map(score => round(score)); // [1, 0.5, -0.5, -1, -0.5, 0.5]
950
+ ```
951
+
952
+ ### 13.6 Ringe kombinieren und das Winkelspektrum
953
+
954
+ `combineRings(left, right)` verbindet zwei Ringe über ihr kleinstes gemeinsames Vielfaches, nicht
955
+ über ihr Produkt. Instant `i` des gemeinsamen Rings liest `i mod m` des linken und `i mod n` des
956
+ rechten Rings, sodass der gemeinsame Ring nach `lcm(m, n)` Instants schließt. Die Paare, die nie
957
+ gemeinsam auftreten können — deren Positionen modulo `gcd(m, n)` nicht übereinstimmen —, fehlen
958
+ einfach; deshalb ist das Ergebnis ein Pullback und kein Produkt, und `aligns(left, right)` meldet,
959
+ ob ein gegebenes Paar darstellbar ist.
960
+
961
+ `useAngularSpectrum(ring, instants, plan)` baut die `(ω, n)`-Ebene: Jeder Instant trägt einen Frame
962
+ von Zeigern bei, Frame `k` der Mode `j` ist die `j`-te Symmetriekomponente dieses Frames, und
963
+ `cells[frame][order]` ist der Koeffizient. Eine Spalte zu nehmen ergibt die Zeitreihe einer
964
+ einzigen räumlichen Mode, und `spectrum(order)` ist ihr zeitliches Spektrum.
965
+
966
+ ```typescript
967
+ import { Sample, SynchronousRing, combineRings, useAngularSpectrum } from "semantic-typescript";
968
+
969
+ const left = new SynchronousRing(["a", "b", "c", "d"]);
970
+ const right = new SynchronousRing(["x", "y", "z", "w", "v", "u"]);
971
+ const joined = combineRings(left, right);
972
+
973
+ joined.period; // 12n
974
+ joined.divisor; // 2n
975
+ joined.ring.size(); // 12n
976
+ joined.aligns(0n, 0n); // true
977
+ joined.aligns(1n, 0n); // false
978
+ joined.ring.get(0n).join("-"); // a-x
979
+
980
+ const angular = useAngularSpectrum(left, [0, 0.25, 0.5, 0.75], new Sample(4, 1));
981
+ angular.order; // 4
982
+ angular.frameCount; // 4
983
+ angular.cells[0][1].magnitude(); // 4
984
+ angular.spectrum(1).get().size(); // 4
985
+ ```
986
+
987
+ ### 13.7 Fallstricke
988
+
989
+ **Exaktheit ist exakt.** `phaseAt(i)` und `phaseAt(i + n)` sind dasselbe Double, nicht zwei Werte
990
+ innerhalb einer Toleranz. Ein Ring, der pro Element `2π / n` akkumuliert, würde das verfehlen;
991
+ dieser reduziert den Index zuerst in `bigint`.
992
+
993
+ **`frequency` ist kein Phasenschritt.** Es ist die Umgebungsrotationsrate, standardmäßig null, und
994
+ alle Zeiger um denselben Winkel zu drehen kann das intrinsische Muster nicht brechen. Wenn sich
995
+ Positionen relativ zueinander bewegen sollen, nutze `rate`, nicht `frequency`.
996
+
997
+ **`purity()` muss beide Grundbins betrachten.** Ein rückwärts laufender Ring ist noch immer die
998
+ `n`-ten Einheitswurzeln; nur `c₁` würde ihn für unrein erklären.
999
+
1000
+ **`n < 2` ist ein definierter Randfall.** `purity()` liefert `0` und `isBalanced()` `false`; ein
1001
+ leerer Ring liefert leere Optionals aus den Abtastmethoden.
1002
+
1003
+ **Ein Span, der einen Faktor mit `n` teilt, kollabiert die Umlaufbahn.** Sechs Elemente mit
1004
+ `span: 2` besuchen nur drei verschiedene Phasen, und der Ring ist nicht mehr balanciert.
1005
+
1006
+ **`resultant` ist rotationsinvariant, nicht zeitinvariant.** Die Umgebungsrotation lässt ihn
1007
+ unverändert; Raten pro Element ändern ihn, während der Ring aus der Phase läuft.
1008
+
1009
+ **Der Ring ist eine Menge, und `analyse` ordnet über Identität zu.** Duplikate entfallen bei der
1010
+ Konstruktion, und ein Objekt, das einem Ringelement strukturell gleicht, aber nicht dieselbe
1011
+ Referenz ist, zählt als verfehlt.
1012
+
1013
+ **`toSpectrum()` ist komplex.** Seine Bins sind nicht konjugiert-symmetrisch, die einseitigen
1014
+ Hilfsfunktionen von `Spectrum` greifen also nicht. Lies die Bins.
1015
+
1016
+ **`windingAt` schneidet gegen null ab, und `errorIntegralAt` integriert die Phase.** Ein halber
1017
+ Umlauf Schlupf meldet `0n`; das Fehlerintegral ist in Umdrehungen · Sekunden, keine Rate.
1018
+
1019
+ ```typescript
1020
+ import { SynchronousRing, useHertz } from "semantic-typescript";
1021
+
1022
+ const collapsed = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 2 });
1023
+ collapsed.phaseAt(1n).get().toDegreeValue(); // 120
1024
+ collapsed.phaseAt(3n).get().toDegreeValue(); // 0
1025
+ Number(collapsed.purity().toFixed(4)); // 0
1026
+
1027
+ const falling = new SynchronousRing(["a", "b", "c", "d"], {
1028
+ frequency: useHertz(2),
1029
+ rate: () => useHertz(0.5),
1030
+ });
1031
+ falling.jetAt(0n, 2).get().slip; // -1.5
1032
+ falling.windingAt(0n, 1).get(); // -1n
1033
+ falling.windingAt(0n, 0.5).get(); // 0n
1034
+
1035
+ const objects = new SynchronousRing([{ key: 1 }, { key: 2 }]);
1036
+ objects.analyse([{ key: 1 }]).missed; // 1 — identity, not structure
1037
+ ```
1038
+
1039
+ ---
1040
+
1041
+ <a id="14-signal-analysis"></a>
1042
+ ## 14. Signalanalyse: Fenster, Spektren und Zeit
1043
+
1044
+ Kapitel 10 behandelte einen Ring als Zyklus von Phasen. Dieses Kapitel ist das zeitliche
1045
+ Gegenstück: eine reellwertige Aufzeichnung, die neben ihren Abtastwerten den beiden Kontext
1046
+ mitführt, den jede Spektralformel braucht — den Abtastplan, der einen Bin-Index in Hertz
1047
+ verwandelt, und die Fensterfunktion, die sowohl den kohärenten Gewinn als auch die
1048
+ Rauschbandbreite festlegt.
1049
+
1050
+ Das Wertobjekt ist `SynchronousSignalCollectable`, sein eingereihtes Gegenstück ist
1051
+ `AsynchronousSignalCollectable`. Beide bieten dieselbe `SignalCollectable`-Oberfläche:
1052
+ dreiundzwanzig Methoden, jede delegiert an genau eine `useSynchronous<Method>`-Fabrik in
1053
+ `synchronous/collectors`. Die Trennung ist Absicht. Die Klasse ist eine dokumentierte Fassade,
1054
+ die Fabrik besitzt die Akkumulation, und der Konstruktor berechnet `sum(w) / count` und
1055
+ `count * sum(w^2) / sum(w)^2` einmal, bei dieser Länge. Wer sich merken muss, „durch den
1056
+ kohärenten Gewinn zu teilen", vergisst es irgendwann, und die daraus folgenden 6,02 Dezibel
1057
+ sehen nach einer plausiblen Messung aus.
1058
+
1059
+ ### 14.1 Ein Signal trägt seine eigene Kalibrierung
1060
+
1061
+ Ein Signal entsteht aus einem Iterable und einer Rate, aus einem vorhandenen `Frame` oder über
1062
+ den Konstruktor mit einem expliziten `Sample`. Das Standardfenster ist das periodische
1063
+ Hann-Fenster; jede Analyse ohne eigenes Fenster verwendet es, und jede Analyse akzeptiert eine
1064
+ Überschreibung. Die Metadaten-Zugriffe lesen Zustand, der beim Bauen festgeschrieben wurde, in
1065
+ konstanter Zeit, sodass ein Signal weitergereicht werden kann, ohne dass der Kontext driftet.
1066
+
1067
+ | Aufruf | Rückgabe | Bedeutung |
1068
+ | :--- | :--- | :--- |
1069
+ | `SynchronousSignalCollectable.useRate(values, rate, window?)` | Signal | Dauer `count / rate` |
1070
+ | `SynchronousSignalCollectable.useFrame(frame, window?)` | Signal | behält den Plan des Frames |
1071
+ | `new SynchronousSignalCollectable(plan, values, window?)` | Signal | Frame-Zahl muss der Abtastwertzahl entsprechen |
1072
+ | `getSample()` / `getValues()` / `getWindow()` | Plan, Werte, Fenster | der eingefrorene Kontext |
1073
+ | `getRate()` | `number` | Abtastwerte pro Sekunde |
1074
+ | `getNyquist()` / `getResolution()` | `Frequency` | `rate / 2` und `rate / count` |
1075
+ | `getCoherentGain()` / `getNoiseBandwidth()` | `number` | bei genau dieser Länge neu berechnet |
1076
+ | `size()` / `at(index)` / `toFrame()` / `equals(other)` / `toString()` | — | Wertsemantik |
1077
+
1078
+ ```typescript
1079
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1080
+
1081
+ const signal = SynchronousSignalCollectable.useRate(
1082
+ Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 64)),
1083
+ 64,
1084
+ );
1085
+
1086
+ signal.size(); // 64n
1087
+ signal.getRate(); // 64
1088
+ signal.getNyquist().getHertz(); // 32
1089
+ signal.getResolution().getHertz(); // 1
1090
+ signal.getCoherentGain(); // 0.5
1091
+ signal.getNoiseBandwidth(); // 1.5
1092
+ ```
1093
+
1094
+ `getResolution()` ist der Bin-Abstand, nicht das Auflösungsvermögen: Er beträgt `rate / count`,
1095
+ weil die Transformation `count` Bins hat, und Nullen-Auffüllen ändert den Abstand, nicht das,
1096
+ was das Fenster trennen kann (Abschnitt 14.8).
1097
+
1098
+ ### 14.2 Die dreiundzwanzig Methoden
1099
+
1100
+ Die Methoden bilden sechs Familien. Sieben sind Spektralmessungen, zwei sind Zeit-Frequenz-
1101
+ Transformationen, vier sind analytische Größen, drei sind Folgestatistiken, fünf sind
1102
+ Formbeschreibungen, und zwei liefern ein weiteres Signal.
1103
+
1104
+ | Methode | Rückgabe | Konvention |
1105
+ | :--- | :--- | :--- |
1106
+ | `spectrum(size?, window?)` | `Spectrum` | roh und unnormiert, volles komplexes Raster |
1107
+ | `oneSided(size?, window?)` | `Bin[]` | konjugierte Hälfte gefaltet, DC und Nyquist nicht verdoppelt |
1108
+ | `amplitude(window?)` | `number[]` | `2·abs(X) / (count · CG)`, DC und Nyquist einmal |
1109
+ | `power(window?)` | `number[]` | `A² / 2` je innerem Bin, `A²` bei DC und Nyquist |
1110
+ | `decibel(window?)` | `number[]` | `20·log10(Amplitude)` |
1111
+ | `powerDecibel(window?)` | `number[]` | `10·log10(Leistung)` |
1112
+ | `powerSpectralDensity(frameSize?, hopSize?, window?)` | `SpectralDensity` | Welch-Mittel, `abs(X)² / (rate · Σw²)` |
1113
+ | `shortTimeFourierTransform(frameSize, hopSize, window?)` | `Spectrum[]` | Ränder erweitert, Zeiten als Frame-Mitten |
1114
+ | `spectrogram(frameSize, hopSize, window?)` | `Spectrogram` | dieselben Spalten mit Zeitachse |
1115
+ | `analyticSignal()` | `Complex[]` | Hilbert-Paar `x + i·H(x)` |
1116
+ | `instantaneousPhase()` | `Angle[]` | in `[0, 2π)` gefaltet |
1117
+ | `instantaneousFrequency(rate?, gate?)` | `number[]` | entfaltete Phasendifferenz, auf die Hüllkurve gegated |
1118
+ | `envelope()` | `number[]` | `abs(z[n])`, an den Rändern zirkulär |
1119
+ | `zeroCrossingRate(perSecond?)` | `number` | Nulldurchgänge pro benachbartem Wertepaar |
1120
+ | `spectralFlux(frameSize?, hopSize?, window?)` | `number[]` | Betragsabstand normierter Frames |
1121
+ | `autocorrelation(maximumLag?, normalization?)` | `number[]` | standardmäßig Koeffizient, `ρ[0] = 1` |
1122
+ | `spectralCentroid(window?)` | `Optional<Frequency>` | leistungsgewichtete Mittelfrequenz |
1123
+ | `spectralBandwidth(window?)` | `Optional<Frequency>` | leistungsgewichtete Standardabweichung |
1124
+ | `spectralRolloff(ratio?, window?)` | `Optional<Frequency>` | erster Bin, der `ratio` der Leistung erreicht |
1125
+ | `spectralFlatness(window?)` | `number` | geometrisches zu arithmetischem Mittel der Leistung |
1126
+ | `signalToNoiseRatio(frameSize?, hopSize?, window?, floorQuantile?)` | `Optional<number>` | Median-Rauschboden, `10·log10` |
1127
+ | `detrended(mode?)` | Signal | `"none"`, `"constant"` (Standard), `"linear"` |
1128
+ | `windowed(window?)` | Signal | wendet ein Fenster an und übernimmt es |
1129
+
1130
+ Die optionalen Argumente sind keine Zierde. `frameSize` ist standardmäßig `min(256, size)`, der
1131
+ Hop eine halbe Fensterlänge abgerundet, `instantaneousFrequency` gated bei `1E-3` der
1132
+ Spitzenhüllkurve, und `spectralRolloff` nutzt `0.85`. Jeder Standard ist der Wert, den eine
1133
+ Referenzimplementierung wählt, wenn der Parameter benannt, aber nicht übergeben wird.
1134
+
1135
+ ### 14.3 Amplitude, Leistung und Dichte
1136
+
1137
+ Drei Normierungen sind nötig, weil drei verschiedene Fragen gestellt werden.
1138
+
1139
+ `amplitude()` beantwortet „wie groß ist der Sinus auf diesem Bin": `2·abs(X[k]) / (count · CG)`
1140
+ für einen inneren Bin und `abs(X[k]) / (count · CG)` bei DC und Nyquist. Der Faktor zwei faltet
1141
+ die konjugierte Hälfte, der kohärente Gewinn entfernt das Fenster. Fehlt eines von beiden, liest
1142
+ ein Sinus mit Einheitsamplitude `0.5`, also minus 6,02 Dezibel.
1143
+
1144
+ `power()` beantwortet „welches mittlere Quadrat hätte ein Sinus auf diesem Bin": `A² / 2` für
1145
+ einen inneren Bin und `A²` bei DC und Nyquist, weil diese beiden nicht schwingen. Das ist die
1146
+ Peak-Konvention, und sie erfüllt bewusst nicht Parseval: Ein Fenster verteilt jede Linie über
1147
+ seine Hauptkeule, sodass die Summe aller Bins das mittlere Quadrat um die äquivalente
1148
+ Rauschbandbreite übersteigt — bei Hann um `1.5`.
1149
+
1150
+ `powerSpectralDensity()` beantwortet „wie viel Leistung pro Hertz": `abs(X)² / (rate · Σw²)`,
1151
+ gemittelt über überlappende Segmente. Nur die Summe der quadrierten Gewichte hält einen
1152
+ Weißrauschboden unverzerrt; Division durch `count` liest `0.375` für Einheitsrauschen, Division
1153
+ durch `count · CG²` liest `1.5`.
1154
+
1155
+ ```typescript
1156
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1157
+
1158
+ const round = (value: number): number => Number(value.toFixed(4));
1159
+ const tone = SynchronousSignalCollectable.useRate(
1160
+ Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1161
+ 256,
1162
+ );
1163
+
1164
+ round(tone.amplitude()[32]); // 1
1165
+ round(tone.power()[32]); // 0.5
1166
+ round(tone.power().reduce((sum, power) => sum + power, 0)); // 0.75
1167
+ round(tone.powerSpectralDensity(64n, 32n).totalPower()); // 0.5
1168
+ tone.powerSpectralDensity(64n, 32n).getSegments(); // 7n
1169
+ ```
1170
+
1171
+ Das Paar der Summen ist die Prüfung, die man sich merken sollte: Die Peak-Summe ist `0.75`,
1172
+ während die Dichte zu `0.5` integriert, und das Verhältnis ist genau die Rauschbandbreite. Nur
1173
+ die Dichte übersteht außerdem eine Änderung der Fensterlänge.
1174
+
1175
+ ### 14.4 Pegel: die beiden Dezibel-Konventionen
1176
+
1177
+ Amplitude und Leistung teilen keinen Faktor, also auch keinen Logarithmus. `amplitudeDecibel`
1178
+ wendet `20·log10` an und ist für Spannung, Auslenkung oder Schalldruck richtig; `powerDecibel`
1179
+ wendet `10·log10` an und ist für Leistung, Energie, mittleres Quadrat oder Dichte richtig. Die
1180
+ Methodennamen des Signals folgen derselben Trennung.
1181
+
1182
+ ```typescript
1183
+ import { amplitudeDecibel, powerDecibel, SynchronousSignalCollectable } from "semantic-typescript";
1184
+
1185
+ const round = (value: number): number => Number(value.toFixed(4));
1186
+ const tone = SynchronousSignalCollectable.useRate(
1187
+ Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1188
+ 256,
1189
+ );
1190
+
1191
+ amplitudeDecibel(0.25); // -12.0412
1192
+ powerDecibel(0.25); // -6.0206
1193
+ round(tone.decibel()[32]); // 0
1194
+ round(tone.powerDecibel()[32]); // -3.0103
1195
+ ```
1196
+
1197
+ Beide Ablesungen desselben Tons sind korrekt: Seine Amplitude ist eins (null Dezibel), sein
1198
+ mittleres Quadrat ist ein Halb (minus 3,01 Dezibel). Eine Leistung durch die Amplitude-Konvention
1199
+ zu schicken verdoppelt den Pegel in Dezibel und macht aus den korrekten minus 6,02 die minus
1200
+ 12,04.
1201
+
1202
+ ### 14.5 Die einseitige Faltung
1203
+
1204
+ Ein reelles Signal erfüllt `X[count − k] = conj(X[k])`, die negative Frequenzhälfte trägt also
1205
+ keine unabhängige Information. Die Faltung verdoppelt jeden inneren Bin und lässt die beiden
1206
+ selbstkonjugierten Bins — DC und, bei gerader Länge, Nyquist — genau so, wie die Transformation
1207
+ sie erzeugt hat. Diese beiden sind ihr eigenes Spiegelbild; Verdoppeln erfindet Energie und
1208
+ meldet exakt 6,02 Dezibel zu hoch.
1209
+
1210
+ ```typescript
1211
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1212
+
1213
+ const round = (value: number): number => Number(value.toFixed(4));
1214
+ const edge = SynchronousSignalCollectable.useRate(
1215
+ Array.from({ length: 16 }, (_unused, index) => 2 + Math.cos(Math.PI * index)),
1216
+ 16,
1217
+ );
1218
+ const bins = edge.oneSided();
1219
+
1220
+ bins.length; // 9
1221
+ round(bins[0].magnitude()); // 16
1222
+ round(bins[8].magnitude()); // 8
1223
+ round(edge.amplitude()[0]); // 2
1224
+ round(edge.amplitude()[8]); // 1
1225
+ ```
1226
+
1227
+ Die rohen Beträge sind `16` und `8`, weil die Gewichte des Fensters sich zu acht summieren;
1228
+ Division durch `count · CG = 8` macht daraus die kalibrierten Amplituden `2` und `1`. Würde man
1229
+ die Randbins verdoppeln, läse man `4` und `2`, und der Fehler würde nie eine Ausnahme auslösen.
1230
+
1231
+ ### 14.6 Zeit-Frequenz-Ansichten
1232
+
1233
+ `shortTimeFourierTransform` und `spectrogram` rahmen die Aufzeichnung so wie die referenzierte
1234
+ Kurzzeit-Transformation: an beiden Enden eine halbe Fensterlänge Nullen, dann ein mit Nullen
1235
+ aufgefüllter Schwanz, sodass eine ganze Zahl von Hops bleibt. Der erste Frame ist auf den ersten
1236
+ Abtastwert zentriert, der letzte erreicht den letzten Abtastwert, und Frame `m` ist bei
1237
+ `m · hop / rate` Sekunden zentriert. Nur vollständige Frames zu behalten würde bis zu
1238
+ `frameSize − 1` Werte verwerfen — bei 1000 Werten, Fenster 256 und Hop 128 sind das 104 Werte,
1239
+ etwa ein Zehntel der Aufzeichnung.
1240
+
1241
+ ```typescript
1242
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1243
+
1244
+ const tone = SynchronousSignalCollectable.useRate(
1245
+ Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 4 * index / 64)),
1246
+ 64,
1247
+ );
1248
+ const spectrogram = tone.spectrogram(16n, 8n);
1249
+
1250
+ spectrogram.size(); // 9
1251
+ spectrogram.getFrameSize(); // 16
1252
+ spectrogram.getBoundary(); // "zeros"
1253
+ spectrogram.getTime(0); // 0
1254
+ spectrogram.getTime(6); // 0.75
1255
+ tone.shortTimeFourierTransform(16n, 8n).length; // 9
1256
+ tone.spectralFlux(16n, 8n).length; // 8
1257
+ ```
1258
+
1259
+ Jede Spalte ist ein `Spectrum` über `frameSize` Bins, genau wie es `spectrum()` für denselben
1260
+ Frame liefern würde, und `getTime()` meldet Frame-Mitten statt Frame-Anfänge.
1261
+
1262
+ ### 14.7 Fenster, kohärenter Gewinn und COLA
1263
+
1264
+ Jede Form hat zwei Varianten. Die symmetrische Variante teilt durch `count − 1`; die periodische
1265
+ (DFT-gerade) Variante teilt durch `count`, einen Abtastwert weniger als eine volle Periode, sodass
1266
+ die Fensterfunktion unter der periodischen Fortsetzung, die die DFT annimmt, stetig ist. Analyse
1267
+ und Überlappungs-Addition brauchen die periodische Form; der Filterentwurf behält die
1268
+ symmetrische.
1269
+
1270
+ Der Unterschied ist messbar und löst nie einen Fehler aus. Bei fünfzig Prozent Überlappung
1271
+ summiert sich das periodische Hann-Fenster an jeder Position zu eins; die symmetrische Form
1272
+ welligt, um `0.84` Dezibel bei sechzehn Werten, `0.21` bei vierundsechzig und immer noch `0.013`
1273
+ bei eintausendvierundzwanzig. Die Klasse legt den kohärenten Gewinn und die äquivalente
1274
+ Rauschbandbreite des Fensters offen, das sie hält, sodass die Korrektur nie fest verdrahtet ist:
1275
+ Ein symmetrisches Hann liest `0.4375` bei acht Werten, nicht `0.5`.
1276
+
1277
+ ```typescript
1278
+ import {
1279
+ coherentGain, equivalentNoiseBandwidth, useFlatTopWindow,
1280
+ useHannPeriodicWindow, useHannWindow,
1281
+ } from "semantic-typescript";
1282
+
1283
+ const round = (value: number): number => Number(value.toFixed(4));
1284
+
1285
+ round(useHannWindow(1, 4)); // 0.75
1286
+ round(useHannPeriodicWindow(1, 4)); // 0.5
1287
+ coherentGain(useHannPeriodicWindow, 16); // 0.5
1288
+ equivalentNoiseBandwidth(useHannPeriodicWindow, 16); // 1.5
1289
+ useFlatTopWindow(0, 1024); // -0.000421
1290
+ Array.from({ length: 1024 }, (_unused, i) => useFlatTopWindow(i, 1024)).filter(w => w < 0).length; // 578
1291
+ ```
1292
+
1293
+ Die Flat-Top-Zeile ist kein Fehler in der Tabelle. Ein Flat-Top-Fenster ist per Definition
1294
+ teilweise negativ — hier sind 578 von 1024 Gewichten negativ —, und das Abschneiden auf null
1295
+ würde die Hauptkeule stören und den geringen Scalloping-Verlust zerstören, für den das Fenster
1296
+ existiert. Die Bibliothek gibt die Reihe unverändert zurück.
1297
+
1298
+ ### 14.8 Fallstricke
1299
+
1300
+ **Nullen-Auffüllen interpoliert, es löst nie auf.** Eine Transformationslänge über der
1301
+ Abtastwertzahl tastet dieselbe DTFT dichter ab: Die Peak-*Position* wird feiner und der
1302
+ Bin-Abstand wird `rate / size`, aber ob zwei Komponenten trennbar sind, bestimmen weiterhin
1303
+ Fensterlänge und -form. Ein Ton genau zwischen zwei Bins liest `82.1093` und `80.8598` auf dem
1304
+ ungefüllten Raster und `128` bei seiner eigenen Frequenz auf dem gefüllten.
1305
+
1306
+ ```typescript
1307
+ import { SynchronousSignalCollectable, useRectangularWindow } from "semantic-typescript";
1308
+
1309
+ const round = (value: number): number => Number(value.toFixed(4));
1310
+ const tone = SynchronousSignalCollectable.useRate(
1311
+ Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 100.5 * index / 256)),
1312
+ 256,
1313
+ );
1314
+ const flat = tone.spectrum(256n, useRectangularWindow);
1315
+ const padded = tone.spectrum(1024n, useRectangularWindow);
1316
+
1317
+ flat.getResolution().getHertz(); // 1
1318
+ padded.getResolution().getHertz(); // 0.25
1319
+ round(flat.magnitudeAt(100).get()); // 82.1093
1320
+ round(flat.magnitudeAt(101).get()); // 80.8598
1321
+ round(padded.magnitudeAt(402).get()); // 128
1322
+ padded.frequencyAt(402).get().getHertz(); // 100.5
1323
+ ```
1324
+
1325
+ Zwei gleich starke Töne im Abstand von `0.4` Bins bleiben bei jeder Auffüllung verschmolzen: Mit
1326
+ 256 Werten und einem Hann-Fenster erzeugt Auffüllen auf 8192 nur ein lokales Maximum, nicht zwei.
1327
+
1328
+ **Die Hilbert-Transformation ist zirkulär.** `analyticSignal`, `envelope` und
1329
+ `instantaneousPhase` behandeln den ersten und den letzten Abtastwert als Nachbarn, sodass eine
1330
+ Aufzeichnung, deren Periode ihre Länge nicht teilt, an beiden Enden verunreinigt ist. Die Mitte
1331
+ ist verlässlich, die Enden nicht.
1332
+
1333
+ ```typescript
1334
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1335
+
1336
+ const round = (value: number): number => Number(value.toFixed(4));
1337
+ const edge = SynchronousSignalCollectable.useRate(
1338
+ Array.from({ length: 512 }, (_unused, index) => Math.cos(2 * Math.PI * 8.3 * index / 512)),
1339
+ 512,
1340
+ );
1341
+ const envelope = edge.envelope();
1342
+
1343
+ round(envelope.slice(8, -8).reduce((sum, value) => sum + value, 0) / 496); // 1.0025
1344
+ round(Math.max(...envelope.slice(0, 8))); // 1.2592
1345
+ round(Math.min(...envelope.slice(-8))); // 0.1314
1346
+ ```
1347
+
1348
+ **Eine verschwindende Hüllkurve definiert keine Phase.** Wo die Hüllkurve zusammenbricht, kann
1349
+ die Differenz zweier Rundungsrauschwinkel eine halbe Umdrehung betragen, was als halbe
1350
+ Abtastrate gelesen wird. Das Gate ist das, was die Zahl sinnvoll hält, und der Standard ist
1351
+ `1E-3` der Spitzenhüllkurve.
1352
+
1353
+ ```typescript
1354
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1355
+
1356
+ const round = (value: number): number => Number(value.toFixed(4));
1357
+ const beat = SynchronousSignalCollectable.useRate(
1358
+ Array.from({ length: 4096 }, (_unused, index) =>
1359
+ Math.cos(2 * Math.PI * 100 * index / 2048) + Math.cos(2 * Math.PI * 102 * index / 2048)),
1360
+ 2048,
1361
+ );
1362
+
1363
+ round(Math.max(...beat.instantaneousFrequency(2048, 0).map(Math.abs))); // 920.218
1364
+ round(Math.max(...beat.instantaneousFrequency().map(Math.abs))); // 101
1365
+ ```
1366
+
1367
+ **Eine unnormierte Korrelation ist keine Ähnlichkeit.** Die rohe Summe `r[k]` wächst mit der
1368
+ Aufzeichnungslänge, zwei Längen sind also nicht vergleichbar. Die Standardregel
1369
+ `"coefficient"` teilt durch `r[0]` und hält jeden Wert in `[-1, 1]`; `"unbiased"` teilt durch
1370
+ `count − k` und zahlt mit einer Varianz, die mit dem Lag wächst.
1371
+
1372
+ ```typescript
1373
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1374
+
1375
+ const round = (value: number): number => Number(value.toFixed(4));
1376
+ const periodic = SynchronousSignalCollectable.useRate(
1377
+ Array.from({ length: 1024 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 1024)),
1378
+ 1024,
1379
+ );
1380
+ const rho = periodic.autocorrelation(200n);
1381
+
1382
+ round(rho[0]); // 1
1383
+ round(rho[128]); // 0.875
1384
+ round(rho[64]); // -0.9375
1385
+ round(periodic.autocorrelation(200n, "none")[0]); // 512
1386
+ ```
1387
+
1388
+ Eine weitere Ablesung verdient eine Warnung: `spectralFlux` normiert jeden Frame vor dem
1389
+ Vergleich, eine reine Pegeländerung erzeugt also null Fluss. Sein erster und letzter Wert sind
1390
+ Randeffekte der Null-Grenze, keine Einsätze.
1391
+
1392
+ ---
1393
+
1394
+ <a id="15-license"></a>
1395
+ ## 15. Lizenz
1396
+
1397
+ MIT © Eloy Kim