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.es.md CHANGED
@@ -1,1381 +1,1381 @@
1
- # **Semantic-TypeScript**
2
-
3
- **Flujo, indexado.** Tus datos, bajo control preciso.
4
-
5
- [English](./readme.md) · [简体中文](./readme.cn.md) · [繁體中文](./readme.tw.md) · [日本語](./readme.jp.md) · [한국어](./readme.kr.md) · [Deutsch](./readme.de.md) · [Français](./readme.fr.md) · Español · [Русский](./readme.ru.md)
6
-
7
- Una biblioteca de procesamiento de flujos con seguridad de tipos que unifica **generadores de JavaScript**, **Java Stream** y
8
- el **direccionamiento de datos basado en índices**. Cada elemento lleva un índice `bigint`, cada canalización es perezosa y
9
- cada operación terminal es una opción explícita.
10
-
11
- ```bash
12
- npm install semantic-typescript
13
- ```
14
-
15
- - Cero dependencias en tiempo de ejecución, ESM puro, incluye sus propios tipos.
16
- - 684 pruebas de regresión: `npm test`.
17
- - Historial de cambios completo: [CHANGELOG.md](./CHANGELOG.md).
18
-
19
- ---
20
-
21
- ## Tabla de contenidos
22
-
23
- 1. [Filosofía de diseño](#1-design-philosophy)
24
- 2. [Arquitectura](#2-architecture)
25
- 3. [Inicio rápido](#3-quick-start)
26
- 4. [Funciones de fábrica](#4-factory-functions)
27
- 5. [Operadores de transformación](#5-transformation-operators)
28
- 6. [Collectables y operaciones terminales](#6-collectables-and-terminal-operations)
29
- 7. [Estadísticas y ventanas](#7-statistics-and-windows)
30
- 8. [El modelo de índices](#8-the-index-model)
31
- 9. [El modelo asíncrono](#9-the-asynchronous-model)
32
- 10. [Análisis periódico: anillos, campos y puntuaciones](#10-periodic-analysis-rings-fields-and-scores)
33
- 11. [Funciones auxiliares](#11-utility-helpers)
34
- 12. [Rendimiento y problemas comunes](#12-performance-and-pitfalls)
35
- 13. [Anillos de fase: un ciclo exacto](#13-phase-rings)
36
- 14. [Análisis de señales: ventanas, espectros y tiempo](#14-signal-analysis)
37
- 15. [Licencia](#15-license)
38
-
39
- ---
40
-
41
- <a id="1-design-philosophy"></a>
42
- ## 1. Filosofía de diseño
43
-
44
- Tres ideas, una sola canalización:
45
-
46
- **El índice es dato.** Cada elemento está direccionado por un `bigint`. La inversión, la rotación, el barajado y
47
- la partición son aritmética de índices, no operaciones de búfer. Nada se materializa hasta que un
48
- collectable lo pide.
49
-
50
- **La pereza es el valor predeterminado.** Una fábrica envuelve un generador; un operador envuelve otro generador. Ningún
51
- elemento se solicita hasta que se ejecuta una operación terminal, así que una fuente infinita es perfectamente
52
- legal siempre que algo aguas abajo la detenga.
53
-
54
- **Los terminales son explícitos.** `map` devuelve un `Semantic`, no un array. Los valores aparecen solo cuando
55
- nombras un collectable — `toUnordered()`, `toOrdered()`, `toNumericStatistics()`, `toWindow()` — y cada
56
- uno declara cuánto cuesta y qué orden produce.
57
-
58
- La misma disciplina se aplica a la mitad matemática de la biblioteca. Un `Complex`, un `Angle` o una
59
- `Frequency` es un valor inmutable cuyas invariantes se comprueban en la construcción; un `Optional`
60
- sustituye a `null`; y un anillo de fase declara exactamente cuántos elementos contiene, qué periodo tiene y
61
- qué equilibrado está.
62
-
63
- ---
64
-
65
- <a id="2-architecture"></a>
66
- ## 2. Arquitectura
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
- | Capa | Módulo | Responsabilidad |
76
- | :--- | :--- | :--- |
77
- | Fuentes | `synchronous/collectors`, `asynchronous/collectors` | Envolver un iterable, un generador, una promesa, un blob o un destino de eventos |
78
- | Canalizaciones | `synchronous/semantic`, `asynchronous/semantic` | Operadores perezosos y reescritura de índices |
79
- | Collectables | `synchronous/semantic`, `asynchronous/semantic` | Terminales ordenados, desordenados, de estadísticas, de ventanas y personalizados |
80
- | Colectores | `synchronous/collector`, `asynchronous/collector` | Las primitivas de plegado sobre las que se construye cada terminal |
81
- | Matemáticas | `mathematics/*` | Ángulos, complejos, frecuencias, espectros, ventanas, estimadores, epiciclos, anillos de fase |
82
- | Estructuras | `structure/*` | Listas, conjuntos, mapas, árboles, bosques, anillos, campos |
83
- | Errores | `structure/error` | La jerarquía tipada `SemanticError` y sus guardas de marca |
84
-
85
- Las familias síncrona y asíncrona comparten un solo vocabulario: los mismos nombres de operador, la misma
86
- semántica de índices, los mismos collectables. Las variantes asíncronas simplemente devuelven promesas y
87
- encolan las escrituras, de modo que una lectura emitida después de una escritura siempre la observa.
88
-
89
- ### 2.1 Qué movió y qué rompió esta versión
90
-
91
- Tres módulos cambiaron de sitio o de forma, y una serie de valores por defecto cambió de significado. Cada comportamiento antiguo sigue siendo alcanzable mediante una opción explícita; nada se eliminó sin un reemplazo.
92
-
93
- | Cambio | Antes | Ahora |
94
- | :--- | :--- | :--- |
95
- | Bosques | `structure/tree/rootedForest`, `structure/tree/unionFindForest` | `structure/forest/rootedForest`, `structure/forest/unionFindForest`; `tree/index.ts` ya no los reexporta |
96
- | Errores | errores nativos ad hoc | una jerarquía de quince clases en `structure/error.ts`, con una guarda de marca por clase y el alias `SemanticRangeError` |
97
- | Opciones de estimador | definiciones fijas | `mathematics/quantile.ts` exporta `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator` y `VarianceOptions` |
98
- | `quantile` | Hyndman–Fan tipo 1 | tipo 7 — R `quantile`, NumPy `linear`, Excel `PERCENTILE.INC` |
99
- | `skewness` / `kurtosis` | `g1` / `g2` | los corregidos por sesgo `G1` / `G2` |
100
- | `variance` / `standardDeviation` | `ddof = 0` | `ddof = 1`, envueltos en un `Optional` |
101
- | `decibel` | una función `20·log10` | eliminada; `amplitudeDecibel` (20·log10) y `powerDecibel` (10·log10) |
102
- | `Spectrum.flatness()` | magnitudes sobre ambas mitades | potencias sobre la mitad unilateral |
103
- | `Spectrum.bandEnergy()` | suma bilateral cruda | unilateral por defecto, plegando la mitad conjugada |
104
- | `shortTimeFourierTransform` / `spectrogram` | Hann simétrica, solo tramas completas | Hann periódica, `boundary: "zeros"`, `padded: true` |
105
-
106
- Los capítulos 6, 7 y 10 documentan los reemplazos; las consecuencias medidas están en [CHANGELOG.md](./CHANGELOG.md).
107
-
108
- ---
109
-
110
- <a id="3-quick-start"></a>
111
- ## 3. Inicio rápido
112
-
113
- ```typescript
114
- import {
115
- useOf, useFrom, useRange, useText, useStringify, usePromise
116
- } from "semantic-typescript";
117
-
118
- // Numeric aggregation
119
- const sum: number = useOf(10, 20, 30, 40)
120
- .map(n => n * 2)
121
- .toNumericStatistics()
122
- .summate(); // 200
123
-
124
- // BigInt aggregation
125
- const bigSum: bigint = useOf(10n, 20n, 30n, 40n)
126
- .map(n => n * 2n)
127
- .toBigintStatistics()
128
- .summate(); // 200n
129
-
130
- // Reordering is index arithmetic — collect with toOrdered() for it to take effect
131
- const reversed: number[] = useFrom([1, 2, 3, 4, 5]).reverse().toOrdered().toArray(); // [5,4,3,2,1]
132
- const rotated: number[] = useFrom([1, 2, 3, 4, 5]).translate(2).toOrdered().toArray(); // [4,5,1,2,3]
133
-
134
- // Lazy evaluation over a large range
135
- const multiples: bigint[] = useRange(0n, 1_000_000n)
136
- .filter(n => n % 17n === 0n)
137
- .limit(10n)
138
- .toUnordered()
139
- .toArray(); // [0n,17n,…,153n]
140
-
141
- // Unicode-safe string processing (code points, not UTF-16 units)
142
- useText("semantic 👍").toUnordered().forEach(character => console.log(character));
143
-
144
- // Circular-safe serialisation
145
- const cyclic: any = { a: 1 }; cyclic.self = cyclic;
146
- useStringify(cyclic); // '{"a":1}'
147
-
148
- // Terminals hand the elements to real structures, not only to arrays
149
- useOf(3, 1, 4, 1, 5).toCollectable().toOrderedSet().toArray(); // [1,3,4,5]
150
- useFrom([3, 1, 4, 1, 5]).toCollectable().toBinarySearchTree().size(); // 5n
151
-
152
- // Async sources
153
- const total = await usePromise(Promise.resolve(7)).toNumericStatistics().summate(); // 7
154
- ```
155
-
156
- ---
157
-
158
- <a id="4-factory-functions"></a>
159
- ## 4. Funciones de fábrica
160
-
161
- La construcción es siempre O(1) — una fábrica solo envuelve un generador. La tabla indica el coste de
162
- *consumir* `n` elementos, más el espacio adicional que la propia fuente necesita.
163
-
164
- | Fábrica | Firma (resultado) | Tiempo (consumir n) | Espacio (fuente) | Notas |
165
- | :--- | :--- | :--- | :--- | :--- |
166
- | `useOf` | `useOf<E>(...elements) → SynchronousSemantic<E>` | O(n) | O(n) | Los índices son 0..n−1; el argumento de propagación ya está materializado |
167
- | `useFrom` | `useFrom<E>(Iterable<E>) → SynchronousSemantic<E>`<br>`useFrom<E>(AsyncIterable<E>) → AsynchronousSemantic<E>` | O(n) | O(1) | Envuelve cualquier iterable; se itera una vez al recolectar |
168
- | `useRange` | `useRange<N>(start, end, step?) → SynchronousSemantic<N>` | O(n) | O(1) | `end` es exclusivo; `step` debe ser distinto de cero; tanto `number` como `bigint` funcionan |
169
- | `useFill` | `useFill<E>(element \| supplier, count) → SynchronousSemantic<E>` | O(n) | O(1) | El proveedor se llama una vez por elemento |
170
- | `useEmpty` | `useEmpty<E>() → SynchronousSemantic<E>` | O(1) | O(1) | Elemento neutro para `concat` |
171
- | `useText` | `useText(text, delimiter?)` | O(n) | O(n) para el texto | Itera **puntos de código**, por lo que los emoji y los CJK siguen siendo elementos únicos |
172
- | `useAttribute` | `useAttribute<T>(object)` | O(v + e) | O(d + v) | Recorre recursivamente objetos anidados con protección contra ciclos |
173
- | `useGenerate` | `useGenerate<E>(supplier, interrupt)` | O(n) × coste del proveedor | O(1) | **Debe** tener un `interrupt` de parada o un `limit` aguas abajo |
174
- | `useIterate` | `useIterate<E>(Iterable<E>)` / `(AsyncIterable<E>)` | O(n) por recolección | O(1) | Reproduce el iterador en cada recolección |
175
- | `useInterval` | `useInterval(period, delay?) → AsynchronousSemantic<number>` | n × period (tiempo real) | O(1) | Emite el tiempo transcurrido |
176
- | `usePromise` | `usePromise<T>(promise) → AsynchronousSemantic<T>` | O(1) | O(1) | Un único elemento en el índice `0n` |
177
- | `useBlob` | `useBlob(blob, chunk?) → AsynchronousSemantic<Uint8Array>` | O(blob size) | O(chunk) por copia | El fragmento predeterminado es 64 KiB |
178
- | `useSubscription` | `useSubscription(target, subscriber, event) → AsynchronousSemantic<Event>` | un elemento por evento | O(1) | `limit()` / `takeWhile()` definen el límite de desuscripción |
179
-
180
- ```typescript
181
- useOf(1, 2, 3).toUnordered().toArray(); // [1,2,3]
182
- useFrom(new Set([1, 2, 2])).toUnordered().toArray(); // [1,2]
183
- useRange(0, 5).toUnordered().toArray(); // [0,1,2,3,4]
184
- useRange(10n, 0n, -5n).toUnordered().toArray(); // [10n,5n]
185
- useFill("x", 3n).toUnordered().toArray(); // ["x","x","x"]
186
- useText("A😊B").toUnordered().toArray(); // ["A","😊","B"]
187
- useGenerate(() => 7, (_e, index) => index >= 3n).toUnordered().toArray(); // [7,7,7]
188
- ```
189
-
190
- ---
191
-
192
- <a id="5-transformation-operators"></a>
193
- ## 5. Operadores de transformación
194
-
195
- Cada operador devuelve un nuevo `Semantic`; ninguno toca un elemento hasta que se ejecuta un collectable. El tiempo es
196
- el coste adicional **por elemento**; el espacio es lo que el operador retiene mientras la canalización se ejecuta.
197
-
198
- | Operador | Propósito | Efecto en el índice | Tiempo | Espacio |
199
- | :--- | :--- | :--- | :--- | :--- |
200
- | `map(mapper)` | transformación 1:1 | reenviado | O(1) | O(1) |
201
- | `filter(predicate)` | conserva los elementos que coinciden | reenviado | O(1) | O(1) |
202
- | `flat(mapper)` / `flatMap(mapper)` | aplanado 1:N | reindexado desde 0 | O(1) por elemento emitido | O(1) |
203
- | `distinct(keyExtractor?)` | descarta claves repetidas, conserva la primera | reenviado | O(1) promedio | O(n) |
204
- | `peek(consumer)` | efecto secundario, el elemento pasa a través | reenviado | O(1) | O(1) |
205
- | `limit(count)` | toma los primeros *n* | reindexado desde 0 | O(1) | O(1) |
206
- | `skip(count)` | descarta los primeros *n* | reenviado | O(1) | O(1) |
207
- | `sub(start, end)` | rebanada posicional `[start, end)` | reenviado | O(1) | O(1) |
208
- | `takeWhile(predicate)` | toma mientras el predicado se cumple | reenviado | O(1) | O(1) |
209
- | `dropWhile(predicate)` | descarta mientras el predicado se cumple | reindexado desde 0 | O(1) | O(1) |
210
- | `concat(other)` | añade un semantic o un iterable | continúa el índice | O(1) | O(1) |
211
- | `redirect(redirector)` | calcula un nuevo índice como quieras | **el tuyo** | O(1) | O(1) |
212
- | `reverse()` | `redirect((_e, i) => -i - 1n)` | negado y desplazado | O(1) | O(1) |
213
- | `translate(offset \| mapper)` | desplaza cada índice | `i + offset` | O(1) | O(1) |
214
- | `shuffle(mapper?)` | aleatoriza el índice | aleatorio | O(1) | O(1) |
215
- | `pipe(conversion)` | pasa el generador a una función personalizada | según tú | el tuyo | el tuyo |
216
- | `semantic()` | eleva un collectable de nuevo a un semantic | sin cambios | O(1) | O(1) |
217
-
218
- **`reverse`, `translate` y `shuffle` solo reescriben índices.** Nada se almacena en búfer ni se rota hasta que
219
- `toOrdered()` normaliza los índices módulo el número de elementos:
220
-
221
- ```typescript
222
- useFrom([1, 2, 3, 4, 5]).reverse().toOrdered().toArray(); // [5,4,3,2,1]
223
- useFrom([1, 2, 3, 4, 5]).translate(2).toOrdered().toArray(); // [4,5,1,2,3] (rotation)
224
- useFrom([1, 2, 3, 4, 5]).translate(7).toOrdered().toArray(); // [4,5,1,2,3] (7 ≡ 2 mod 5)
225
- useFrom([1, 2, 3]).shuffle((_e, i) => -i - 1n).toOrdered().toArray(); // [3,2,1]
226
- ```
227
-
228
- ---
229
-
230
- <a id="6-collectables-and-terminal-operations"></a>
231
- ## 6. Collectables y operaciones terminales
232
-
233
- ### 6.1 Collectables
234
-
235
- El collectable posee tanto el **orden** como el **control de índices**. `n` es el número de elementos.
236
-
237
- | Collectable | Estructura | Tiempo | Orden | Úsalo cuando |
238
- | :--- | :--- | :--- | :--- | :--- |
239
- | `toUnordered()` | `Map<bigint, E>` indexado por índice | O(n) | inserción | máximo rendimiento; los índices sin procesar se conservan intactos |
240
- | `toOrdered()` | array ordenado por índice de elemento | O(n log n) | por índice normalizado | la inversión o la rotación deben surtir efecto |
241
- | `toOrdered(comparator)` | ordenado por tu comparador | O(n log n) | por tu comparador | vistas ordenadas |
242
- | `toNumericStatistics()` | búfer ordenado por valor | O(n log n) | por valor | estadísticas sobre `number` |
243
- | `toBigintStatistics()` | búfer ordenado por valor | O(n log n) | por valor | estadísticas sobre `bigint` |
244
- | `toWindow()` | búfer ordenado por valor | O(n log n) | por valor | `slide` / `tumble` |
245
- | `toCollectable(mapper)` | lo que devuelva el mapper | — | — | collectables personalizados |
246
-
247
- ### 6.2 Terminales
248
-
249
- Cada terminal vive en el **collectable**, no en el `Semantic`. Eso es lo que «los terminales son explícitos» significa en la práctica: una canalización no produce nada hasta que se nombra un collectable, y `toCollectable()` sin mapper es la forma más barata de nombrarlo.
250
-
251
- | Terminal | Devuelve | Tiempo | Notas |
252
- | :--- | :--- | :--- | :--- |
253
- | `toArray()` / `toSet()` | `Array<E>` / `Set<E>` | O(n) | Vacía el búfer; `toSet` desduplica por identidad |
254
- | `toMap(key, value?)` | `Map<K, V>` | O(n) promedio | Gana la última escritura |
255
- | `count()` / `isEmpty()` | `bigint` / `boolean` | O(1) | El búfer ya lo sabe |
256
- | `forEach(action)` | `void` | O(n) | `action(element, index)` |
257
- | `join(…)` | `string` | O(n) | `join()`, `join(delimiter)`, `join(prefix, delimiter, suffix)` |
258
- | `reduce(…)` | `Optional<E>` / `E` / `R` | O(n) | Flujo vacío → `Optional` vacío |
259
- | `group(classifier)` / `groupBy(key, value)` | `Map<K, E[]>` | O(n) promedio | — |
260
- | `partition(count)` | `Array<Array<E>>` | O(n) | Round-robin por índice |
261
- | `partitionBy(classifier)` | `Array<Array<E>>` | O(n) | El número de cubo proviene del clasificador |
262
- | `anyMatch` / `allMatch` / `nonMatch` | `boolean` | O(n) peor caso, con cortocircuito | — |
263
- | `findAny` / `findFirst` / `findLast` | `Optional<E>` | O(1)–O(n) | Posicional en orden materializado |
264
- | `findAt(k)` / `findNegativeAt(k)` | `Optional<E>` | O(k) / O(n−k) | Cuenta desde el principio / desde el final |
265
- | `findMaximum(cmp?)` / `findMinimum(cmp?)` | `Optional<E>` | O(n) | Por defecto usa una comparación estructural |
266
- | `frequency()` | `Map<E, bigint>` | O(n) promedio | Solo collectables de estadísticas |
267
- | `write(stream, accumulator?)` | `WritableStream<S>` | O(n) | Terminal de tipo IO |
268
- | `collect(collector)` | `R` | O(n) | El plegado genérico detrás de cada terminal anterior |
269
-
270
- ### 6.3 Colectores de estructura
271
-
272
- `toArray`, `toSet` y `toMap` terminan una canalización en un contenedor de JavaScript. Otros doce terminales la terminan en una de las estructuras de la propia biblioteca, de modo que el resultado lleva las invariantes que esa estructura garantiza en lugar de las que un array tiene por casualidad. Están disponibles en cada collectable, incluido el collectable desnudo que devuelve `toCollectable()`.
273
-
274
- | Terminal | Devuelve | Orden | Coste |
275
- | :--- | :--- | :--- | :--- |
276
- | `toArray()` | `Array<E>` | orden de recorrido | O(n) |
277
- | `toSet()` | `Set<E>` | orden de inserción, SameValueZero | O(n) promedio |
278
- | `toMap(key, value?)` | `Map<K, V>` | orden de inserción; gana la última escritura | O(n) promedio |
279
- | `toOrderedSet(comparator?)` | `SynchronousOrderedSet<E>` | ascendente según el comparador | O(n²) peor caso |
280
- | `toUnorderedSet()` | `SynchronousUnorderedSet<E>` | sin especificar (cubos de hash) | O(n) esperado |
281
- | `toOrderedMap(key, value?)` | `SynchronousOrderedMap<K, V>` | orden de inserción; gana la última escritura | O(n) esperado |
282
- | `toUnorderedMap(key, value?)` | `SynchronousUnorderedMap<K, V>` | sin especificar | O(n) esperado |
283
- | `toList()` | `SynchronousList<E>` | orden de recorrido, duplicados conservados | O(n) |
284
- | `toRing(span?)` | `SynchronousRing<E>` | el orden de recorrido fija las posiciones en el anillo | O(n) |
285
- | `toLRUSet(capacity)` | `SynchronousUnorderedLRUSet<E>` | por recencia; solo sobreviven los últimos `capacity` | O(n²) peor caso |
286
- | `toLRUMap(capacity, key, value?)` | `SynchronousUnorderedLRUMap<K, V>` | por recencia | O(n) esperado |
287
- | `toSkipSet(comparator?)` | `SynchronousSkipSet<E>` | ascendente según el comparador, navegable | O(n²) peor caso |
288
- | `toSkipMap(key, value?)` | `SynchronousSkipMap<K, V>` | según el comparador de claves, navegable | O(n) esperado |
289
- | `toBinarySearchTree(comparator?)` | `SynchronousBinarySearchTree<E>` | ascendente según el comparador, equilibrado en altura | O(n log n) |
290
- | `toRootedForest()` | `SynchronousRootedForest<E>` | el orden de recorrido se vuelve la estructura | O(n) |
291
-
292
- El tipo del resultado sigue el tipo de elemento del propio collectable, así que ningún terminal recibe un argumento de tipo: un `SynchronousCollectable<E>` entrega directamente `Array<E>`, `SynchronousList<E>` o `SynchronousOrderedSet<E>`. Solo quedan los argumentos de negocio: el comparador de `toOrderedSet` / `toSkipSet` / `toBinarySearchTree`, la capacidad del par LRU, el span de `toRing` y los extractores de clave y valor de la familia de mapas.
293
-
294
- Un anillo es un conjunto, así que los elementos repetidos conservan su primera posición; el resto de estructuras de arriba conserva duplicados salvo que su propio contrato diga otra cosa (`toOrderedSet`, `toUnorderedSet` y `toSkipSet` son conjuntos; `toBinarySearchTree` y `toList` no lo son).
295
-
296
- ```typescript
297
- const numbers = useOf(3, 1, 4, 1, 5).toCollectable();
298
-
299
- numbers.toOrderedSet().toArray(); // [1,3,4,5]
300
- numbers.toUnorderedSet().size(); // 4n — the duplicate 1 collapses
301
- numbers.toList().toArray(); // [3,1,4,1,5]
302
- numbers.toSkipSet().first().get(); // 1 — navigable by comparator
303
- numbers.toBinarySearchTree().size(); // 5n — duplicates retained
304
- numbers.toRootedForest().getRoots().length; // 1
305
- useOf(1, 2, 3).toCollectable().toRing().phaseAt(3n).get().toDegreeValue(); // 0
306
- useOf(3, 1, 3).toCollectable().toOrderedMap(n => n, n => `v${n}`).get(3).get(); // "v3"
307
- ```
308
-
309
- Los collectables asíncronos exponen los mismos terminales y resuelven cada uno en una promesa, así que la forma de la llamada solo gana un `await`.
310
-
311
- ---
312
-
313
- <a id="7-statistics-and-windows"></a>
314
- ## 7. Estadísticas y ventanas
315
-
316
- ### 7.1 La superficie estadística
317
-
318
- ```typescript
319
- useOf(3, 1, 4, 1, 5).toNumericStatistics().summate(); // 14
320
- useOf(3, 1, 4, 1, 5).toNumericStatistics().average(); // 2.8
321
- useOf(3, 1, 4, 1, 5).toNumericStatistics().median(); // 3
322
- useOf(3, 1, 4, 1, 5).toNumericStatistics().mode(); // 1
323
- useOf(3, 1, 4, 1, 5).toNumericStatistics().variance().get(); // 3.2 (n − 1)
324
- useOf(3, 1, 4, 1, 5).toNumericStatistics().standardDeviation().get(); // 1.7889 (n − 1)
325
- ```
326
-
327
- Ambas familias estadísticas exponen la misma superficie — `summate`, `average`, `median`, `mode`, `variance`, `standardDeviation`, `frequency`, `findMaximum`, `findMinimum` — con aritmética `bigint` para `toBigintStatistics()`. Las medidas de dispersión y la familia de cuantiles devuelven un `Optional`, porque una muestra puede ser demasiado pequeña para que estén definidas; desenvuélvelas con `.get()`, `.orElse(…)` o `.isPresent()`.
328
-
329
- ### 7.2 Estimadores y valores por defecto
330
-
331
- Los valores por defecto son las definiciones que usan R, NumPy, pandas y Excel, así que un valor calculado aquí se puede comparar con esas herramientas sin factor de corrección.
332
-
333
- | Estadístico | Defecto | Antes | Recuperar el valor antiguo con |
334
- | :--- | :--- | :--- | :--- |
335
- | `quantile(probability)` | Hyndman–Fan tipo 7 | tipo 1 | `{ method: "type-1" }` |
336
- | `interquartileRange()` | Hyndman–Fan tipo 7 | tipo 1 | `{ method: "type-1" }` |
337
- | `skewness()` | `G1` corregido por sesgo | `g1` | `{ estimator: "g1" }` |
338
- | `kurtosis()` | `G2` de exceso corregido por sesgo | `g2` | `{ estimator: "g2" }` |
339
- | `variance()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
340
- | `standardDeviation()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
341
-
342
- Los nueve tipos de Hyndman–Fan están disponibles como `"type-1"` … `"type-9"`, y `{ excess: false }` vuelve a sumar tres a la curtosis para informar del cuarto momento estandarizado no excesivo. Los tipos de opciones — `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator`, `VarianceOptions` — los exporta `mathematics/quantile.ts`.
343
-
344
- ```typescript
345
- const sample = useOf(1, 2, 3, 4).toNumericStatistics();
346
-
347
- sample.variance().get(); // 1.6667 (5/3, ddof = 1)
348
- sample.variance({ ddof: 0 }).get(); // 1.25
349
- sample.standardDeviation().get(); // 1.2910
350
- sample.skewness().get(); // 0
351
- sample.kurtosis().get(); // -1.2 (G2)
352
- sample.kurtosis({ estimator: "g2" }).get(); // -1.36
353
- sample.kurtosis({ excess: false }).get(); // 1.8
354
-
355
- const quartiles = useOf(1, 2, 3, 4, 5, 6, 7, 8).toNumericStatistics();
356
- quartiles.quantile(0.75).get(); // 6.25 (type 7)
357
- quartiles.quantile(0.75, { method: "type-1" }).get(); // 6 (nearest rank)
358
- quartiles.interquartileRange().get(); // 3.5
359
- ```
360
-
361
- `G1` y `G2` no están definidos por debajo de tres y cuatro observaciones, y la varianza y la desviación típica muestrales tampoco por debajo de dos con `ddof = 1`. Eso se informa con un `Optional` vacío en lugar de con un cero engañoso:
362
-
363
- ```typescript
364
- useOf(1, 2, 3).toNumericStatistics().kurtosis().isEmpty(); // true (G2 needs four observations)
365
- useOf(1, 2).toNumericStatistics().skewness().isEmpty(); // true (G1 needs three)
366
- useOf(1).toNumericStatistics().variance().isEmpty(); // true (ddof = 1 needs two)
367
- useOf(1).toNumericStatistics().variance({ ddof: 0 }).get(); // 0
368
- ```
369
-
370
- Las vistas bigint aceptan las mismas opciones y redondean cada resultado a un `bigint` entero.
371
-
372
- ### 7.3 Ventanas
373
-
374
- Las ventanas vuelven a exponer los elementos almacenados en el búfer como una canalización de canalizaciones, de modo que una ventana se puede consumir sin materializarla antes en un array. Ambas formas incluyen una última ventana parcial.
375
-
376
- ```typescript
377
- useFrom([1, 2, 3, 4]).toWindow().slide(2n, 1n).toUnordered().toArray()
378
- .map(window => window.toUnordered().toArray()); // [[1,2],[2,3],[3,4],[4]]
379
-
380
- useFrom([1, 2, 3, 4]).toWindow().tumble(2n).toUnordered().toArray()
381
- .map(window => window.toUnordered().toArray()); // [[1,2],[3,4]]
382
- ```
383
-
384
- ---
385
-
386
- <a id="8-the-index-model"></a>
387
- ## 8. El modelo de índices
388
-
389
- Un índice es un `bigint` asociado a cada elemento. Los operadores lo **reenvían**, lo **reindexan**
390
- desde cero o te dejan **calcularlo**.
391
-
392
- La recolección ordenada normaliza cada índice módulo el número de elementos y luego ordena por el resultado. Por eso
393
- `translate(7)` sobre cinco elementos equivale a `translate(2)`: `7 ≡ 2 (mod 5)`.
394
-
395
- La recolección desordenada conserva el índice que produjo la canalización y devuelve los elementos en orden de llegada. Los
396
- índices negativos son legales y, una vez normalizados, simplemente ordenan antes que los no negativos.
397
-
398
- ---
399
-
400
- <a id="9-the-asynchronous-model"></a>
401
- ## 9. El modelo asíncrono
402
-
403
- `AsynchronousSemantic` refleja la superficie síncrona devolviendo promesas. Las escrituras se encolan, así que una
404
- lectura emitida después de una escritura siempre la observa.
405
-
406
- ```typescript
407
- const ring = new AsynchronousRing([1, 2]);
408
- const added = ring.add(3); // queued
409
- const sampled = ring.elementAt(2n); // observes the write above
410
- await added;
411
- (await sampled).get().element; // 3
412
- ```
413
-
414
- Las fábricas que son asíncronas por naturaleza — `usePromise`, `useBlob`, `useInterval`,
415
- `useSubscription` — devuelven `AsynchronousSemantic` directamente, y `useFrom` / `useIterate` se
416
- actualizan solas cuando reciben un `AsyncIterable`.
417
-
418
- ---
419
-
420
- <a id="10-periodic-analysis-rings-fields-and-scores"></a>
421
- ## 10. Análisis periódico: anillos, campos y puntuaciones
422
-
423
- La mitad matemática de la biblioteca trata un ciclo como un objeto de primera clase en lugar de una tabla
424
- de consulta. Tres construcciones trabajan juntas.
425
-
426
- ### 10.1 Un anillo es un entero
427
-
428
- Un `Ring` es un **anillo n-ésimo** (n-fold ring): el número de elementos, el periodo posicional y el número de fases
429
- distintas son el mismo entero `n`. La fase de una posición se calcula a partir de su residuo exacto, así que el
430
- anillo es periódico hasta el bit:
431
-
432
- ```typescript
433
- import { SynchronousRing } from "semantic-typescript";
434
-
435
- const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
436
- ring.phaseAt(0n).get().toDegreeValue(); // 0
437
- ring.phaseAt(1n).get().toDegreeValue(); // 60
438
- ring.phaseAt(6n).get().getValue() === ring.phaseAt(0n).get().getValue(); // true, exactly
439
- ring.purity(); // 1
440
- ring.isBalanced(); // true
441
- ring.sequences().map(z => z.magnitude()); // [0, 6, 0, 0, 0, 0]
442
- ```
443
-
444
- `frequency` es la tasa de rotación **ambiente**, no un paso de fase entre posiciones: el entorno
445
- desplaza todos los fasores un mismo ángulo, así que nunca puede romper el patrón intrínseco. `span` fija cuántas
446
- vueltas ocupa el anillo.
447
-
448
- `purity()` es la proporción de energía transportada por el modo fundamental,
449
- `max(|c₁|², |c_{n−1}|²) / (n · Σ|z|²)`. Vale `1` exactamente cuando el anillo es la raíz n-ésima de la unidad
450
- salvo rotación, y cuesta `O(n)` porque solo se necesitan dos bins. Se prueban ambas direcciones
451
- porque un anillo puede recorrer el círculo en cualquiera de los dos sentidos.
452
-
453
- ### 10.2 Un campo es una puntuación acotada
454
-
455
- Un `PhaseField` asigna una fase relativa a una puntuación en `[-1, 1]`:
456
-
457
- ```typescript
458
- import { useField, useRadian } from "semantic-typescript";
459
-
460
- const cosine = useField([1]); // cos(2πΔ): +1 aligned, −1 opposed
461
- const mixed = useField([0.4, 0.3, 0.3]); // first three harmonics
462
-
463
- cosine.sample(useRadian(0)); // 1
464
- cosine.sample(useRadian(Math.PI)); // -1
465
- ```
466
-
467
- La puntuación es una serie de cosenos truncada, `a₀ + Σ wₘ·cos(2πmΔ)`, y el constructor impone
468
- `|a₀| + Σ|wₘ| ≤ 1`. Por la desigualdad triangular la puntuación nunca puede salir de `[-1, 1]`: la cota es una
469
- **consecuencia de la construcción**, no un recorte aplicado a posteriori.
470
-
471
- La razón de que el campo sea una serie y no una tabla: una tabla de `n` puntos reproduce exactamente sus propias
472
- muestras, pero su interpolante trigonométrico se pasa entre ellas. Una tabla de cinco puntos con puntuaciones
473
- `[+1, +0.5, −1, −0.5, 0]` interpola sobre `[−1.0428, 1.0041]`, lo que ya rompe la promesa.
474
- Una serie acotada no puede.
475
-
476
- ### 10.3 El análisis mide los datos contra cada elemento
477
-
478
- Cada elemento del anillo es una fase de referencia. `analyse` sitúa cada observación de vuelta en el anillo y
479
- la mide contra cada referencia:
480
-
481
- ```typescript
482
- const ring = new SynchronousRing(["a", "b", "c", "d", "e"]);
483
- const report = ring.analyse(["a", "c", "c", "a", "zz", "e"]);
484
-
485
- report.matched; // 5
486
- report.missed; // 1 ('zz' is not on the ring)
487
- report.references.map(r => r.count); // [2, 0, 2, 0, 1]
488
- ```
489
-
490
- Cada fila de referencia informa de `count`, la fase relativa media `mean` y la longitud resultante media
491
- `resultant`. Merece la pena recordar una identidad: **`resultant` es la misma para todas las referencias**,
492
- porque cambiar la línea base solo rota todas las fases relativas en una constante. Lo que difiere por
493
- referencia es el recuento y la media.
494
-
495
- ```typescript
496
- const ring = new SynchronousRing(["a", "b", "c"]);
497
- ring.scoresAt(0, cosine); // one score per position, in [-1, 1]
498
- ```
499
-
500
- ### 10.4 Instrumentos relacionados
501
-
502
- | Llamada | Qué responde | Coste |
503
- | :--- | :--- | :--- |
504
- | `purity()` | ¿el anillo sigue siendo la raíz n-ésima de la unidad? | O(n) |
505
- | `resultant(t?)` | ¿cómo de concentrados están los fasores ahora mismo? | O(n) |
506
- | `analyse(data)` | ¿cómo se sitúan estos datos frente a cada elemento? | O(n · m) |
507
- | `toSpectrum()` | el anillo como espectro, con el número de onda en el eje de frecuencias | O(n²) |
508
- | `useAngularSpectrum(ring, instants, plan)` | el plano `(ω, n)` a lo largo del tiempo | O(n² · frames) |
509
-
510
- Otros dos primitivos completan el modelo: `combineRings(a, b)` une dos anillos en su mínimo común
511
- múltiplo (un pullback, no un producto: los pares que no pueden compartir un instante simplemente están ausentes), y
512
- `rate()` da a cada elemento su propia frecuencia intrínseca para que un anillo pueda derivar respecto al
513
- entorno. `jetAt`, `windingAt` y `errorIntegralAt` exponen el estado diferencial de esa deriva.
514
-
515
- ### 10.5 Ventanas espectrales
516
-
517
- Las construcciones de anillos son una mitad de la superficie analítica; la otra trabaja sobre señales muestreadas. Su primitiva más reutilizada es la ventana, y cada forma de aquí tiene dos variantes. La variante simétrica divide la trama por `count − 1`; la variante periódica (DFT par) la divide por `count`, un muestreo menos que un periodo completo, de modo que el taper es continuo bajo la extensión periódica que supone la DFT.
518
-
519
- ```typescript
520
- useHannWindow(1, 4); // 0.75 symmetric
521
- useHannPeriodicWindow(1, 4); // 0.5 periodic
522
- coherentGain(useHannPeriodicWindow, 16); // 0.5
523
- equivalentNoiseBandwidth(useHannPeriodicWindow, 16); // 1.5
524
- ```
525
-
526
- | Forma | Simétrica | Periódica | Notas |
527
- | :--- | :--- | :--- | :--- |
528
- | Rectangular | `useRectangularWindow` | — | sin taper; sin distinción periódica |
529
- | Hann | `useHannWindow` | `useHannPeriodicWindow` | la opción por defecto del análisis de corto plazo |
530
- | Hamming | `useHammingWindow` | `useHammingPeriodicWindow` | — |
531
- | Blackman | `useBlackmanWindow` | `useBlackmanPeriodicWindow` | — |
532
- | Nuttall | `useNuttallWindow` | `useNuttallPeriodicWindow` | Blackman–Harris de cuatro términos |
533
- | Flat-top | `useFlatTopWindow` | `useFlatTopPeriodicWindow` | precisión de amplitud antes que resolución |
534
- | Kaiser | `useKaiserWindow(beta)` | `useKaiserPeriodicWindow(beta)` | `beta` fija el ancho del lóbulo principal |
535
-
536
- Usa la variante periódica para el análisis y el overlap-add: con un salto de media trama, la ventana de Hann periódica suma `1.000000` a cualquier longitud, mientras que la simétrica ondula `0.841 dB` con `N = 16` y todavía `0.013 dB` con `N = 1024`. Conserva la variante simétrica para el diseño de filtros, donde la trama no es un periodo del taper.
537
-
538
- Las ventanas flat-top devuelven **pesos negativos** cerca de los extremos de la trama, porque una ventana flat-top es exactamente eso. Se devuelven sin recortar, porque recortarlos perturba el lóbulo principal y destruye la baja pérdida de scalloping que justifica la existencia de la ventana:
539
-
540
- ```typescript
541
- useFlatTopWindow(0, 1024); // -0.000421 — negative by definition
542
- Array.from({ length: 1024 }, (_unused, index) => useFlatTopWindow(index, 1024))
543
- .filter(weight => weight < 0).length; // 578
544
- ```
545
-
546
- ### 10.6 Niveles, planitud y energía de banda
547
-
548
- `decibel` ha desaparecido como función única, porque una amplitud y una potencia no comparten factor. `amplitudeDecibel` aplica `20·log10` a una razón de tipo tensión; `powerDecibel` aplica `10·log10` a una potencia, una energía, un cuadrado medio o una densidad espectral de potencia:
549
-
550
- ```typescript
551
- amplitudeDecibel(0.25); // -12.0412
552
- powerDecibel(0.25); // -6.0206
553
- ```
554
-
555
- `Bin.decibel` y `Spectrum.decibelOf` son niveles de amplitud; `Spectrum.powerDecibelOf` es el nivel de potencia correspondiente. Dar una potencia a la forma de amplitud informa `20·log10(power)` y duplica el nivel en decibelios.
556
-
557
- Dos medidas del espectro cambiaron de lectura:
558
-
559
- ```typescript
560
- const twoBins = new Spectrum(new Sample(2, 1), [new Complex(2, 0), new Complex(1, 0)]);
561
- twoBins.flatness(); // 0.8 — geometric over arithmetic mean of power
562
-
563
- const shape = new Spectrum(new Sample(4, 1), [
564
- new Complex(1, 0), new Complex(2, 0), new Complex(0, 0), new Complex(0, 0),
565
- ]);
566
- shape.bandEnergy(new Frequency(0), new Frequency(2)); // 9
567
- shape.bandEnergy(new Frequency(0), new Frequency(2), "two-sided"); // 5
568
- ```
569
-
570
- `flatness()` compara las medias geométrica y aritmética de `power()` sobre la mitad unilateral del espectro, no la media de magnitudes sobre ambas mitades, y el doblado de amplitud de `oneSided()`, que es solo para visualización, no se aplica deliberadamente. `bandEnergy()` usa por defecto la lectura unilateral, plegando la mitad conjugada: DC y Nyquist cuentan una vez y cada bin interior cuenta dos; una banda que cubre todo el eje satisface entonces Parseval, `Σ|X|² = N·Σx²`. Pasa `"two-sided"` para un espectro complejo, cuyos bins ya son independientes.
571
-
572
- ### 10.7 Análisis de corto plazo
573
-
574
- `Sampler.shortTimeFourierTransform` y `Sampler.spectrogram` siguen las convenciones de `stft` de SciPy: ventana de Hann periódica, borde relleno de ceros en ambos extremos y cola rellena de ceros.
575
-
576
- ```typescript
577
- const sampler = new Sampler(new Sample(64, 1));
578
- const spectrogram = sampler.spectrogram(time => Math.cos(2 * Math.PI * 4 * time), 16, 8);
579
-
580
- spectrogram.size(); // 9 frames
581
- spectrogram.getBoundary(); // "zeros"
582
- spectrogram.getTime(6); // 0.75 — frame centres, not frame starts
583
- sampler.spectrogram(time => Math.cos(2 * Math.PI * 4 * time), 16, 8, useHannWindow,
584
- { boundary: "none", padded: false }).size(); // 7 — complete frames only
585
- ```
586
-
587
- La extensión centra la primera trama en el primer muestreo, y el relleno de cola mantiene alcanzables los últimos muestreos: 64 muestreos con trama 16 y salto 8 producen 9 tramas en vez de 7, y 1000 muestreos con trama 256 y salto 128 producen 9 en vez de 6. Los tiempos de trama son centros — `(k·hop + (frameSize mod 2)/2)/rate` para una vista extendida, `(k·hop + frameSize/2)/rate` para una no extendida — y `toFrame()` recorta la extensión antes de invertir.
588
-
589
- ---
590
-
591
- <a id="11-utility-helpers"></a>
592
- ## 11. Funciones auxiliares
593
-
594
- ### 11.1 La jerarquía de errores
595
-
596
- `structure/error.ts` exporta una jerarquía de quince clases para los errores lanzados *alrededor* de una canalización: por tu propio código, un colector personalizado o un adaptador.
597
-
598
- ```text
599
- SemanticError
600
- ├── IllegalOperationError
601
- ├── ValidationError
602
- │ ├── IndexError
603
- │ ├── RangeError (also exported as SemanticRangeError)
604
- │ └── ConfigurationError
605
- ├── CapacityError
606
- ├── StateError
607
- │ └── IllegalStateError
608
- ├── ConcurrencyError
609
- ├── CollectionError
610
- ├── SerializationError
611
- ├── NumericError
612
- │ └── ArithmeticError
613
- └── PhaseError
614
- ```
615
-
616
- Cada clase fija `name`, reenvía la `cause` nativa, lleva una bolsa `context` opcional y se serializa mediante `toJSON()`; cada una tiene una guarda de marca con su mismo nombre.
617
-
618
- ```typescript
619
- const error = new CapacityError("LRU capacity exhausted", { context: { capacity: 0 } });
620
-
621
- error.name; // "CapacityError"
622
- isCapacityError(error); // true
623
- isValidationError(error); // false — a capacity failure is not a validation failure
624
- isSemanticError(error); // true
625
- error.toJSON(); // { name: "CapacityError", message: "…", context: { capacity: 0 } }
626
- ```
627
-
628
- `SemanticRangeError` es la misma clase que el `RangeError` exportado, así que `instanceof` se cumple con ambos nombres; importar `RangeError` ensombrece el constructor global en todo el módulo, y por eso existe el alias. Ninguna de las dos clases la lanza la comprobación de argumentos de la biblioteca, que sigue lanzando `TypeError` y `RangeError` nativos: la jerarquía es el vocabulario de la capa de arriba.
629
-
630
- ### 11.2 Tabla de utilidades
631
-
632
- | Auxiliar | Propósito |
633
- | :--- | :--- |
634
- | `useCompare(a, b)` | Orden total entre primitivos mixtos |
635
- | `useToNumber` / `useToBigInt` | Conversión numérica sin pérdida y con comprobación de rango |
636
- | `useRandom` | Auxiliares aleatorios con semilla o sin ella |
637
- | `useTraverse` | Recorrido estructural de objetos y arrays anidados |
638
- | `useStringify` | Serialización JSON segura ante referencias circulares |
639
- | `useHash` / `maskOf` / `register` / `unregister` | Hash estructural y el registro de estrategias de hash |
640
- | `invalidate` / `validate` | Guardas que lanzan una forma de error coherente |
641
- | `Optional<T>` | El sustituto de `null` de la biblioteca: `Optional.of(x)` / `Optional.empty()` |
642
-
643
- ### 11.3 `useTraverse`
644
-
645
- `useTraverse(target, callback)` aplana cualquier objeto en sus **hojas no nulas**. Desciende por los objetos anidados, recorre los valores iterables por posición, omite `null` y `undefined` y nunca vuelve a entrar en un objeto que ya visitó; así, un grafo de objetos con un ciclo termina en lugar de desbordar la pila.
646
-
647
- ```typescript
648
- const source = { id: 7, tags: ["a", "b"], owner: { name: "Ada", note: null } };
649
- const leaves: string[] = [];
650
-
651
- useTraverse(source, (key, value, path) => {
652
- leaves.push(`${path.join(".")} = ${String(value)}`);
653
- return true;
654
- });
655
-
656
- leaves; // ["id = 7", "tags.0 = a", "tags.1 = b", "owner.name = Ada"]
657
- ```
658
-
659
- El callback se ejecuta una vez por hoja y recibe la clave propia de la hoja (su índice cuando proviene de un iterable), el valor y la ruta que lleva hasta ella. Devolver `false` abandona **todo** el recorrido, no solo la rama actual:
660
-
661
- ```typescript
662
- let visited = 0;
663
- useTraverse({ a: 1, b: 2, c: 3 }, () => {
664
- visited += 1;
665
- return false;
666
- });
667
- visited; // 1 — the first leaf already stops the walk
668
- ```
669
-
670
- Un ciclo es seguro por la misma razón:
671
-
672
- ```typescript
673
- const cyclic: any = { name: "root", child: { name: "leaf" } };
674
- cyclic.child.parent = cyclic;
675
-
676
- const names: string[] = [];
677
- useTraverse(cyclic, (_key, value) => {
678
- if (typeof value === "string") names.push(value);
679
- return true;
680
- });
681
- names; // ["root", "leaf"] — the back edge to `cyclic` is not revisited
682
- ```
683
-
684
- **Cada callback recibe su propia instantánea de la ruta.** El recorrido mantiene internamente un único array de trabajo y lo copia, en el momento de entregar una hoja, a un array nuevo congelado con `Object.freeze`. La instantánea ya no cambia, así que puede guardarse tal cual; dos callbacks nunca comparten un array, y repetir el recorrido no altera las instantáneas ya entregadas. Su tipo es `ReadonlyArray`, de modo que escribir en ella es un error de compilación, y el array congelado lanza además un `TypeError` en tiempo de ejecución. Esa seguridad cuesta una asignación `O(depth)` por hoja informada; una rama que no informa nada no asigna nada extra.
685
-
686
- ```typescript
687
- const paths: Array<ReadonlyArray<PropertyKey>> = [];
688
- useTraverse({ a: { b: 1 }, c: 2 }, (_key, _value, path) => {
689
- paths.push(path); // safe to keep as-is
690
- return true;
691
- });
692
- paths[0] === paths[1]; // false — one array per call
693
- paths.map(path => path.join(".")); // ["a.b", "c"]
694
- ```
695
-
696
- `useAttribute` es este recorrido convertido en fábrica: informa de las mismas hojas como pares `{ key, value }`, así que las reglas —y el coste— son idénticos.
697
-
698
- ```typescript
699
- useAttribute({ a: 1, b: { c: 2 } }).toUnordered().toArray();
700
- // [{ key: "a", value: 1 }, { key: "c", value: 2 }]
701
- ```
702
-
703
- ---
704
-
705
- <a id="12-performance-and-pitfalls"></a>
706
- ## 12. Rendimiento y problemas comunes
707
-
708
- **La pereza tiene un precio por etapa.** Cada operador añade un salto de generador. Para bucles numéricos
709
- ajustados sobre arrays en memoria, un `for` simple superará a cualquier canalización; la canalización gana cuando la fuente es
710
- perezosa, infinita, o la canalización es lo bastante larga como para que la disciplina de índices compense.
711
-
712
- **`toOrdered()` ordena; `toUnordered()` no.** Si una inversión parece "no haber funcionado", el
713
- colector era desordenado: la reescritura de índices solo se convierte en orden cuando los índices se normalizan.
714
-
715
- **`useGenerate` sin un `interrupt` nunca termina.** Proporciona siempre un predicado de parada o un
716
- `limit` / `takeWhile` aguas abajo.
717
-
718
- **`Ring` es un conjunto ordenado por inserción.** Los elementos son únicos y conservan el orden en
719
- que se añadieron por primera vez. Un `add` duplicado devuelve `false`, y un `set` que introduciría
720
- uno lanza una excepción. Esto es lo que permite que una fase sea función del *elemento* y no de la
721
- posición que resulte ocupar.
722
-
723
- **`Spectrum` asume una señal real en sus auxiliares de un solo lado.** `oneSided()`, `getNyquist()` y
724
- `centroid()` asumen simetría conjugada. El espectro espacial de un anillo de fase es complejo y en
725
- general no simétrico: un anillo que recorre el círculo hacia atrás pone toda su energía en el bin
726
- `n − 1`, más allá de Nyquist. Lee los bins directamente.
727
-
728
- **Los colectores de estructura son terminales, no vistas.** Cada uno vacía la canalización y luego construye una estructura completa, así que cuesta O(n) de tiempo y de espacio y no puede ejecutarse sobre una fuente infinita. Úsalo al final de una canalización, nunca dentro de un bucle sobre una.
729
-
730
- **`variance()` y sus compañeros devuelven un `Optional`.** `variance().get()` sobre una muestra demasiado pequeña lanza una excepción; usa `.orElse(0)` o comprueba `.isPresent()` cuando el tamaño de la muestra no está bajo tu control. Lo mismo vale para `standardDeviation`, `quantile`, `interquartileRange`, `skewness` y `kurtosis`.
731
-
732
- **Las ventanas periódicas son las ventanas de análisis.** `useHannWindow` y las demás formas simétricas son para diseño de filtros; un análisis de corto plazo quiere `useHannPeriodicWindow` y sus hermanas, o la suma de overlap-add ondulará en cada unión de tramas.
733
-
734
- ---
735
-
736
- <a id="13-phase-rings"></a>
737
- ## 13. Anillos de fase: un ciclo exacto
738
-
739
- El capítulo 10 presentó el anillo como el modelo del ciclo y lo usó para medir un conjunto de datos
740
- contra cada fase. Este capítulo es la superficie completa: cómo una posición se convierte en un
741
- ángulo, qué cambia y qué no cambia la frecuencia ambiente, y los instrumentos que leen el anillo
742
- contra el tiempo, contra los datos y contra otro anillo.
743
-
744
- Un `SynchronousRing` es una lista posicional respaldada por un anillo circular doblemente enlazado
745
- cuya cabeza y cola están unidas. Es también un *conjunto* ordenado por inserción: ningún elemento
746
- aparece dos veces, y una escritura rechazada deja intactos tanto los enlaces como las muestras. Lo
747
- que lo convierte en anillo y no en una lista es que la fase de una posición es una función pura de
748
- su resto módulo la longitud actual, así que el anillo es periódico al bit y su equilibrio no puede
749
- derivar.
750
-
751
- ### 13.1 Un anillo es un entero, calculado exactamente
752
-
753
- Un anillo lleva `n` elementos, y el número de elementos, el periodo posicional y el número de fases
754
- distintas son el mismo entero. La posición `i` lleva
755
-
756
- `phase + span · 2π · (((i mod n) + n) mod n) / n`
757
-
758
- con el resto reducido en `bigint` antes de convertirse en `number`. Esa es toda la razón por la que
759
- `phaseAt(i)` y `phaseAt(i + n)` son *idénticos* y no meramente parecidos: la segunda llamada se
760
- reduce al mismo entero, evalúa el mismo producto y produce el mismo doble. Un índice mucho más allá
761
- del rango entero exacto de `number` aterriza igualmente en la posición correcta, y un índice
762
- negativo recorre el anillo hacia atrás.
763
-
764
- | Llamada | Devuelve | Significado |
765
- | :--- | :--- | :--- |
766
- | `new SynchronousRing(source, options?)` | anillo | `source` es cualquier iterable, los duplicados se descartan |
767
- | `getPhase()` / `getFrequency()` / `getSpan()` | `Angle`, `Frequency`, `number` | los tres parámetros de construcción |
768
- | `size()` | `bigint` | el `n` que también es el periodo |
769
- | `phaseAt(index)` | `Optional<Angle>` | solo fase intrínseca; ignora tiempo, tasa y frecuencia |
770
- | `elementAt(index)` | `Optional<RingElement>` | elemento, fase intrínseca, frecuencia ambiente, fasor |
771
- | `frequencyAt(index)` | `Optional<Frequency>` | la tasa ambiente, vacía en un anillo vacío |
772
- | `toComplex()` / `toComplex(index)` | `Complex[]` / `Optional<Complex>` | fasores intrínsecos |
773
-
774
- `RingOptions` añade dos funciones por elemento: `magnitude(element, index)` da la longitud del fasor
775
- y vale uno por defecto, y `rate(element, index)` da al elemento su propia frecuencia intrínseca y
776
- vale cero por defecto, es decir, un marcador fijo.
777
-
778
- ```typescript
779
- import { SynchronousRing } from "semantic-typescript";
780
-
781
- const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
782
-
783
- ring.size(); // 6n
784
- ring.phaseAt(0n).get().toDegreeValue(); // 0
785
- ring.phaseAt(1n).get().toDegreeValue(); // 60
786
- ring.phaseAt(-1n).get().toDegreeValue(); // 300
787
- ring.phaseAt(6n).get().getValue() === ring.phaseAt(0n).get().getValue(); // true
788
- ring.phaseAt(10n ** 20n).get().toDegreeValue(); // 240
789
- ```
790
-
791
- `phaseAt(-1n)` vale `300` en un anillo de seis elementos, y `10n ** 20n` se reduce a `4` módulo
792
- seis, así que se lee `240`. Ninguno de los dos casos acumula un producto en coma flotante, que es lo
793
- que mantiene exacta la identidad cuando cambia la representación del índice.
794
-
795
- ### 13.2 La frecuencia ambiente y el estado en un instante
796
-
797
- `frequency` es la **velocidad de rotación ambiente** del entorno en el que está inmerso el anillo,
798
- no un paso de fase entre posiciones. El entorno avanza la fase de cada elemento el mismo ángulo, así
799
- que no puede cambiar el patrón intrínseco, ni romper el equilibrio, ni alterar ningún resultado de
800
- `phaseAt`. Su valor por defecto es `0 Hz`.
801
-
802
- `relativeAt(index, time)` resta ese ángulo común y deja la coordenada contra la que se puntúa un
803
- `PhaseField`. `viewAt` y `viewsAt` devuelven el estado completo en un instante, incluida la `rate`
804
- propia del elemento, y los accesores diferenciales exponen la deriva:
805
-
806
- | Llamada | Fórmula | Lee |
807
- | :--- | :--- | :--- |
808
- | `relativeAt(index, time)` | `intrínseca + tasaPropia · t − ambiente · t` | la fase que ve el entorno |
809
- | `viewAt(index, time)` / `viewsAt(time)` | lo mismo, más elemento, índice, tiempo, tasa, fasor | el estado completo |
810
- | `jetAt(index, time)` | fase en `t`, tasa propia, `slip = tasaPropia − ambiente` | el estado diferencial |
811
- | `windingAt(index, time)` | `trunc(slip · t)` | vueltas enteras deslizadas, truncadas hacia cero |
812
- | `errorIntegralAt(index, time, origin?)` | `∫ fase relativa dt`, en vueltas · segundos | cuánto tiempo lleva adelantado un elemento |
813
- | `resultant(time?)` | `abs(Σ w · e^{iθ}) / Σ w` | concentración de fase, inmune a la rotación ambiente |
814
- | `analyse(data, time?)` | cuentas y medias por referencia | los datos medidos contra cada elemento |
815
- | `scoreAt(index, time, field)` / `scoresAt(time, field)` | el campo muestreado en la fase relativa | una puntuación acotada por posición |
816
-
817
- ```typescript
818
- import { SynchronousRing, useHertz } from "semantic-typescript";
819
-
820
- const moving = new SynchronousRing(["a", "b", "c", "d", "e", "f"], {
821
- frequency: useHertz(1),
822
- });
823
-
824
- moving.getFrequency().getHertz(); // 1
825
- moving.phaseAt(1n).get().toDegreeValue(); // 60 — intrinsic
826
- moving.relativeAt(1n, 0.25).get().toDegreeValue(); // 330 — 60 − 90
827
- moving.viewAt(1n, 0.25).get().relative.toDegreeValue(); // 330
828
- moving.jetAt(1n, 0.25).get().slip; // -1
829
- moving.purity(); // 1 — rotation cannot break it
830
- ```
831
-
832
- A `1 Hz` el entorno avanza un cuarto de vuelta en `0.25 s`, así que la fase relativa de la posición
833
- uno es `60 − 90 = 330` grados mientras su fase intrínseca sigue siendo `60`. El jet informa un
834
- `slip` de `-1` hercios porque el elemento mismo no se mueve, y `purity()` sigue valiendo `1`:
835
- rotar todos los fasores el mismo ángulo no es una deformación.
836
-
837
- ### 13.3 Pureza y equilibrio
838
-
839
- `purity()` es la parte de la energía del anillo que porta el modo fundamental:
840
-
841
- `max(abs(c₁)², abs(c_{n−1})²) / (n · Σ abs(z)²)`
842
-
843
- Se prueban los dos bins fundamentales porque un anillo puede recorrer el círculo en cualquier
844
- dirección. Un anillo construido con `span: 5` sobre seis elementos avanza `300` grados, es decir
845
- `−60` grados: gira al revés, coloca toda su energía en `c₅` y una fórmula que solo mirara `c₁` lo
846
- declararía impuro. El coste sigue siendo `O(n)` porque solo hacen falta esos dos bins y la energía
847
- total.
848
-
849
- Los casos límite forman parte del contrato. Un anillo de menos de dos elementos no tiene modo
850
- fundamental y reporta `purity() === 0` e `isBalanced() === false`, sea cual sea su contenido. Un
851
- anillo cuyos fasores tienen todos longitud cero no tiene energía y también reporta cero.
852
- `isBalanced(epsilon?)` es `1 − purity() <= epsilon`, con `1E-9` como tolerancia por defecto.
853
-
854
- ```typescript
855
- import { SynchronousRing } from "semantic-typescript";
856
-
857
- const forward = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
858
- const backward = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 5 });
859
- const weighted = new SynchronousRing(["a", "b", "c", "d"], {
860
- magnitude: element => element === "a" ? 1 : 0,
861
- });
862
-
863
- forward.purity(); // 1
864
- backward.phaseAt(1n).get().toDegreeValue(); // 300 — it runs backwards
865
- backward.purity(); // 1 — the energy sits in c₅
866
- new SynchronousRing(["only"]).purity(); // 0
867
- new SynchronousRing(["only"]).isBalanced(); // false
868
- weighted.purity(); // 0.25
869
- ```
870
-
871
- ### 13.4 Longitud resultante y componentes simétricas
872
-
873
- `resultant(time?)` es la longitud resultante media: el módulo de la suma de fasores dividido por la
874
- suma de los módulos. A diferencia de `purity`, que pregunta si el anillo sigue siendo las raíces
875
- `n`-ésimas de la unidad, `resultant` pregunta cuán concentrados están los fasores ahora mismo. Un
876
- anillo equilibrado reporta `0`; un anillo cuyos elementos comparten un mismo ángulo reportaría `1`,
877
- pero la semántica de conjunto prohíbe los duplicados, así que en la práctica `1` aparece cuando
878
- `magnitude` anula todos menos uno.
879
-
880
- `zeroSequence()` es el fasor medio, es decir `c₀ / n`. `sequences()` devuelve las `n` componentes
881
- simétricas, de menor orden a mayor, mediante una transformada directa. `toSpectrum()` las envuelve
882
- en un `Spectrum` cuyo bin `j` reporta `j` ciclos por vuelta de anillo, así que el eje de frecuencias
883
- es el eje de números de onda.
884
-
885
- ```typescript
886
- import { SynchronousRing } from "semantic-typescript";
887
-
888
- const round = (value: number): number => Number(value.toFixed(4));
889
- const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
890
-
891
- round(ring.resultant().get()); // 0
892
- round(ring.zeroSequence().get().magnitude()); // 0
893
- ring.sequences().map(component => round(component.magnitude())); // [0, 6, 0, 0, 0, 0]
894
- ring.toSpectrum().get().size(); // 6
895
- ring.toSpectrum().get().getResolution().getHertz(); // 1
896
- ```
897
-
898
- Las componentes son complejas y en general **no conjugadas-simétricas**: un anillo equilibrado pone
899
- toda su energía en el bin uno, y uno que gira al revés en el bin `n − 1`. Los ayudantes unilaterales
900
- de `Spectrum` suponen una señal real; lea los bins directamente en lugar de plegarlos.
901
-
902
- ### 13.5 Analizar datos contra cada elemento
903
-
904
- `analyse(data, time?)` empareja cada observación con el anillo por identidad y la mide contra cada
905
- elemento como fase de referencia. Las observaciones que el anillo no contiene se cuentan como
906
- `missed` en lugar de adivinarse, porque el anillo es la única fuente de fases. Cada fila de
907
- referencia reporta `count`, la fase relativa media `mean` y la longitud resultante media `resultant`.
908
-
909
- Una identidad merece recordarse: **`resultant` es la misma para cada referencia**. Cambiar la base
910
- solo rota todas las fases relativas una constante, y una rotación no puede cambiar una longitud
911
- resultante. Lo que difiere por referencia es la cuenta y la media.
912
-
913
- ```typescript
914
- import { SynchronousRing } from "semantic-typescript";
915
-
916
- const round = (value: number): number => Number(value.toFixed(4));
917
- const ring = new SynchronousRing(["a", "b", "c", "d", "e"]);
918
- const report = ring.analyse(["a", "c", "c", "a", "zz", "e"]);
919
-
920
- report.matched; // 5
921
- report.missed; // 1
922
- report.references.map(row => row.count); // [2, 0, 2, 0, 1]
923
- report.references.map(row => round(row.resultant)); // [0.1453, 0.1453, 0.1453, 0.1453, 0.1453]
924
- ```
925
-
926
- La puntuación es la otra cara de la misma moneda. Un `PhaseField` es una serie de cosenos acotada
927
- (capítulo 10.2), y `scoreAt`/`scoresAt` lo muestrean en la fase relativa, así que el mismo anillo
928
- puede puntuarse contra una alineación, contra un patrón armónico o contra cualquier campo sin
929
- cambiar el anillo.
930
-
931
- ```typescript
932
- import { SynchronousRing, useField } from "semantic-typescript";
933
-
934
- const round = (value: number): number => Number(value.toFixed(4));
935
- const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
936
- const cosine = useField([1]);
937
-
938
- round(ring.scoreAt(0n, 0, cosine).get()); // 1
939
- round(ring.scoreAt(1n, 0, cosine).get()); // 0.5
940
- ring.scoresAt(0, cosine).map(score => round(score)); // [1, 0.5, -0.5, -1, -0.5, 0.5]
941
- ```
942
-
943
- ### 13.6 Combinar anillos y el espectro angular
944
-
945
- `combineRings(left, right)` une dos anillos por su mínimo común múltiplo, no por su producto. El
946
- instante `i` del anillo conjunto lee `i mod m` del anillo izquierdo e `i mod n` del derecho, así que
947
- el anillo conjunto se cierra tras `lcm(m, n)` instantes. Los pares que nunca pueden coexistir —los
948
- que difieren en posición módulo `gcd(m, n)`— simplemente no existen; por eso el resultado es un
949
- pullback y no un producto, y `aligns(left, right)` informa si un par dado es representable.
950
-
951
- `useAngularSpectrum(ring, instants, plan)` construye el plano `(ω, n)`: cada instante aporta una
952
- trama de fasores, la trama `k` del modo `j` es la `j`-ésima componente simétrica de esa trama, y
953
- `cells[frame][order]` es el coeficiente. Tomar una columna da la serie temporal de un único modo
954
- espacial, y `spectrum(order)` es su espectro temporal.
955
-
956
- ```typescript
957
- import { Sample, SynchronousRing, combineRings, useAngularSpectrum } from "semantic-typescript";
958
-
959
- const left = new SynchronousRing(["a", "b", "c", "d"]);
960
- const right = new SynchronousRing(["x", "y", "z", "w", "v", "u"]);
961
- const joined = combineRings(left, right);
962
-
963
- joined.period; // 12n
964
- joined.divisor; // 2n
965
- joined.ring.size(); // 12n
966
- joined.aligns(0n, 0n); // true
967
- joined.aligns(1n, 0n); // false
968
- joined.ring.get(0n).join("-"); // a-x
969
-
970
- const angular = useAngularSpectrum(left, [0, 0.25, 0.5, 0.75], new Sample(4, 1));
971
- angular.order; // 4
972
- angular.frameCount; // 4
973
- angular.cells[0][1].magnitude(); // 4
974
- angular.spectrum(1).get().size(); // 4
975
- ```
976
-
977
- ### 13.7 Trampas
978
-
979
- **La exactitud es exacta.** `phaseAt(i)` y `phaseAt(i + n)` son el mismo doble, no dos valores
980
- dentro de una tolerancia. Un anillo construido acumulando `2π / n` por elemento fallaría esto; este
981
- reduce primero el índice en `bigint`.
982
-
983
- **`frequency` no es un paso de fase.** Es la velocidad de rotación ambiente, vale cero por defecto,
984
- y rotar todos los fasores el mismo ángulo no puede romper el patrón intrínseco. Si quiere que las
985
- posiciones avancen unas respecto de otras, use `rate`, no `frequency`.
986
-
987
- **`purity()` debe mirar los dos bins fundamentales.** Un anillo que gira al revés sigue siendo las
988
- raíces `n`-ésimas de la unidad; solo `c₁` lo declararía impuro.
989
-
990
- **`n < 2` es un caso límite definido.** `purity()` devuelve `0` e `isBalanced()` devuelve `false`;
991
- un anillo vacío devuelve opcionales vacíos en los métodos de muestreo.
992
-
993
- **Un span que comparte un factor con `n` colapsa la órbita.** Seis elementos con `span: 2` visitan
994
- solo tres fases distintas, y el anillo deja de estar equilibrado.
995
-
996
- **`resultant` es invariante a la rotación, no al tiempo.** La rotación ambiente lo deja igual; las
997
- `rate` por elemento lo cambian a medida que el anillo se desfasa.
998
-
999
- **El anillo es un conjunto, y `analyse` empareja por identidad.** Los duplicados se descartan al
1000
- construir, y un objeto estructuralmente igual a un elemento del anillo pero que no es la misma
1001
- referencia se cuenta como perdido.
1002
-
1003
- **`toSpectrum()` es complejo.** Sus bins no son conjugados-simétricos, así que los ayudantes
1004
- unilaterales de `Spectrum` no aplican. Lea los bins.
1005
-
1006
- **`windingAt` trunca hacia cero, y `errorIntegralAt` integra la fase.** Media vuelta de deslizamiento
1007
- reporta `0n`; la integral de error está en vueltas · segundos, no es una tasa.
1008
-
1009
- ```typescript
1010
- import { SynchronousRing, useHertz } from "semantic-typescript";
1011
-
1012
- const collapsed = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 2 });
1013
- collapsed.phaseAt(1n).get().toDegreeValue(); // 120
1014
- collapsed.phaseAt(3n).get().toDegreeValue(); // 0
1015
- Number(collapsed.purity().toFixed(4)); // 0
1016
-
1017
- const falling = new SynchronousRing(["a", "b", "c", "d"], {
1018
- frequency: useHertz(2),
1019
- rate: () => useHertz(0.5),
1020
- });
1021
- falling.jetAt(0n, 2).get().slip; // -1.5
1022
- falling.windingAt(0n, 1).get(); // -1n
1023
- falling.windingAt(0n, 0.5).get(); // 0n
1024
-
1025
- const objects = new SynchronousRing([{ key: 1 }, { key: 2 }]);
1026
- objects.analyse([{ key: 1 }]).missed; // 1 — identity, not structure
1027
- ```
1028
-
1029
- ---
1030
-
1031
- <a id="14-signal-analysis"></a>
1032
- ## 14. Análisis de señales: ventanas, espectros y tiempo
1033
-
1034
- El capítulo 10 trataba un anillo como un ciclo de fases. Este capítulo es su contraparte temporal:
1035
- un registro real que lleva, además de sus muestras, los dos contextos que necesita toda fórmula
1036
- espectral — el plan de muestreo que convierte un índice de bin en hercios, y la ventana que fija a
1037
- la vez la ganancia coherente y el ancho de banda de ruido.
1038
-
1039
- El objeto valor es `SynchronousSignalCollectable`, y su gemelo en cola es
1040
- `AsynchronousSignalCollectable`. Ambos exponen la misma superficie `SignalCollectable`:
1041
- veintitrés métodos, cada uno delegando en exactamente una fábrica `useSynchronous<Method>` de
1042
- `synchronous/collectors`. La separación es deliberada. La clase es una fachada documentada, la
1043
- fábrica posee la acumulación, y el constructor calcula una sola vez, a esta longitud,
1044
- `sum(w) / count` y `count * sum(w^2) / sum(w)^2`. Quien tenga que recordar «dividir por la
1045
- ganancia coherente» acabará olvidándolo, y los 6,02 decibelios resultantes parecen una medición
1046
- plausible.
1047
-
1048
- ### 14.1 Una señal lleva su propia calibración
1049
-
1050
- Una señal se construye desde un iterable y una tasa, desde un `Frame` existente, o desde el
1051
- constructor con un `Sample` explícito. La ventana por defecto es la ventana de Hann periódica;
1052
- toda análisis que no nombre una la usa, y todo análisis acepta una sustitución. Los accesores de
1053
- metadatos leen en tiempo constante un estado fijado en la construcción, así que una señal puede
1054
- pasarse de mano en mano sin que el contexto derive.
1055
-
1056
- | Llamada | Devuelve | Significado |
1057
- | :--- | :--- | :--- |
1058
- | `SynchronousSignalCollectable.useRate(values, rate, window?)` | señal | duración `count / rate` |
1059
- | `SynchronousSignalCollectable.useFrame(frame, window?)` | señal | conserva el plan del frame |
1060
- | `new SynchronousSignalCollectable(plan, values, window?)` | señal | el número de tramas debe igualar el de muestras |
1061
- | `getSample()` / `getValues()` / `getWindow()` | plan, muestras, ventana | el contexto congelado |
1062
- | `getRate()` | `number` | muestras por segundo |
1063
- | `getNyquist()` / `getResolution()` | `Frequency` | `rate / 2` y `rate / count` |
1064
- | `getCoherentGain()` / `getNoiseBandwidth()` | `number` | recalculados a esta longitud exacta |
1065
- | `size()` / `at(index)` / `toFrame()` / `equals(other)` / `toString()` | — | semántica de valor |
1066
-
1067
- ```typescript
1068
- import { SynchronousSignalCollectable } from "semantic-typescript";
1069
-
1070
- const signal = SynchronousSignalCollectable.useRate(
1071
- Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 64)),
1072
- 64,
1073
- );
1074
-
1075
- signal.size(); // 64n
1076
- signal.getRate(); // 64
1077
- signal.getNyquist().getHertz(); // 32
1078
- signal.getResolution().getHertz(); // 1
1079
- signal.getCoherentGain(); // 0.5
1080
- signal.getNoiseBandwidth(); // 1.5
1081
- ```
1082
-
1083
- `getResolution()` es el espaciado entre bins, no el poder de resolución: vale `rate / count`
1084
- porque la transformada tiene `count` bins, y rellenar con ceros cambia el espaciado sin cambiar
1085
- qué puede separar la ventana (sección 14.8).
1086
-
1087
- ### 14.2 Los veintitrés métodos
1088
-
1089
- Los métodos forman seis familias. Siete son lecturas espectrales, dos son transformadas
1090
- tiempo–frecuencia, cuatro son magnitudes analíticas, tres son estadísticos de secuencia, cinco son
1091
- descriptores de forma, y dos devuelven otra señal.
1092
-
1093
- | Método | Devuelve | Convención |
1094
- | :--- | :--- | :--- |
1095
- | `spectrum(size?, window?)` | `Spectrum` | crudo y sin normalizar, rejilla compleja completa |
1096
- | `oneSided(size?, window?)` | `Bin[]` | mitad conjugada plegada, DC y Nyquist sin duplicar |
1097
- | `amplitude(window?)` | `number[]` | `2·abs(X) / (count · CG)`, DC y Nyquist una sola vez |
1098
- | `power(window?)` | `number[]` | `A² / 2` por bin interior, `A²` en DC y Nyquist |
1099
- | `decibel(window?)` | `number[]` | `20·log10(amplitud)` |
1100
- | `powerDecibel(window?)` | `number[]` | `10·log10(potencia)` |
1101
- | `powerSpectralDensity(frameSize?, hopSize?, window?)` | `SpectralDensity` | media de Welch, `abs(X)² / (rate · Σw²)` |
1102
- | `shortTimeFourierTransform(frameSize, hopSize, window?)` | `Spectrum[]` | bordes extendidos, tiempos en el centro de trama |
1103
- | `spectrogram(frameSize, hopSize, window?)` | `Spectrogram` | las mismas columnas con eje de tiempo |
1104
- | `analyticSignal()` | `Complex[]` | par de Hilbert `x + i·H(x)` |
1105
- | `instantaneousPhase()` | `Angle[]` | plegada en `[0, 2π)` |
1106
- | `instantaneousFrequency(rate?, gate?)` | `number[]` | diferencia de fase desenvuelta, con puerta de envolvente |
1107
- | `envelope()` | `number[]` | `abs(z[n])`, circular en los bordes |
1108
- | `zeroCrossingRate(perSecond?)` | `number` | cruces por par de muestras adyacentes |
1109
- | `spectralFlux(frameSize?, hopSize?, window?)` | `number[]` | distancia de norma unitaria entre tramas |
1110
- | `autocorrelation(maximumLag?, normalization?)` | `number[]` | coeficiente por defecto, `ρ[0] = 1` |
1111
- | `spectralCentroid(window?)` | `Optional<Frequency>` | frecuencia media ponderada por potencia |
1112
- | `spectralBandwidth(window?)` | `Optional<Frequency>` | desviación típica ponderada por potencia |
1113
- | `spectralRolloff(ratio?, window?)` | `Optional<Frequency>` | primer bin que alcanza `ratio` de la potencia |
1114
- | `spectralFlatness(window?)` | `number` | media geométrica sobre aritmética de la potencia |
1115
- | `signalToNoiseRatio(frameSize?, hopSize?, window?, floorQuantile?)` | `Optional<number>` | suelo mediano, `10·log10` |
1116
- | `detrended(mode?)` | señal | `"none"`, `"constant"` (defecto), `"linear"` |
1117
- | `windowed(window?)` | señal | aplica una ventana y la adopta |
1118
-
1119
- Los argumentos opcionales no son decoración. `frameSize` vale `min(256, size)` por defecto, el
1120
- salto media trama redondeada hacia abajo, `instantaneousFrequency` pone la puerta en `1E-3` de la
1121
- envolvente de pico, y `spectralRolloff` usa `0.85`. Cada valor por defecto es el que elige una
1122
- implementación de referencia cuando el parámetro se nombra pero no se pasa.
1123
-
1124
- ### 14.3 Amplitud, potencia y densidad
1125
-
1126
- Hacen falta tres normalizaciones porque se hacen tres preguntas distintas.
1127
-
1128
- `amplitude()` responde «qué tamaño tiene el seno en este bin»: `2·abs(X[k]) / (count · CG)` para
1129
- un bin interior y `abs(X[k]) / (count · CG)` en DC y Nyquist. El factor dos pliega la mitad
1130
- conjugada; la ganancia coherente quita la ventana. Omitir cualquiera de los dos hace que un seno de
1131
- amplitud unidad se lea `0.5`, es decir, menos 6,02 decibelios.
1132
-
1133
- `power()` responde «qué cuadrado medio tendría un seno en este bin»: `A² / 2` para un bin interior
1134
- y `A²` en DC y Nyquist, porque estos dos no oscilan. Es la convención de pico, y deliberadamente no
1135
- cumple Parseval: la ventana reparte cada línea por su lóbulo principal, así que la suma de todos los
1136
- bins supera el cuadrado medio en el ancho de banda de ruido equivalente — `1.5` para Hann.
1137
-
1138
- `powerSpectralDensity()` responde «cuánta potencia por hercio»: `abs(X)² / (rate · Σw²)`,
1139
- promediada sobre segmentos solapados. Solo la suma de los pesos al cuadrado mantiene insesgado un
1140
- suelo de ruido blanco; dividir por `count` lee `0.375` para ruido unitario, dividir por
1141
- `count · CG²` lee `1.5`.
1142
-
1143
- ```typescript
1144
- import { SynchronousSignalCollectable } from "semantic-typescript";
1145
-
1146
- const round = (value: number): number => Number(value.toFixed(4));
1147
- const tone = SynchronousSignalCollectable.useRate(
1148
- Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1149
- 256,
1150
- );
1151
-
1152
- round(tone.amplitude()[32]); // 1
1153
- round(tone.power()[32]); // 0.5
1154
- round(tone.power().reduce((sum, power) => sum + power, 0)); // 0.75
1155
- round(tone.powerSpectralDensity(64n, 32n).totalPower()); // 0.5
1156
- tone.powerSpectralDensity(64n, 32n).getSegments(); // 7n
1157
- ```
1158
-
1159
- El par de totales es la comprobación que conviene recordar: la suma en convención de pico vale
1160
- `0.75` mientras que la densidad integra `0.5`, y la razón es exactamente el ancho de banda de
1161
- ruido. La densidad es además la única de las tres que sobrevive a un cambio de longitud de trama.
1162
-
1163
- ### 14.4 Niveles: las dos convenciones de decibelio
1164
-
1165
- Una amplitud y una potencia no comparten factor, así que tampoco comparten logaritmo.
1166
- `amplitudeDecibel` aplica `20·log10` y es correcto para una tensión, un desplazamiento o una
1167
- presión sonora; `powerDecibel` aplica `10·log10` y es correcto para una potencia, una energía, un
1168
- cuadrado medio o una densidad. Los nombres de método de la señal siguen la misma división.
1169
-
1170
- ```typescript
1171
- import { amplitudeDecibel, powerDecibel, SynchronousSignalCollectable } from "semantic-typescript";
1172
-
1173
- const round = (value: number): number => Number(value.toFixed(4));
1174
- const tone = SynchronousSignalCollectable.useRate(
1175
- Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1176
- 256,
1177
- );
1178
-
1179
- amplitudeDecibel(0.25); // -12.0412
1180
- powerDecibel(0.25); // -6.0206
1181
- round(tone.decibel()[32]); // 0
1182
- round(tone.powerDecibel()[32]); // -3.0103
1183
- ```
1184
-
1185
- Las dos lecturas del mismo tono son correctas: su amplitud es uno (cero decibelios) y su cuadrado
1186
- medio es un medio (menos 3,01 decibelios). Pasar una potencia por la convención de amplitud
1187
- duplicaría el nivel en decibelios, convirtiendo el menos 6,02 correcto en menos 12,04.
1188
-
1189
- ### 14.5 El plegado unilateral
1190
-
1191
- Una señal real cumple `X[count − k] = conj(X[k])`, así que la mitad de frecuencias negativas no
1192
- lleva información independiente. El plegado duplica cada bin interior y deja los dos bins
1193
- autoconjugados — DC y, para longitud par, Nyquist — exactamente como los produjo la transformada.
1194
- Esos dos son su propio espejo; duplicarlos inventa energía y reporta exactamente 6,02 decibelios de
1195
- más.
1196
-
1197
- ```typescript
1198
- import { SynchronousSignalCollectable } from "semantic-typescript";
1199
-
1200
- const round = (value: number): number => Number(value.toFixed(4));
1201
- const edge = SynchronousSignalCollectable.useRate(
1202
- Array.from({ length: 16 }, (_unused, index) => 2 + Math.cos(Math.PI * index)),
1203
- 16,
1204
- );
1205
- const bins = edge.oneSided();
1206
-
1207
- bins.length; // 9
1208
- round(bins[0].magnitude()); // 16
1209
- round(bins[8].magnitude()); // 8
1210
- round(edge.amplitude()[0]); // 2
1211
- round(edge.amplitude()[8]); // 1
1212
- ```
1213
-
1214
- Las magnitudes crudas son `16` y `8` porque los pesos de la ventana suman ocho; dividir por
1215
- `count · CG = 8` las convierte en las amplitudes calibradas `2` y `1`. Duplicar los bins de borde
1216
- leería `4` y `2`, y el error nunca lanzaría una excepción.
1217
-
1218
- ### 14.6 Vistas tiempo–frecuencia
1219
-
1220
- `shortTimeFourierTransform` y `spectrogram` enmarcan el registro como la transformada de corto
1221
- tiempo de referencia: media trama de ceros en cada extremo y luego una cola rellenada con ceros
1222
- hasta completar un número entero de saltos. La primera trama se centra en la primera muestra, la
1223
- última alcanza la última muestra, y la trama `m` se centra en `m · hop / rate` segundos. Conservar
1224
- solo las tramas completas descartaría hasta `frameSize − 1` muestras — para 1000 muestras, trama
1225
- 256 y salto 128 son 104 muestras, alrededor de una décima parte del registro.
1226
-
1227
- ```typescript
1228
- import { SynchronousSignalCollectable } from "semantic-typescript";
1229
-
1230
- const tone = SynchronousSignalCollectable.useRate(
1231
- Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 4 * index / 64)),
1232
- 64,
1233
- );
1234
- const spectrogram = tone.spectrogram(16n, 8n);
1235
-
1236
- spectrogram.size(); // 9
1237
- spectrogram.getFrameSize(); // 16
1238
- spectrogram.getBoundary(); // "zeros"
1239
- spectrogram.getTime(0); // 0
1240
- spectrogram.getTime(6); // 0.75
1241
- tone.shortTimeFourierTransform(16n, 8n).length; // 9
1242
- tone.spectralFlux(16n, 8n).length; // 8
1243
- ```
1244
-
1245
- Cada columna es un `Spectrum` de `frameSize` bins, exactamente lo que produciría `spectrum()` para
1246
- la misma trama, y `getTime()` informa centros de trama, no inicios.
1247
-
1248
- ### 14.7 Ventanas, ganancia coherente y COLA
1249
-
1250
- Cada forma tiene dos variantes. La simétrica divide por `count − 1`; la periódica (DFT par) divide
1251
- por `count`, una muestra menos que un periodo completo, de modo que la ventana es continua bajo la
1252
- extensión periódica que supone la DFT. El análisis y la suma con solapamiento necesitan la forma
1253
- periódica; el diseño de filtros conserva la simétrica.
1254
-
1255
- La diferencia es medible y nunca lanza un error. Al cincuenta por ciento de solapamiento, la
1256
- ventana de Hann periódica suma uno en toda posición; la forma simétrica ondula, `0.84` decibelios
1257
- con dieciséis muestras, `0.21` con sesenta y cuatro y todavía `0.013` con mil veinticuatro. La
1258
- clase expone la ganancia coherente y el ancho de banda de ruido equivalente de la ventana que
1259
- guarda, así que la corrección nunca está codificada a mano: un Hann simétrico lee `0.4375` con
1260
- ocho muestras, no `0.5`.
1261
-
1262
- ```typescript
1263
- import {
1264
- coherentGain, equivalentNoiseBandwidth, useFlatTopWindow,
1265
- useHannPeriodicWindow, useHannWindow,
1266
- } from "semantic-typescript";
1267
-
1268
- const round = (value: number): number => Number(value.toFixed(4));
1269
-
1270
- round(useHannWindow(1, 4)); // 0.75
1271
- round(useHannPeriodicWindow(1, 4)); // 0.5
1272
- coherentGain(useHannPeriodicWindow, 16); // 0.5
1273
- equivalentNoiseBandwidth(useHannPeriodicWindow, 16); // 1.5
1274
- useFlatTopWindow(0, 1024); // -0.000421
1275
- Array.from({ length: 1024 }, (_unused, i) => useFlatTopWindow(i, 1024)).filter(w => w < 0).length; // 578
1276
- ```
1277
-
1278
- La fila del flat-top no es un error de la tabla. Una ventana flat-top es parcialmente negativa por
1279
- definición — aquí 578 de 1024 pesos — y recortar esos pesos a cero perturbaría el lóbulo principal
1280
- y destruiría la baja pérdida de scalloping para la que existe la ventana. La biblioteca devuelve la
1281
- serie sin cambios.
1282
-
1283
- ### 14.8 Trampas
1284
-
1285
- **Rellenar con ceros interpola, nunca resuelve.** Una longitud de transformada mayor que el número
1286
- de muestras muestrea la misma DTFT con más densidad: la *posición* del pico se afina y el espaciado
1287
- entre bins pasa a `rate / size`, pero la capacidad de separar dos componentes sigue gobernada por
1288
- la longitud y la forma de la ventana. Un tono exactamente entre dos bins lee `82.1093` y `80.8598`
1289
- en la rejilla sin relleno, y `128` en su propia frecuencia en la rejilla rellenada.
1290
-
1291
- ```typescript
1292
- import { SynchronousSignalCollectable, useRectangularWindow } from "semantic-typescript";
1293
-
1294
- const round = (value: number): number => Number(value.toFixed(4));
1295
- const tone = SynchronousSignalCollectable.useRate(
1296
- Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 100.5 * index / 256)),
1297
- 256,
1298
- );
1299
- const flat = tone.spectrum(256n, useRectangularWindow);
1300
- const padded = tone.spectrum(1024n, useRectangularWindow);
1301
-
1302
- flat.getResolution().getHertz(); // 1
1303
- padded.getResolution().getHertz(); // 0.25
1304
- round(flat.magnitudeAt(100).get()); // 82.1093
1305
- round(flat.magnitudeAt(101).get()); // 80.8598
1306
- round(padded.magnitudeAt(402).get()); // 128
1307
- padded.frequencyAt(402).get().getHertz(); // 100.5
1308
- ```
1309
-
1310
- Dos tonos iguales separados `0.4` bins siguen fusionados con cualquier relleno: con un registro de
1311
- 256 puntos y una ventana de Hann, rellenar hasta 8192 produce un solo máximo local, no dos.
1312
-
1313
- **La transformada de Hilbert es circular.** `analyticSignal`, `envelope` e `instantaneousPhase`
1314
- tratan la primera y la última muestra como vecinas, así que un registro cuya periodo no divide su
1315
- longitud queda contaminado en ambos extremos. El centro del registro es fiable; los extremos no.
1316
-
1317
- ```typescript
1318
- import { SynchronousSignalCollectable } from "semantic-typescript";
1319
-
1320
- const round = (value: number): number => Number(value.toFixed(4));
1321
- const edge = SynchronousSignalCollectable.useRate(
1322
- Array.from({ length: 512 }, (_unused, index) => Math.cos(2 * Math.PI * 8.3 * index / 512)),
1323
- 512,
1324
- );
1325
- const envelope = edge.envelope();
1326
-
1327
- round(envelope.slice(8, -8).reduce((sum, value) => sum + value, 0) / 496); // 1.0025
1328
- round(Math.max(...envelope.slice(0, 8))); // 1.2592
1329
- round(Math.min(...envelope.slice(-8))); // 0.1314
1330
- ```
1331
-
1332
- **Una envolvente que se anula no define una fase.** Donde la envolvente se desploma, la diferencia
1333
- de dos ángulos de ruido de redondeo puede llegar a media vuelta, lo que se lee como la mitad de la
1334
- tasa de muestreo. La puerta es lo que mantiene significativo el número, y el valor por defecto es
1335
- `1E-3` de la envolvente de pico.
1336
-
1337
- ```typescript
1338
- import { SynchronousSignalCollectable } from "semantic-typescript";
1339
-
1340
- const round = (value: number): number => Number(value.toFixed(4));
1341
- const beat = SynchronousSignalCollectable.useRate(
1342
- Array.from({ length: 4096 }, (_unused, index) =>
1343
- Math.cos(2 * Math.PI * 100 * index / 2048) + Math.cos(2 * Math.PI * 102 * index / 2048)),
1344
- 2048,
1345
- );
1346
-
1347
- round(Math.max(...beat.instantaneousFrequency(2048, 0).map(Math.abs))); // 920.218
1348
- round(Math.max(...beat.instantaneousFrequency().map(Math.abs))); // 101
1349
- ```
1350
-
1351
- **Una correlación sin normalizar no es una similitud.** La suma cruda `r[k]` crece con la longitud
1352
- del registro, así que dos longitudes no son comparables. La regla por defecto `"coefficient"`
1353
- divide por `r[0]` y mantiene cada valor en `[-1, 1]`; `"unbiased"` divide por `count − k` y paga
1354
- con una varianza que crece con el desfase.
1355
-
1356
- ```typescript
1357
- import { SynchronousSignalCollectable } from "semantic-typescript";
1358
-
1359
- const round = (value: number): number => Number(value.toFixed(4));
1360
- const periodic = SynchronousSignalCollectable.useRate(
1361
- Array.from({ length: 1024 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 1024)),
1362
- 1024,
1363
- );
1364
- const rho = periodic.autocorrelation(200n);
1365
-
1366
- round(rho[0]); // 1
1367
- round(rho[128]); // 0.875
1368
- round(rho[64]); // -0.9375
1369
- round(periodic.autocorrelation(200n, "none")[0]); // 512
1370
- ```
1371
-
1372
- Una lectura más merece advertencia: `spectralFlux` normaliza cada trama antes de compararla, así
1373
- que un cambio puro de nivel produce flujo cero. Su primer y último valor son artefactos de borde de
1374
- la frontera de ceros, no ataques.
1375
-
1376
- ---
1377
-
1378
- <a id="15-license"></a>
1379
- ## 15. Licencia
1380
-
1381
- MIT © Eloy Kim
1
+ # **Semantic-TypeScript**
2
+
3
+ **Flujo, indexado.** Tus datos, bajo control preciso.
4
+
5
+ [English](./readme.md) · [简体中文](./readme.cn.md) · [繁體中文](./readme.tw.md) · [日本語](./readme.jp.md) · [한국어](./readme.kr.md) · [Deutsch](./readme.de.md) · [Français](./readme.fr.md) · Español · [Русский](./readme.ru.md)
6
+
7
+ Una biblioteca de procesamiento de flujos con seguridad de tipos que unifica **generadores de JavaScript**, **Java Stream** y
8
+ el **direccionamiento de datos basado en índices**. Cada elemento lleva un índice `bigint`, cada canalización es perezosa y
9
+ cada operación terminal es una opción explícita.
10
+
11
+ ```bash
12
+ npm install semantic-typescript
13
+ ```
14
+
15
+ - Cero dependencias en tiempo de ejecución, ESM puro, incluye sus propios tipos.
16
+ - 684 pruebas de regresión: `npm test`.
17
+ - Historial de cambios completo: [CHANGELOG.md](./CHANGELOG.md).
18
+
19
+ ---
20
+
21
+ ## Tabla de contenidos
22
+
23
+ 1. [Filosofía de diseño](#1-design-philosophy)
24
+ 2. [Arquitectura](#2-architecture)
25
+ 3. [Inicio rápido](#3-quick-start)
26
+ 4. [Funciones de fábrica](#4-factory-functions)
27
+ 5. [Operadores de transformación](#5-transformation-operators)
28
+ 6. [Collectables y operaciones terminales](#6-collectables-and-terminal-operations)
29
+ 7. [Estadísticas y ventanas](#7-statistics-and-windows)
30
+ 8. [El modelo de índices](#8-the-index-model)
31
+ 9. [El modelo asíncrono](#9-the-asynchronous-model)
32
+ 10. [Análisis periódico: anillos, campos y puntuaciones](#10-periodic-analysis-rings-fields-and-scores)
33
+ 11. [Funciones auxiliares](#11-utility-helpers)
34
+ 12. [Rendimiento y problemas comunes](#12-performance-and-pitfalls)
35
+ 13. [Anillos de fase: un ciclo exacto](#13-phase-rings)
36
+ 14. [Análisis de señales: ventanas, espectros y tiempo](#14-signal-analysis)
37
+ 15. [Licencia](#15-license)
38
+
39
+ ---
40
+
41
+ <a id="1-design-philosophy"></a>
42
+ ## 1. Filosofía de diseño
43
+
44
+ Tres ideas, una sola canalización:
45
+
46
+ **El índice es dato.** Cada elemento está direccionado por un `bigint`. La inversión, la rotación, el barajado y
47
+ la partición son aritmética de índices, no operaciones de búfer. Nada se materializa hasta que un
48
+ collectable lo pide.
49
+
50
+ **La pereza es el valor predeterminado.** Una fábrica envuelve un generador; un operador envuelve otro generador. Ningún
51
+ elemento se solicita hasta que se ejecuta una operación terminal, así que una fuente infinita es perfectamente
52
+ legal siempre que algo aguas abajo la detenga.
53
+
54
+ **Los terminales son explícitos.** `map` devuelve un `Semantic`, no un array. Los valores aparecen solo cuando
55
+ nombras un collectable — `toUnordered()`, `toOrdered()`, `toNumericStatistics()`, `toWindow()` — y cada
56
+ uno declara cuánto cuesta y qué orden produce.
57
+
58
+ La misma disciplina se aplica a la mitad matemática de la biblioteca. Un `Complex`, un `Angle` o una
59
+ `Frequency` es un valor inmutable cuyas invariantes se comprueban en la construcción; un `Optional`
60
+ sustituye a `null`; y un anillo de fase declara exactamente cuántos elementos contiene, qué periodo tiene y
61
+ qué equilibrado está.
62
+
63
+ ---
64
+
65
+ <a id="2-architecture"></a>
66
+ ## 2. Arquitectura
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
+ | Capa | Módulo | Responsabilidad |
76
+ | :--- | :--- | :--- |
77
+ | Fuentes | `synchronous/collectors`, `asynchronous/collectors` | Envolver un iterable, un generador, una promesa, un blob o un destino de eventos |
78
+ | Canalizaciones | `synchronous/semantic`, `asynchronous/semantic` | Operadores perezosos y reescritura de índices |
79
+ | Collectables | `synchronous/semantic`, `asynchronous/semantic` | Terminales ordenados, desordenados, de estadísticas, de ventanas y personalizados |
80
+ | Colectores | `synchronous/collector`, `asynchronous/collector` | Las primitivas de plegado sobre las que se construye cada terminal |
81
+ | Matemáticas | `mathematics/*` | Ángulos, complejos, frecuencias, espectros, ventanas, estimadores, epiciclos, anillos de fase |
82
+ | Estructuras | `structure/*` | Listas, conjuntos, mapas, árboles, bosques, anillos, campos |
83
+ | Errores | `structure/error` | La jerarquía tipada `SemanticError` y sus guardas de marca |
84
+
85
+ Las familias síncrona y asíncrona comparten un solo vocabulario: los mismos nombres de operador, la misma
86
+ semántica de índices, los mismos collectables. Las variantes asíncronas simplemente devuelven promesas y
87
+ encolan las escrituras, de modo que una lectura emitida después de una escritura siempre la observa.
88
+
89
+ ### 2.1 Qué movió y qué rompió esta versión
90
+
91
+ Tres módulos cambiaron de sitio o de forma, y una serie de valores por defecto cambió de significado. Cada comportamiento antiguo sigue siendo alcanzable mediante una opción explícita; nada se eliminó sin un reemplazo.
92
+
93
+ | Cambio | Antes | Ahora |
94
+ | :--- | :--- | :--- |
95
+ | Bosques | `structure/tree/rootedForest`, `structure/tree/unionFindForest` | `structure/forest/rootedForest`, `structure/forest/unionFindForest`; `tree/index.ts` ya no los reexporta |
96
+ | Errores | errores nativos ad hoc | una jerarquía de quince clases en `structure/error.ts`, con una guarda de marca por clase y el alias `SemanticRangeError` |
97
+ | Opciones de estimador | definiciones fijas | `mathematics/quantile.ts` exporta `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator` y `VarianceOptions` |
98
+ | `quantile` | Hyndman–Fan tipo 1 | tipo 7 — R `quantile`, NumPy `linear`, Excel `PERCENTILE.INC` |
99
+ | `skewness` / `kurtosis` | `g1` / `g2` | los corregidos por sesgo `G1` / `G2` |
100
+ | `variance` / `standardDeviation` | `ddof = 0` | `ddof = 1`, envueltos en un `Optional` |
101
+ | `decibel` | una función `20·log10` | eliminada; `amplitudeDecibel` (20·log10) y `powerDecibel` (10·log10) |
102
+ | `Spectrum.flatness()` | magnitudes sobre ambas mitades | potencias sobre la mitad unilateral |
103
+ | `Spectrum.bandEnergy()` | suma bilateral cruda | unilateral por defecto, plegando la mitad conjugada |
104
+ | `shortTimeFourierTransform` / `spectrogram` | Hann simétrica, solo tramas completas | Hann periódica, `boundary: "zeros"`, `padded: true` |
105
+
106
+ Los capítulos 6, 7 y 10 documentan los reemplazos; las consecuencias medidas están en [CHANGELOG.md](./CHANGELOG.md).
107
+
108
+ ---
109
+
110
+ <a id="3-quick-start"></a>
111
+ ## 3. Inicio rápido
112
+
113
+ ```typescript
114
+ import {
115
+ useOf, useFrom, useRange, useText, useStringify, usePromise
116
+ } from "semantic-typescript";
117
+
118
+ // Numeric aggregation
119
+ const sum: number = useOf(10, 20, 30, 40)
120
+ .map(n => n * 2)
121
+ .toNumericStatistics()
122
+ .summate(); // 200
123
+
124
+ // BigInt aggregation
125
+ const bigSum: bigint = useOf(10n, 20n, 30n, 40n)
126
+ .map(n => n * 2n)
127
+ .toBigintStatistics()
128
+ .summate(); // 200n
129
+
130
+ // Reordering is index arithmetic — collect with toOrdered() for it to take effect
131
+ const reversed: number[] = useFrom([1, 2, 3, 4, 5]).reverse().toOrdered().toArray(); // [5,4,3,2,1]
132
+ const rotated: number[] = useFrom([1, 2, 3, 4, 5]).translate(2).toOrdered().toArray(); // [4,5,1,2,3]
133
+
134
+ // Lazy evaluation over a large range
135
+ const multiples: bigint[] = useRange(0n, 1_000_000n)
136
+ .filter(n => n % 17n === 0n)
137
+ .limit(10n)
138
+ .toUnordered()
139
+ .toArray(); // [0n,17n,…,153n]
140
+
141
+ // Unicode-safe string processing (code points, not UTF-16 units)
142
+ useText("semantic 👍").toUnordered().forEach(character => console.log(character));
143
+
144
+ // Circular-safe serialisation
145
+ const cyclic: any = { a: 1 }; cyclic.self = cyclic;
146
+ useStringify(cyclic); // '{"a":1}'
147
+
148
+ // Terminals hand the elements to real structures, not only to arrays
149
+ useOf(3, 1, 4, 1, 5).toCollectable().toOrderedSet().toArray(); // [1,3,4,5]
150
+ useFrom([3, 1, 4, 1, 5]).toCollectable().toBinarySearchTree().size(); // 5n
151
+
152
+ // Async sources
153
+ const total = await usePromise(Promise.resolve(7)).toNumericStatistics().summate(); // 7
154
+ ```
155
+
156
+ ---
157
+
158
+ <a id="4-factory-functions"></a>
159
+ ## 4. Funciones de fábrica
160
+
161
+ La construcción es siempre O(1) — una fábrica solo envuelve un generador. La tabla indica el coste de
162
+ *consumir* `n` elementos, más el espacio adicional que la propia fuente necesita.
163
+
164
+ | Fábrica | Firma (resultado) | Tiempo (consumir n) | Espacio (fuente) | Notas |
165
+ | :--- | :--- | :--- | :--- | :--- |
166
+ | `useOf` | `useOf<E>(...elements) → SynchronousSemantic<E>` | O(n) | O(n) | Los índices son 0..n−1; el argumento de propagación ya está materializado |
167
+ | `useFrom` | `useFrom<E>(Iterable<E>) → SynchronousSemantic<E>`<br>`useFrom<E>(AsyncIterable<E>) → AsynchronousSemantic<E>` | O(n) | O(1) | Envuelve cualquier iterable; se itera una vez al recolectar |
168
+ | `useRange` | `useRange<N>(start, end, step?) → SynchronousSemantic<N>` | O(n) | O(1) | `end` es exclusivo; `step` debe ser distinto de cero; tanto `number` como `bigint` funcionan |
169
+ | `useFill` | `useFill<E>(element \| supplier, count) → SynchronousSemantic<E>` | O(n) | O(1) | El proveedor se llama una vez por elemento |
170
+ | `useEmpty` | `useEmpty<E>() → SynchronousSemantic<E>` | O(1) | O(1) | Elemento neutro para `concat` |
171
+ | `useText` | `useText(text, delimiter?)` | O(n) | O(n) para el texto | Itera **puntos de código**, por lo que los emoji y los CJK siguen siendo elementos únicos |
172
+ | `useAttribute` | `useAttribute<T>(object)` | O(v + e) | O(d + v) | Recorre recursivamente objetos anidados con protección contra ciclos |
173
+ | `useGenerate` | `useGenerate<E>(supplier, interrupt)` | O(n) × coste del proveedor | O(1) | **Debe** tener un `interrupt` de parada o un `limit` aguas abajo |
174
+ | `useIterate` | `useIterate<E>(Iterable<E>)` / `(AsyncIterable<E>)` | O(n) por recolección | O(1) | Reproduce el iterador en cada recolección |
175
+ | `useInterval` | `useInterval(period, delay?) → AsynchronousSemantic<number>` | n × period (tiempo real) | O(1) | Emite el tiempo transcurrido |
176
+ | `usePromise` | `usePromise<T>(promise) → AsynchronousSemantic<T>` | O(1) | O(1) | Un único elemento en el índice `0n` |
177
+ | `useBlob` | `useBlob(blob, chunk?) → AsynchronousSemantic<Uint8Array>` | O(blob size) | O(chunk) por copia | El fragmento predeterminado es 64 KiB |
178
+ | `useSubscription` | `useSubscription(target, subscriber, event) → AsynchronousSemantic<Event>` | un elemento por evento | O(1) | `limit()` / `takeWhile()` definen el límite de desuscripción |
179
+
180
+ ```typescript
181
+ useOf(1, 2, 3).toUnordered().toArray(); // [1,2,3]
182
+ useFrom(new Set([1, 2, 2])).toUnordered().toArray(); // [1,2]
183
+ useRange(0, 5).toUnordered().toArray(); // [0,1,2,3,4]
184
+ useRange(10n, 0n, -5n).toUnordered().toArray(); // [10n,5n]
185
+ useFill("x", 3n).toUnordered().toArray(); // ["x","x","x"]
186
+ useText("A😊B").toUnordered().toArray(); // ["A","😊","B"]
187
+ useGenerate(() => 7, (_e, index) => index >= 3n).toUnordered().toArray(); // [7,7,7]
188
+ ```
189
+
190
+ ---
191
+
192
+ <a id="5-transformation-operators"></a>
193
+ ## 5. Operadores de transformación
194
+
195
+ Cada operador devuelve un nuevo `Semantic`; ninguno toca un elemento hasta que se ejecuta un collectable. El tiempo es
196
+ el coste adicional **por elemento**; el espacio es lo que el operador retiene mientras la canalización se ejecuta.
197
+
198
+ | Operador | Propósito | Efecto en el índice | Tiempo | Espacio |
199
+ | :--- | :--- | :--- | :--- | :--- |
200
+ | `map(mapper)` | transformación 1:1 | reenviado | O(1) | O(1) |
201
+ | `filter(predicate)` | conserva los elementos que coinciden | reenviado | O(1) | O(1) |
202
+ | `flat(mapper)` / `flatMap(mapper)` | aplanado 1:N | reindexado desde 0 | O(1) por elemento emitido | O(1) |
203
+ | `distinct(keyExtractor?)` | descarta claves repetidas, conserva la primera | reenviado | O(1) promedio | O(n) |
204
+ | `peek(consumer)` | efecto secundario, el elemento pasa a través | reenviado | O(1) | O(1) |
205
+ | `limit(count)` | toma los primeros *n* | reindexado desde 0 | O(1) | O(1) |
206
+ | `skip(count)` | descarta los primeros *n* | reenviado | O(1) | O(1) |
207
+ | `sub(start, end)` | rebanada posicional `[start, end)` | reenviado | O(1) | O(1) |
208
+ | `takeWhile(predicate)` | toma mientras el predicado se cumple | reenviado | O(1) | O(1) |
209
+ | `dropWhile(predicate)` | descarta mientras el predicado se cumple | reindexado desde 0 | O(1) | O(1) |
210
+ | `concat(other)` | añade un semantic o un iterable | continúa el índice | O(1) | O(1) |
211
+ | `redirect(redirector)` | calcula un nuevo índice como quieras | **el tuyo** | O(1) | O(1) |
212
+ | `reverse()` | `redirect((_e, i) => -i - 1n)` | negado y desplazado | O(1) | O(1) |
213
+ | `translate(offset \| mapper)` | desplaza cada índice | `i + offset` | O(1) | O(1) |
214
+ | `shuffle(mapper?)` | aleatoriza el índice | aleatorio | O(1) | O(1) |
215
+ | `pipe(conversion)` | pasa el generador a una función personalizada | según tú | el tuyo | el tuyo |
216
+ | `semantic()` | eleva un collectable de nuevo a un semantic | sin cambios | O(1) | O(1) |
217
+
218
+ **`reverse`, `translate` y `shuffle` solo reescriben índices.** Nada se almacena en búfer ni se rota hasta que
219
+ `toOrdered()` normaliza los índices módulo el número de elementos:
220
+
221
+ ```typescript
222
+ useFrom([1, 2, 3, 4, 5]).reverse().toOrdered().toArray(); // [5,4,3,2,1]
223
+ useFrom([1, 2, 3, 4, 5]).translate(2).toOrdered().toArray(); // [4,5,1,2,3] (rotation)
224
+ useFrom([1, 2, 3, 4, 5]).translate(7).toOrdered().toArray(); // [4,5,1,2,3] (7 ≡ 2 mod 5)
225
+ useFrom([1, 2, 3]).shuffle((_e, i) => -i - 1n).toOrdered().toArray(); // [3,2,1]
226
+ ```
227
+
228
+ ---
229
+
230
+ <a id="6-collectables-and-terminal-operations"></a>
231
+ ## 6. Collectables y operaciones terminales
232
+
233
+ ### 6.1 Collectables
234
+
235
+ El collectable posee tanto el **orden** como el **control de índices**. `n` es el número de elementos.
236
+
237
+ | Collectable | Estructura | Tiempo | Orden | Úsalo cuando |
238
+ | :--- | :--- | :--- | :--- | :--- |
239
+ | `toUnordered()` | `Map<bigint, E>` indexado por índice | O(n) | inserción | máximo rendimiento; los índices sin procesar se conservan intactos |
240
+ | `toOrdered()` | array ordenado por índice de elemento | O(n log n) | por índice normalizado | la inversión o la rotación deben surtir efecto |
241
+ | `toOrdered(comparator)` | ordenado por tu comparador | O(n log n) | por tu comparador | vistas ordenadas |
242
+ | `toNumericStatistics()` | búfer ordenado por valor | O(n log n) | por valor | estadísticas sobre `number` |
243
+ | `toBigintStatistics()` | búfer ordenado por valor | O(n log n) | por valor | estadísticas sobre `bigint` |
244
+ | `toWindow()` | búfer ordenado por valor | O(n log n) | por valor | `slide` / `tumble` |
245
+ | `toCollectable(mapper)` | lo que devuelva el mapper | — | — | collectables personalizados |
246
+
247
+ ### 6.2 Terminales
248
+
249
+ Cada terminal vive en el **collectable**, no en el `Semantic`. Eso es lo que «los terminales son explícitos» significa en la práctica: una canalización no produce nada hasta que se nombra un collectable, y `toCollectable()` sin mapper es la forma más barata de nombrarlo.
250
+
251
+ | Terminal | Devuelve | Tiempo | Notas |
252
+ | :--- | :--- | :--- | :--- |
253
+ | `toArray()` / `toSet()` | `Array<E>` / `Set<E>` | O(n) | Vacía el búfer; `toSet` desduplica por identidad |
254
+ | `toMap(key, value?)` | `Map<K, V>` | O(n) promedio | Gana la última escritura |
255
+ | `count()` / `isEmpty()` | `bigint` / `boolean` | O(1) | El búfer ya lo sabe |
256
+ | `forEach(action)` | `void` | O(n) | `action(element, index)` |
257
+ | `join(…)` | `string` | O(n) | `join()`, `join(delimiter)`, `join(prefix, delimiter, suffix)` |
258
+ | `reduce(…)` | `Optional<E>` / `E` / `R` | O(n) | Flujo vacío → `Optional` vacío |
259
+ | `group(classifier)` / `groupBy(key, value)` | `Map<K, E[]>` | O(n) promedio | — |
260
+ | `partition(count)` | `Array<Array<E>>` | O(n) | Round-robin por índice |
261
+ | `partitionBy(classifier)` | `Array<Array<E>>` | O(n) | El número de cubo proviene del clasificador |
262
+ | `anyMatch` / `allMatch` / `nonMatch` | `boolean` | O(n) peor caso, con cortocircuito | — |
263
+ | `findAny` / `findFirst` / `findLast` | `Optional<E>` | O(1)–O(n) | Posicional en orden materializado |
264
+ | `findAt(k)` / `findNegativeAt(k)` | `Optional<E>` | O(k) / O(n−k) | Cuenta desde el principio / desde el final |
265
+ | `findMaximum(cmp?)` / `findMinimum(cmp?)` | `Optional<E>` | O(n) | Por defecto usa una comparación estructural |
266
+ | `frequency()` | `Map<E, bigint>` | O(n) promedio | Solo collectables de estadísticas |
267
+ | `write(stream, accumulator?)` | `WritableStream<S>` | O(n) | Terminal de tipo IO |
268
+ | `collect(collector)` | `R` | O(n) | El plegado genérico detrás de cada terminal anterior |
269
+
270
+ ### 6.3 Colectores de estructura
271
+
272
+ `toArray`, `toSet` y `toMap` terminan una canalización en un contenedor de JavaScript. Otros doce terminales la terminan en una de las estructuras de la propia biblioteca, de modo que el resultado lleva las invariantes que esa estructura garantiza en lugar de las que un array tiene por casualidad. Están disponibles en cada collectable, incluido el collectable desnudo que devuelve `toCollectable()`.
273
+
274
+ | Terminal | Devuelve | Orden | Coste |
275
+ | :--- | :--- | :--- | :--- |
276
+ | `toArray()` | `Array<E>` | orden de recorrido | O(n) |
277
+ | `toSet()` | `Set<E>` | orden de inserción, SameValueZero | O(n) promedio |
278
+ | `toMap(key, value?)` | `Map<K, V>` | orden de inserción; gana la última escritura | O(n) promedio |
279
+ | `toOrderedSet(comparator?)` | `SynchronousOrderedSet<E>` | ascendente según el comparador | O(n²) peor caso |
280
+ | `toUnorderedSet()` | `SynchronousUnorderedSet<E>` | sin especificar (cubos de hash) | O(n) esperado |
281
+ | `toOrderedMap(key, value?)` | `SynchronousOrderedMap<K, V>` | orden de inserción; gana la última escritura | O(n) esperado |
282
+ | `toUnorderedMap(key, value?)` | `SynchronousUnorderedMap<K, V>` | sin especificar | O(n) esperado |
283
+ | `toList()` | `SynchronousList<E>` | orden de recorrido, duplicados conservados | O(n) |
284
+ | `toRing(span?)` | `SynchronousRing<E>` | el orden de recorrido fija las posiciones en el anillo | O(n) |
285
+ | `toLRUSet(capacity)` | `SynchronousUnorderedLRUSet<E>` | por recencia; solo sobreviven los últimos `capacity` | O(n²) peor caso |
286
+ | `toLRUMap(capacity, key, value?)` | `SynchronousUnorderedLRUMap<K, V>` | por recencia | O(n) esperado |
287
+ | `toSkipSet(comparator?)` | `SynchronousSkipSet<E>` | ascendente según el comparador, navegable | O(n²) peor caso |
288
+ | `toSkipMap(key, value?)` | `SynchronousSkipMap<K, V>` | según el comparador de claves, navegable | O(n) esperado |
289
+ | `toBinarySearchTree(comparator?)` | `SynchronousBinarySearchTree<E>` | ascendente según el comparador, equilibrado en altura | O(n log n) |
290
+ | `toRootedForest()` | `SynchronousRootedForest<E>` | el orden de recorrido se vuelve la estructura | O(n) |
291
+
292
+ El tipo del resultado sigue el tipo de elemento del propio collectable, así que ningún terminal recibe un argumento de tipo: un `SynchronousCollectable<E>` entrega directamente `Array<E>`, `SynchronousList<E>` o `SynchronousOrderedSet<E>`. Solo quedan los argumentos de negocio: el comparador de `toOrderedSet` / `toSkipSet` / `toBinarySearchTree`, la capacidad del par LRU, el span de `toRing` y los extractores de clave y valor de la familia de mapas.
293
+
294
+ Un anillo es un conjunto, así que los elementos repetidos conservan su primera posición; el resto de estructuras de arriba conserva duplicados salvo que su propio contrato diga otra cosa (`toOrderedSet`, `toUnorderedSet` y `toSkipSet` son conjuntos; `toBinarySearchTree` y `toList` no lo son).
295
+
296
+ ```typescript
297
+ const numbers = useOf(3, 1, 4, 1, 5).toCollectable();
298
+
299
+ numbers.toOrderedSet().toArray(); // [1,3,4,5]
300
+ numbers.toUnorderedSet().size(); // 4n — the duplicate 1 collapses
301
+ numbers.toList().toArray(); // [3,1,4,1,5]
302
+ numbers.toSkipSet().first().get(); // 1 — navigable by comparator
303
+ numbers.toBinarySearchTree().size(); // 5n — duplicates retained
304
+ numbers.toRootedForest().getRoots().length; // 1
305
+ useOf(1, 2, 3).toCollectable().toRing().phaseAt(3n).get().toDegreeValue(); // 0
306
+ useOf(3, 1, 3).toCollectable().toOrderedMap(n => n, n => `v${n}`).get(3).get(); // "v3"
307
+ ```
308
+
309
+ Los collectables asíncronos exponen los mismos terminales y resuelven cada uno en una promesa, así que la forma de la llamada solo gana un `await`.
310
+
311
+ ---
312
+
313
+ <a id="7-statistics-and-windows"></a>
314
+ ## 7. Estadísticas y ventanas
315
+
316
+ ### 7.1 La superficie estadística
317
+
318
+ ```typescript
319
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().summate(); // 14
320
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().average(); // 2.8
321
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().median(); // 3
322
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().mode(); // 1
323
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().variance().get(); // 3.2 (n − 1)
324
+ useOf(3, 1, 4, 1, 5).toNumericStatistics().standardDeviation().get(); // 1.7889 (n − 1)
325
+ ```
326
+
327
+ Ambas familias estadísticas exponen la misma superficie — `summate`, `average`, `median`, `mode`, `variance`, `standardDeviation`, `frequency`, `findMaximum`, `findMinimum` — con aritmética `bigint` para `toBigintStatistics()`. Las medidas de dispersión y la familia de cuantiles devuelven un `Optional`, porque una muestra puede ser demasiado pequeña para que estén definidas; desenvuélvelas con `.get()`, `.orElse(…)` o `.isPresent()`.
328
+
329
+ ### 7.2 Estimadores y valores por defecto
330
+
331
+ Los valores por defecto son las definiciones que usan R, NumPy, pandas y Excel, así que un valor calculado aquí se puede comparar con esas herramientas sin factor de corrección.
332
+
333
+ | Estadístico | Defecto | Antes | Recuperar el valor antiguo con |
334
+ | :--- | :--- | :--- | :--- |
335
+ | `quantile(probability)` | Hyndman–Fan tipo 7 | tipo 1 | `{ method: "type-1" }` |
336
+ | `interquartileRange()` | Hyndman–Fan tipo 7 | tipo 1 | `{ method: "type-1" }` |
337
+ | `skewness()` | `G1` corregido por sesgo | `g1` | `{ estimator: "g1" }` |
338
+ | `kurtosis()` | `G2` de exceso corregido por sesgo | `g2` | `{ estimator: "g2" }` |
339
+ | `variance()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
340
+ | `standardDeviation()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
341
+
342
+ Los nueve tipos de Hyndman–Fan están disponibles como `"type-1"` … `"type-9"`, y `{ excess: false }` vuelve a sumar tres a la curtosis para informar del cuarto momento estandarizado no excesivo. Los tipos de opciones — `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator`, `VarianceOptions` — los exporta `mathematics/quantile.ts`.
343
+
344
+ ```typescript
345
+ const sample = useOf(1, 2, 3, 4).toNumericStatistics();
346
+
347
+ sample.variance().get(); // 1.6667 (5/3, ddof = 1)
348
+ sample.variance({ ddof: 0 }).get(); // 1.25
349
+ sample.standardDeviation().get(); // 1.2910
350
+ sample.skewness().get(); // 0
351
+ sample.kurtosis().get(); // -1.2 (G2)
352
+ sample.kurtosis({ estimator: "g2" }).get(); // -1.36
353
+ sample.kurtosis({ excess: false }).get(); // 1.8
354
+
355
+ const quartiles = useOf(1, 2, 3, 4, 5, 6, 7, 8).toNumericStatistics();
356
+ quartiles.quantile(0.75).get(); // 6.25 (type 7)
357
+ quartiles.quantile(0.75, { method: "type-1" }).get(); // 6 (nearest rank)
358
+ quartiles.interquartileRange().get(); // 3.5
359
+ ```
360
+
361
+ `G1` y `G2` no están definidos por debajo de tres y cuatro observaciones, y la varianza y la desviación típica muestrales tampoco por debajo de dos con `ddof = 1`. Eso se informa con un `Optional` vacío en lugar de con un cero engañoso:
362
+
363
+ ```typescript
364
+ useOf(1, 2, 3).toNumericStatistics().kurtosis().isEmpty(); // true (G2 needs four observations)
365
+ useOf(1, 2).toNumericStatistics().skewness().isEmpty(); // true (G1 needs three)
366
+ useOf(1).toNumericStatistics().variance().isEmpty(); // true (ddof = 1 needs two)
367
+ useOf(1).toNumericStatistics().variance({ ddof: 0 }).get(); // 0
368
+ ```
369
+
370
+ Las vistas bigint aceptan las mismas opciones y redondean cada resultado a un `bigint` entero.
371
+
372
+ ### 7.3 Ventanas
373
+
374
+ Las ventanas vuelven a exponer los elementos almacenados en el búfer como una canalización de canalizaciones, de modo que una ventana se puede consumir sin materializarla antes en un array. Ambas formas incluyen una última ventana parcial.
375
+
376
+ ```typescript
377
+ useFrom([1, 2, 3, 4]).toWindow().slide(2n, 1n).toUnordered().toArray()
378
+ .map(window => window.toUnordered().toArray()); // [[1,2],[2,3],[3,4],[4]]
379
+
380
+ useFrom([1, 2, 3, 4]).toWindow().tumble(2n).toUnordered().toArray()
381
+ .map(window => window.toUnordered().toArray()); // [[1,2],[3,4]]
382
+ ```
383
+
384
+ ---
385
+
386
+ <a id="8-the-index-model"></a>
387
+ ## 8. El modelo de índices
388
+
389
+ Un índice es un `bigint` asociado a cada elemento. Los operadores lo **reenvían**, lo **reindexan**
390
+ desde cero o te dejan **calcularlo**.
391
+
392
+ La recolección ordenada normaliza cada índice módulo el número de elementos y luego ordena por el resultado. Por eso
393
+ `translate(7)` sobre cinco elementos equivale a `translate(2)`: `7 ≡ 2 (mod 5)`.
394
+
395
+ La recolección desordenada conserva el índice que produjo la canalización y devuelve los elementos en orden de llegada. Los
396
+ índices negativos son legales y, una vez normalizados, simplemente ordenan antes que los no negativos.
397
+
398
+ ---
399
+
400
+ <a id="9-the-asynchronous-model"></a>
401
+ ## 9. El modelo asíncrono
402
+
403
+ `AsynchronousSemantic` refleja la superficie síncrona devolviendo promesas. Las escrituras se encolan, así que una
404
+ lectura emitida después de una escritura siempre la observa.
405
+
406
+ ```typescript
407
+ const ring = new AsynchronousRing([1, 2]);
408
+ const added = ring.add(3); // queued
409
+ const sampled = ring.elementAt(2n); // observes the write above
410
+ await added;
411
+ (await sampled).get().element; // 3
412
+ ```
413
+
414
+ Las fábricas que son asíncronas por naturaleza — `usePromise`, `useBlob`, `useInterval`,
415
+ `useSubscription` — devuelven `AsynchronousSemantic` directamente, y `useFrom` / `useIterate` se
416
+ actualizan solas cuando reciben un `AsyncIterable`.
417
+
418
+ ---
419
+
420
+ <a id="10-periodic-analysis-rings-fields-and-scores"></a>
421
+ ## 10. Análisis periódico: anillos, campos y puntuaciones
422
+
423
+ La mitad matemática de la biblioteca trata un ciclo como un objeto de primera clase en lugar de una tabla
424
+ de consulta. Tres construcciones trabajan juntas.
425
+
426
+ ### 10.1 Un anillo es un entero
427
+
428
+ Un `Ring` es un **anillo n-ésimo** (n-fold ring): el número de elementos, el periodo posicional y el número de fases
429
+ distintas son el mismo entero `n`. La fase de una posición se calcula a partir de su residuo exacto, así que el
430
+ anillo es periódico hasta el bit:
431
+
432
+ ```typescript
433
+ import { SynchronousRing } from "semantic-typescript";
434
+
435
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
436
+ ring.phaseAt(0n).get().toDegreeValue(); // 0
437
+ ring.phaseAt(1n).get().toDegreeValue(); // 60
438
+ ring.phaseAt(6n).get().getValue() === ring.phaseAt(0n).get().getValue(); // true, exactly
439
+ ring.purity(); // 1
440
+ ring.isBalanced(); // true
441
+ ring.sequences().map(z => z.magnitude()); // [0, 6, 0, 0, 0, 0]
442
+ ```
443
+
444
+ `frequency` es la tasa de rotación **ambiente**, no un paso de fase entre posiciones: el entorno
445
+ desplaza todos los fasores un mismo ángulo, así que nunca puede romper el patrón intrínseco. `span` fija cuántas
446
+ vueltas ocupa el anillo.
447
+
448
+ `purity()` es la proporción de energía transportada por el modo fundamental,
449
+ `max(|c₁|², |c_{n−1}|²) / (n · Σ|z|²)`. Vale `1` exactamente cuando el anillo es la raíz n-ésima de la unidad
450
+ salvo rotación, y cuesta `O(n)` porque solo se necesitan dos bins. Se prueban ambas direcciones
451
+ porque un anillo puede recorrer el círculo en cualquiera de los dos sentidos.
452
+
453
+ ### 10.2 Un campo es una puntuación acotada
454
+
455
+ Un `PhaseField` asigna una fase relativa a una puntuación en `[-1, 1]`:
456
+
457
+ ```typescript
458
+ import { useField, useRadian } from "semantic-typescript";
459
+
460
+ const cosine = useField([1]); // cos(2πΔ): +1 aligned, −1 opposed
461
+ const mixed = useField([0.4, 0.3, 0.3]); // first three harmonics
462
+
463
+ cosine.sample(useRadian(0)); // 1
464
+ cosine.sample(useRadian(Math.PI)); // -1
465
+ ```
466
+
467
+ La puntuación es una serie de cosenos truncada, `a₀ + Σ wₘ·cos(2πmΔ)`, y el constructor impone
468
+ `|a₀| + Σ|wₘ| ≤ 1`. Por la desigualdad triangular la puntuación nunca puede salir de `[-1, 1]`: la cota es una
469
+ **consecuencia de la construcción**, no un recorte aplicado a posteriori.
470
+
471
+ La razón de que el campo sea una serie y no una tabla: una tabla de `n` puntos reproduce exactamente sus propias
472
+ muestras, pero su interpolante trigonométrico se pasa entre ellas. Una tabla de cinco puntos con puntuaciones
473
+ `[+1, +0.5, −1, −0.5, 0]` interpola sobre `[−1.0428, 1.0041]`, lo que ya rompe la promesa.
474
+ Una serie acotada no puede.
475
+
476
+ ### 10.3 El análisis mide los datos contra cada elemento
477
+
478
+ Cada elemento del anillo es una fase de referencia. `analyse` sitúa cada observación de vuelta en el anillo y
479
+ la mide contra cada referencia:
480
+
481
+ ```typescript
482
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e"]);
483
+ const report = ring.analyse(["a", "c", "c", "a", "zz", "e"]);
484
+
485
+ report.matched; // 5
486
+ report.missed; // 1 ('zz' is not on the ring)
487
+ report.references.map(r => r.count); // [2, 0, 2, 0, 1]
488
+ ```
489
+
490
+ Cada fila de referencia informa de `count`, la fase relativa media `mean` y la longitud resultante media
491
+ `resultant`. Merece la pena recordar una identidad: **`resultant` es la misma para todas las referencias**,
492
+ porque cambiar la línea base solo rota todas las fases relativas en una constante. Lo que difiere por
493
+ referencia es el recuento y la media.
494
+
495
+ ```typescript
496
+ const ring = new SynchronousRing(["a", "b", "c"]);
497
+ ring.scoresAt(0, cosine); // one score per position, in [-1, 1]
498
+ ```
499
+
500
+ ### 10.4 Instrumentos relacionados
501
+
502
+ | Llamada | Qué responde | Coste |
503
+ | :--- | :--- | :--- |
504
+ | `purity()` | ¿el anillo sigue siendo la raíz n-ésima de la unidad? | O(n) |
505
+ | `resultant(t?)` | ¿cómo de concentrados están los fasores ahora mismo? | O(n) |
506
+ | `analyse(data)` | ¿cómo se sitúan estos datos frente a cada elemento? | O(n · m) |
507
+ | `toSpectrum()` | el anillo como espectro, con el número de onda en el eje de frecuencias | O(n²) |
508
+ | `useAngularSpectrum(ring, instants, plan)` | el plano `(ω, n)` a lo largo del tiempo | O(n² · frames) |
509
+
510
+ Otros dos primitivos completan el modelo: `combineRings(a, b)` une dos anillos en su mínimo común
511
+ múltiplo (un pullback, no un producto: los pares que no pueden compartir un instante simplemente están ausentes), y
512
+ `rate()` da a cada elemento su propia frecuencia intrínseca para que un anillo pueda derivar respecto al
513
+ entorno. `jetAt`, `windingAt` y `errorIntegralAt` exponen el estado diferencial de esa deriva.
514
+
515
+ ### 10.5 Ventanas espectrales
516
+
517
+ Las construcciones de anillos son una mitad de la superficie analítica; la otra trabaja sobre señales muestreadas. Su primitiva más reutilizada es la ventana, y cada forma de aquí tiene dos variantes. La variante simétrica divide la trama por `count − 1`; la variante periódica (DFT par) la divide por `count`, un muestreo menos que un periodo completo, de modo que el taper es continuo bajo la extensión periódica que supone la DFT.
518
+
519
+ ```typescript
520
+ useHannWindow(1, 4); // 0.75 symmetric
521
+ useHannPeriodicWindow(1, 4); // 0.5 periodic
522
+ coherentGain(useHannPeriodicWindow, 16); // 0.5
523
+ equivalentNoiseBandwidth(useHannPeriodicWindow, 16); // 1.5
524
+ ```
525
+
526
+ | Forma | Simétrica | Periódica | Notas |
527
+ | :--- | :--- | :--- | :--- |
528
+ | Rectangular | `useRectangularWindow` | — | sin taper; sin distinción periódica |
529
+ | Hann | `useHannWindow` | `useHannPeriodicWindow` | la opción por defecto del análisis de corto plazo |
530
+ | Hamming | `useHammingWindow` | `useHammingPeriodicWindow` | — |
531
+ | Blackman | `useBlackmanWindow` | `useBlackmanPeriodicWindow` | — |
532
+ | Nuttall | `useNuttallWindow` | `useNuttallPeriodicWindow` | Blackman–Harris de cuatro términos |
533
+ | Flat-top | `useFlatTopWindow` | `useFlatTopPeriodicWindow` | precisión de amplitud antes que resolución |
534
+ | Kaiser | `useKaiserWindow(beta)` | `useKaiserPeriodicWindow(beta)` | `beta` fija el ancho del lóbulo principal |
535
+
536
+ Usa la variante periódica para el análisis y el overlap-add: con un salto de media trama, la ventana de Hann periódica suma `1.000000` a cualquier longitud, mientras que la simétrica ondula `0.841 dB` con `N = 16` y todavía `0.013 dB` con `N = 1024`. Conserva la variante simétrica para el diseño de filtros, donde la trama no es un periodo del taper.
537
+
538
+ Las ventanas flat-top devuelven **pesos negativos** cerca de los extremos de la trama, porque una ventana flat-top es exactamente eso. Se devuelven sin recortar, porque recortarlos perturba el lóbulo principal y destruye la baja pérdida de scalloping que justifica la existencia de la ventana:
539
+
540
+ ```typescript
541
+ useFlatTopWindow(0, 1024); // -0.000421 — negative by definition
542
+ Array.from({ length: 1024 }, (_unused, index) => useFlatTopWindow(index, 1024))
543
+ .filter(weight => weight < 0).length; // 578
544
+ ```
545
+
546
+ ### 10.6 Niveles, planitud y energía de banda
547
+
548
+ `decibel` ha desaparecido como función única, porque una amplitud y una potencia no comparten factor. `amplitudeDecibel` aplica `20·log10` a una razón de tipo tensión; `powerDecibel` aplica `10·log10` a una potencia, una energía, un cuadrado medio o una densidad espectral de potencia:
549
+
550
+ ```typescript
551
+ amplitudeDecibel(0.25); // -12.0412
552
+ powerDecibel(0.25); // -6.0206
553
+ ```
554
+
555
+ `Bin.decibel` y `Spectrum.decibelOf` son niveles de amplitud; `Spectrum.powerDecibelOf` es el nivel de potencia correspondiente. Dar una potencia a la forma de amplitud informa `20·log10(power)` y duplica el nivel en decibelios.
556
+
557
+ Dos medidas del espectro cambiaron de lectura:
558
+
559
+ ```typescript
560
+ const twoBins = new Spectrum(new Sample(2, 1), [new Complex(2, 0), new Complex(1, 0)]);
561
+ twoBins.flatness(); // 0.8 — geometric over arithmetic mean of power
562
+
563
+ const shape = new Spectrum(new Sample(4, 1), [
564
+ new Complex(1, 0), new Complex(2, 0), new Complex(0, 0), new Complex(0, 0),
565
+ ]);
566
+ shape.bandEnergy(new Frequency(0), new Frequency(2)); // 9
567
+ shape.bandEnergy(new Frequency(0), new Frequency(2), "two-sided"); // 5
568
+ ```
569
+
570
+ `flatness()` compara las medias geométrica y aritmética de `power()` sobre la mitad unilateral del espectro, no la media de magnitudes sobre ambas mitades, y el doblado de amplitud de `oneSided()`, que es solo para visualización, no se aplica deliberadamente. `bandEnergy()` usa por defecto la lectura unilateral, plegando la mitad conjugada: DC y Nyquist cuentan una vez y cada bin interior cuenta dos; una banda que cubre todo el eje satisface entonces Parseval, `Σ|X|² = N·Σx²`. Pasa `"two-sided"` para un espectro complejo, cuyos bins ya son independientes.
571
+
572
+ ### 10.7 Análisis de corto plazo
573
+
574
+ `Sampler.shortTimeFourierTransform` y `Sampler.spectrogram` siguen las convenciones de `stft` de SciPy: ventana de Hann periódica, borde relleno de ceros en ambos extremos y cola rellena de ceros.
575
+
576
+ ```typescript
577
+ const sampler = new Sampler(new Sample(64, 1));
578
+ const spectrogram = sampler.spectrogram(time => Math.cos(2 * Math.PI * 4 * time), 16, 8);
579
+
580
+ spectrogram.size(); // 9 frames
581
+ spectrogram.getBoundary(); // "zeros"
582
+ spectrogram.getTime(6); // 0.75 — frame centres, not frame starts
583
+ sampler.spectrogram(time => Math.cos(2 * Math.PI * 4 * time), 16, 8, useHannWindow,
584
+ { boundary: "none", padded: false }).size(); // 7 — complete frames only
585
+ ```
586
+
587
+ La extensión centra la primera trama en el primer muestreo, y el relleno de cola mantiene alcanzables los últimos muestreos: 64 muestreos con trama 16 y salto 8 producen 9 tramas en vez de 7, y 1000 muestreos con trama 256 y salto 128 producen 9 en vez de 6. Los tiempos de trama son centros — `(k·hop + (frameSize mod 2)/2)/rate` para una vista extendida, `(k·hop + frameSize/2)/rate` para una no extendida — y `toFrame()` recorta la extensión antes de invertir.
588
+
589
+ ---
590
+
591
+ <a id="11-utility-helpers"></a>
592
+ ## 11. Funciones auxiliares
593
+
594
+ ### 11.1 La jerarquía de errores
595
+
596
+ `structure/error.ts` exporta una jerarquía de quince clases para los errores lanzados *alrededor* de una canalización: por tu propio código, un colector personalizado o un adaptador.
597
+
598
+ ```text
599
+ SemanticError
600
+ ├── IllegalOperationError
601
+ ├── ValidationError
602
+ │ ├── IndexError
603
+ │ ├── RangeError (also exported as SemanticRangeError)
604
+ │ └── ConfigurationError
605
+ ├── CapacityError
606
+ ├── StateError
607
+ │ └── IllegalStateError
608
+ ├── ConcurrencyError
609
+ ├── CollectionError
610
+ ├── SerializationError
611
+ ├── NumericError
612
+ │ └── ArithmeticError
613
+ └── PhaseError
614
+ ```
615
+
616
+ Cada clase fija `name`, reenvía la `cause` nativa, lleva una bolsa `context` opcional y se serializa mediante `toJSON()`; cada una tiene una guarda de marca con su mismo nombre.
617
+
618
+ ```typescript
619
+ const error = new CapacityError("LRU capacity exhausted", { context: { capacity: 0 } });
620
+
621
+ error.name; // "CapacityError"
622
+ isCapacityError(error); // true
623
+ isValidationError(error); // false — a capacity failure is not a validation failure
624
+ isSemanticError(error); // true
625
+ error.toJSON(); // { name: "CapacityError", message: "…", context: { capacity: 0 } }
626
+ ```
627
+
628
+ `SemanticRangeError` es la misma clase que el `RangeError` exportado, así que `instanceof` se cumple con ambos nombres; importar `RangeError` ensombrece el constructor global en todo el módulo, y por eso existe el alias. Ninguna de las dos clases la lanza la comprobación de argumentos de la biblioteca, que sigue lanzando `TypeError` y `RangeError` nativos: la jerarquía es el vocabulario de la capa de arriba.
629
+
630
+ ### 11.2 Tabla de utilidades
631
+
632
+ | Auxiliar | Propósito |
633
+ | :--- | :--- |
634
+ | `useCompare(a, b)` | Orden total entre primitivos mixtos |
635
+ | `useToNumber` / `useToBigInt` | Conversión numérica sin pérdida y con comprobación de rango |
636
+ | `useRandom` | Auxiliares aleatorios con semilla o sin ella |
637
+ | `useTraverse` | Recorrido estructural de objetos y arrays anidados |
638
+ | `useStringify` | Serialización JSON segura ante referencias circulares |
639
+ | `useHash` / `maskOf` / `register` / `unregister` | Hash estructural y el registro de estrategias de hash |
640
+ | `invalidate` / `validate` | Guardas que lanzan una forma de error coherente |
641
+ | `Optional<T>` | El sustituto de `null` de la biblioteca: `Optional.of(x)` / `Optional.empty()` |
642
+
643
+ ### 11.3 `useTraverse`
644
+
645
+ `useTraverse(target, callback)` aplana cualquier objeto en sus **hojas no nulas**. Desciende por los objetos anidados, recorre los valores iterables por posición, omite `null` y `undefined` y nunca vuelve a entrar en un objeto que ya visitó; así, un grafo de objetos con un ciclo termina en lugar de desbordar la pila.
646
+
647
+ ```typescript
648
+ const source = { id: 7, tags: ["a", "b"], owner: { name: "Ada", note: null } };
649
+ const leaves: string[] = [];
650
+
651
+ useTraverse(source, (key, value, path) => {
652
+ leaves.push(`${path.join(".")} = ${String(value)}`);
653
+ return true;
654
+ });
655
+
656
+ leaves; // ["id = 7", "tags.0 = a", "tags.1 = b", "owner.name = Ada"]
657
+ ```
658
+
659
+ El callback se ejecuta una vez por hoja y recibe la clave propia de la hoja (su índice cuando proviene de un iterable), el valor y la ruta que lleva hasta ella. Devolver `false` abandona **todo** el recorrido, no solo la rama actual:
660
+
661
+ ```typescript
662
+ let visited = 0;
663
+ useTraverse({ a: 1, b: 2, c: 3 }, () => {
664
+ visited += 1;
665
+ return false;
666
+ });
667
+ visited; // 1 — the first leaf already stops the walk
668
+ ```
669
+
670
+ Un ciclo es seguro por la misma razón:
671
+
672
+ ```typescript
673
+ const cyclic: any = { name: "root", child: { name: "leaf" } };
674
+ cyclic.child.parent = cyclic;
675
+
676
+ const names: string[] = [];
677
+ useTraverse(cyclic, (_key, value) => {
678
+ if (typeof value === "string") names.push(value);
679
+ return true;
680
+ });
681
+ names; // ["root", "leaf"] — the back edge to `cyclic` is not revisited
682
+ ```
683
+
684
+ **Cada callback recibe su propia instantánea de la ruta.** El recorrido mantiene internamente un único array de trabajo y lo copia, en el momento de entregar una hoja, a un array nuevo congelado con `Object.freeze`. La instantánea ya no cambia, así que puede guardarse tal cual; dos callbacks nunca comparten un array, y repetir el recorrido no altera las instantáneas ya entregadas. Su tipo es `ReadonlyArray`, de modo que escribir en ella es un error de compilación, y el array congelado lanza además un `TypeError` en tiempo de ejecución. Esa seguridad cuesta una asignación `O(depth)` por hoja informada; una rama que no informa nada no asigna nada extra.
685
+
686
+ ```typescript
687
+ const paths: Array<ReadonlyArray<PropertyKey>> = [];
688
+ useTraverse({ a: { b: 1 }, c: 2 }, (_key, _value, path) => {
689
+ paths.push(path); // safe to keep as-is
690
+ return true;
691
+ });
692
+ paths[0] === paths[1]; // false — one array per call
693
+ paths.map(path => path.join(".")); // ["a.b", "c"]
694
+ ```
695
+
696
+ `useAttribute` es este recorrido convertido en fábrica: informa de las mismas hojas como pares `{ key, value }`, así que las reglas —y el coste— son idénticos.
697
+
698
+ ```typescript
699
+ useAttribute({ a: 1, b: { c: 2 } }).toUnordered().toArray();
700
+ // [{ key: "a", value: 1 }, { key: "c", value: 2 }]
701
+ ```
702
+
703
+ ---
704
+
705
+ <a id="12-performance-and-pitfalls"></a>
706
+ ## 12. Rendimiento y problemas comunes
707
+
708
+ **La pereza tiene un precio por etapa.** Cada operador añade un salto de generador. Para bucles numéricos
709
+ ajustados sobre arrays en memoria, un `for` simple superará a cualquier canalización; la canalización gana cuando la fuente es
710
+ perezosa, infinita, o la canalización es lo bastante larga como para que la disciplina de índices compense.
711
+
712
+ **`toOrdered()` ordena; `toUnordered()` no.** Si una inversión parece "no haber funcionado", el
713
+ colector era desordenado: la reescritura de índices solo se convierte en orden cuando los índices se normalizan.
714
+
715
+ **`useGenerate` sin un `interrupt` nunca termina.** Proporciona siempre un predicado de parada o un
716
+ `limit` / `takeWhile` aguas abajo.
717
+
718
+ **`Ring` es un conjunto ordenado por inserción.** Los elementos son únicos y conservan el orden en
719
+ que se añadieron por primera vez. Un `add` duplicado devuelve `false`, y un `set` que introduciría
720
+ uno lanza una excepción. Esto es lo que permite que una fase sea función del *elemento* y no de la
721
+ posición que resulte ocupar.
722
+
723
+ **`Spectrum` asume una señal real en sus auxiliares de un solo lado.** `oneSided()`, `getNyquist()` y
724
+ `centroid()` asumen simetría conjugada. El espectro espacial de un anillo de fase es complejo y en
725
+ general no simétrico: un anillo que recorre el círculo hacia atrás pone toda su energía en el bin
726
+ `n − 1`, más allá de Nyquist. Lee los bins directamente.
727
+
728
+ **Los colectores de estructura son terminales, no vistas.** Cada uno vacía la canalización y luego construye una estructura completa, así que cuesta O(n) de tiempo y de espacio y no puede ejecutarse sobre una fuente infinita. Úsalo al final de una canalización, nunca dentro de un bucle sobre una.
729
+
730
+ **`variance()` y sus compañeros devuelven un `Optional`.** `variance().get()` sobre una muestra demasiado pequeña lanza una excepción; usa `.orElse(0)` o comprueba `.isPresent()` cuando el tamaño de la muestra no está bajo tu control. Lo mismo vale para `standardDeviation`, `quantile`, `interquartileRange`, `skewness` y `kurtosis`.
731
+
732
+ **Las ventanas periódicas son las ventanas de análisis.** `useHannWindow` y las demás formas simétricas son para diseño de filtros; un análisis de corto plazo quiere `useHannPeriodicWindow` y sus hermanas, o la suma de overlap-add ondulará en cada unión de tramas.
733
+
734
+ ---
735
+
736
+ <a id="13-phase-rings"></a>
737
+ ## 13. Anillos de fase: un ciclo exacto
738
+
739
+ El capítulo 10 presentó el anillo como el modelo del ciclo y lo usó para medir un conjunto de datos
740
+ contra cada fase. Este capítulo es la superficie completa: cómo una posición se convierte en un
741
+ ángulo, qué cambia y qué no cambia la frecuencia ambiente, y los instrumentos que leen el anillo
742
+ contra el tiempo, contra los datos y contra otro anillo.
743
+
744
+ Un `SynchronousRing` es una lista posicional respaldada por un anillo circular doblemente enlazado
745
+ cuya cabeza y cola están unidas. Es también un *conjunto* ordenado por inserción: ningún elemento
746
+ aparece dos veces, y una escritura rechazada deja intactos tanto los enlaces como las muestras. Lo
747
+ que lo convierte en anillo y no en una lista es que la fase de una posición es una función pura de
748
+ su resto módulo la longitud actual, así que el anillo es periódico al bit y su equilibrio no puede
749
+ derivar.
750
+
751
+ ### 13.1 Un anillo es un entero, calculado exactamente
752
+
753
+ Un anillo lleva `n` elementos, y el número de elementos, el periodo posicional y el número de fases
754
+ distintas son el mismo entero. La posición `i` lleva
755
+
756
+ `phase + span · 2π · (((i mod n) + n) mod n) / n`
757
+
758
+ con el resto reducido en `bigint` antes de convertirse en `number`. Esa es toda la razón por la que
759
+ `phaseAt(i)` y `phaseAt(i + n)` son *idénticos* y no meramente parecidos: la segunda llamada se
760
+ reduce al mismo entero, evalúa el mismo producto y produce el mismo doble. Un índice mucho más allá
761
+ del rango entero exacto de `number` aterriza igualmente en la posición correcta, y un índice
762
+ negativo recorre el anillo hacia atrás.
763
+
764
+ | Llamada | Devuelve | Significado |
765
+ | :--- | :--- | :--- |
766
+ | `new SynchronousRing(source, options?)` | anillo | `source` es cualquier iterable, los duplicados se descartan |
767
+ | `getPhase()` / `getFrequency()` / `getSpan()` | `Angle`, `Frequency`, `number` | los tres parámetros de construcción |
768
+ | `size()` | `bigint` | el `n` que también es el periodo |
769
+ | `phaseAt(index)` | `Optional<Angle>` | solo fase intrínseca; ignora tiempo, tasa y frecuencia |
770
+ | `elementAt(index)` | `Optional<RingElement>` | elemento, fase intrínseca, frecuencia ambiente, fasor |
771
+ | `frequencyAt(index)` | `Optional<Frequency>` | la tasa ambiente, vacía en un anillo vacío |
772
+ | `toComplex()` / `toComplex(index)` | `Complex[]` / `Optional<Complex>` | fasores intrínsecos |
773
+
774
+ `RingOptions` añade dos funciones por elemento: `magnitude(element, index)` da la longitud del fasor
775
+ y vale uno por defecto, y `rate(element, index)` da al elemento su propia frecuencia intrínseca y
776
+ vale cero por defecto, es decir, un marcador fijo.
777
+
778
+ ```typescript
779
+ import { SynchronousRing } from "semantic-typescript";
780
+
781
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
782
+
783
+ ring.size(); // 6n
784
+ ring.phaseAt(0n).get().toDegreeValue(); // 0
785
+ ring.phaseAt(1n).get().toDegreeValue(); // 60
786
+ ring.phaseAt(-1n).get().toDegreeValue(); // 300
787
+ ring.phaseAt(6n).get().getValue() === ring.phaseAt(0n).get().getValue(); // true
788
+ ring.phaseAt(10n ** 20n).get().toDegreeValue(); // 240
789
+ ```
790
+
791
+ `phaseAt(-1n)` vale `300` en un anillo de seis elementos, y `10n ** 20n` se reduce a `4` módulo
792
+ seis, así que se lee `240`. Ninguno de los dos casos acumula un producto en coma flotante, que es lo
793
+ que mantiene exacta la identidad cuando cambia la representación del índice.
794
+
795
+ ### 13.2 La frecuencia ambiente y el estado en un instante
796
+
797
+ `frequency` es la **velocidad de rotación ambiente** del entorno en el que está inmerso el anillo,
798
+ no un paso de fase entre posiciones. El entorno avanza la fase de cada elemento el mismo ángulo, así
799
+ que no puede cambiar el patrón intrínseco, ni romper el equilibrio, ni alterar ningún resultado de
800
+ `phaseAt`. Su valor por defecto es `0 Hz`.
801
+
802
+ `relativeAt(index, time)` resta ese ángulo común y deja la coordenada contra la que se puntúa un
803
+ `PhaseField`. `viewAt` y `viewsAt` devuelven el estado completo en un instante, incluida la `rate`
804
+ propia del elemento, y los accesores diferenciales exponen la deriva:
805
+
806
+ | Llamada | Fórmula | Lee |
807
+ | :--- | :--- | :--- |
808
+ | `relativeAt(index, time)` | `intrínseca + tasaPropia · t − ambiente · t` | la fase que ve el entorno |
809
+ | `viewAt(index, time)` / `viewsAt(time)` | lo mismo, más elemento, índice, tiempo, tasa, fasor | el estado completo |
810
+ | `jetAt(index, time)` | fase en `t`, tasa propia, `slip = tasaPropia − ambiente` | el estado diferencial |
811
+ | `windingAt(index, time)` | `trunc(slip · t)` | vueltas enteras deslizadas, truncadas hacia cero |
812
+ | `errorIntegralAt(index, time, origin?)` | `∫ fase relativa dt`, en vueltas · segundos | cuánto tiempo lleva adelantado un elemento |
813
+ | `resultant(time?)` | `abs(Σ w · e^{iθ}) / Σ w` | concentración de fase, inmune a la rotación ambiente |
814
+ | `analyse(data, time?)` | cuentas y medias por referencia | los datos medidos contra cada elemento |
815
+ | `scoreAt(index, time, field)` / `scoresAt(time, field)` | el campo muestreado en la fase relativa | una puntuación acotada por posición |
816
+
817
+ ```typescript
818
+ import { SynchronousRing, useHertz } from "semantic-typescript";
819
+
820
+ const moving = new SynchronousRing(["a", "b", "c", "d", "e", "f"], {
821
+ frequency: useHertz(1),
822
+ });
823
+
824
+ moving.getFrequency().getHertz(); // 1
825
+ moving.phaseAt(1n).get().toDegreeValue(); // 60 — intrinsic
826
+ moving.relativeAt(1n, 0.25).get().toDegreeValue(); // 330 — 60 − 90
827
+ moving.viewAt(1n, 0.25).get().relative.toDegreeValue(); // 330
828
+ moving.jetAt(1n, 0.25).get().slip; // -1
829
+ moving.purity(); // 1 — rotation cannot break it
830
+ ```
831
+
832
+ A `1 Hz` el entorno avanza un cuarto de vuelta en `0.25 s`, así que la fase relativa de la posición
833
+ uno es `60 − 90 = 330` grados mientras su fase intrínseca sigue siendo `60`. El jet informa un
834
+ `slip` de `-1` hercios porque el elemento mismo no se mueve, y `purity()` sigue valiendo `1`:
835
+ rotar todos los fasores el mismo ángulo no es una deformación.
836
+
837
+ ### 13.3 Pureza y equilibrio
838
+
839
+ `purity()` es la parte de la energía del anillo que porta el modo fundamental:
840
+
841
+ `max(abs(c₁)², abs(c_{n−1})²) / (n · Σ abs(z)²)`
842
+
843
+ Se prueban los dos bins fundamentales porque un anillo puede recorrer el círculo en cualquier
844
+ dirección. Un anillo construido con `span: 5` sobre seis elementos avanza `300` grados, es decir
845
+ `−60` grados: gira al revés, coloca toda su energía en `c₅` y una fórmula que solo mirara `c₁` lo
846
+ declararía impuro. El coste sigue siendo `O(n)` porque solo hacen falta esos dos bins y la energía
847
+ total.
848
+
849
+ Los casos límite forman parte del contrato. Un anillo de menos de dos elementos no tiene modo
850
+ fundamental y reporta `purity() === 0` e `isBalanced() === false`, sea cual sea su contenido. Un
851
+ anillo cuyos fasores tienen todos longitud cero no tiene energía y también reporta cero.
852
+ `isBalanced(epsilon?)` es `1 − purity() <= epsilon`, con `1E-9` como tolerancia por defecto.
853
+
854
+ ```typescript
855
+ import { SynchronousRing } from "semantic-typescript";
856
+
857
+ const forward = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
858
+ const backward = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 5 });
859
+ const weighted = new SynchronousRing(["a", "b", "c", "d"], {
860
+ magnitude: element => element === "a" ? 1 : 0,
861
+ });
862
+
863
+ forward.purity(); // 1
864
+ backward.phaseAt(1n).get().toDegreeValue(); // 300 — it runs backwards
865
+ backward.purity(); // 1 — the energy sits in c₅
866
+ new SynchronousRing(["only"]).purity(); // 0
867
+ new SynchronousRing(["only"]).isBalanced(); // false
868
+ weighted.purity(); // 0.25
869
+ ```
870
+
871
+ ### 13.4 Longitud resultante y componentes simétricas
872
+
873
+ `resultant(time?)` es la longitud resultante media: el módulo de la suma de fasores dividido por la
874
+ suma de los módulos. A diferencia de `purity`, que pregunta si el anillo sigue siendo las raíces
875
+ `n`-ésimas de la unidad, `resultant` pregunta cuán concentrados están los fasores ahora mismo. Un
876
+ anillo equilibrado reporta `0`; un anillo cuyos elementos comparten un mismo ángulo reportaría `1`,
877
+ pero la semántica de conjunto prohíbe los duplicados, así que en la práctica `1` aparece cuando
878
+ `magnitude` anula todos menos uno.
879
+
880
+ `zeroSequence()` es el fasor medio, es decir `c₀ / n`. `sequences()` devuelve las `n` componentes
881
+ simétricas, de menor orden a mayor, mediante una transformada directa. `toSpectrum()` las envuelve
882
+ en un `Spectrum` cuyo bin `j` reporta `j` ciclos por vuelta de anillo, así que el eje de frecuencias
883
+ es el eje de números de onda.
884
+
885
+ ```typescript
886
+ import { SynchronousRing } from "semantic-typescript";
887
+
888
+ const round = (value: number): number => Number(value.toFixed(4));
889
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
890
+
891
+ round(ring.resultant().get()); // 0
892
+ round(ring.zeroSequence().get().magnitude()); // 0
893
+ ring.sequences().map(component => round(component.magnitude())); // [0, 6, 0, 0, 0, 0]
894
+ ring.toSpectrum().get().size(); // 6
895
+ ring.toSpectrum().get().getResolution().getHertz(); // 1
896
+ ```
897
+
898
+ Las componentes son complejas y en general **no conjugadas-simétricas**: un anillo equilibrado pone
899
+ toda su energía en el bin uno, y uno que gira al revés en el bin `n − 1`. Los ayudantes unilaterales
900
+ de `Spectrum` suponen una señal real; lea los bins directamente en lugar de plegarlos.
901
+
902
+ ### 13.5 Analizar datos contra cada elemento
903
+
904
+ `analyse(data, time?)` empareja cada observación con el anillo por identidad y la mide contra cada
905
+ elemento como fase de referencia. Las observaciones que el anillo no contiene se cuentan como
906
+ `missed` en lugar de adivinarse, porque el anillo es la única fuente de fases. Cada fila de
907
+ referencia reporta `count`, la fase relativa media `mean` y la longitud resultante media `resultant`.
908
+
909
+ Una identidad merece recordarse: **`resultant` es la misma para cada referencia**. Cambiar la base
910
+ solo rota todas las fases relativas una constante, y una rotación no puede cambiar una longitud
911
+ resultante. Lo que difiere por referencia es la cuenta y la media.
912
+
913
+ ```typescript
914
+ import { SynchronousRing } from "semantic-typescript";
915
+
916
+ const round = (value: number): number => Number(value.toFixed(4));
917
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e"]);
918
+ const report = ring.analyse(["a", "c", "c", "a", "zz", "e"]);
919
+
920
+ report.matched; // 5
921
+ report.missed; // 1
922
+ report.references.map(row => row.count); // [2, 0, 2, 0, 1]
923
+ report.references.map(row => round(row.resultant)); // [0.1453, 0.1453, 0.1453, 0.1453, 0.1453]
924
+ ```
925
+
926
+ La puntuación es la otra cara de la misma moneda. Un `PhaseField` es una serie de cosenos acotada
927
+ (capítulo 10.2), y `scoreAt`/`scoresAt` lo muestrean en la fase relativa, así que el mismo anillo
928
+ puede puntuarse contra una alineación, contra un patrón armónico o contra cualquier campo sin
929
+ cambiar el anillo.
930
+
931
+ ```typescript
932
+ import { SynchronousRing, useField } from "semantic-typescript";
933
+
934
+ const round = (value: number): number => Number(value.toFixed(4));
935
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
936
+ const cosine = useField([1]);
937
+
938
+ round(ring.scoreAt(0n, 0, cosine).get()); // 1
939
+ round(ring.scoreAt(1n, 0, cosine).get()); // 0.5
940
+ ring.scoresAt(0, cosine).map(score => round(score)); // [1, 0.5, -0.5, -1, -0.5, 0.5]
941
+ ```
942
+
943
+ ### 13.6 Combinar anillos y el espectro angular
944
+
945
+ `combineRings(left, right)` une dos anillos por su mínimo común múltiplo, no por su producto. El
946
+ instante `i` del anillo conjunto lee `i mod m` del anillo izquierdo e `i mod n` del derecho, así que
947
+ el anillo conjunto se cierra tras `lcm(m, n)` instantes. Los pares que nunca pueden coexistir —los
948
+ que difieren en posición módulo `gcd(m, n)`— simplemente no existen; por eso el resultado es un
949
+ pullback y no un producto, y `aligns(left, right)` informa si un par dado es representable.
950
+
951
+ `useAngularSpectrum(ring, instants, plan)` construye el plano `(ω, n)`: cada instante aporta una
952
+ trama de fasores, la trama `k` del modo `j` es la `j`-ésima componente simétrica de esa trama, y
953
+ `cells[frame][order]` es el coeficiente. Tomar una columna da la serie temporal de un único modo
954
+ espacial, y `spectrum(order)` es su espectro temporal.
955
+
956
+ ```typescript
957
+ import { Sample, SynchronousRing, combineRings, useAngularSpectrum } from "semantic-typescript";
958
+
959
+ const left = new SynchronousRing(["a", "b", "c", "d"]);
960
+ const right = new SynchronousRing(["x", "y", "z", "w", "v", "u"]);
961
+ const joined = combineRings(left, right);
962
+
963
+ joined.period; // 12n
964
+ joined.divisor; // 2n
965
+ joined.ring.size(); // 12n
966
+ joined.aligns(0n, 0n); // true
967
+ joined.aligns(1n, 0n); // false
968
+ joined.ring.get(0n).join("-"); // a-x
969
+
970
+ const angular = useAngularSpectrum(left, [0, 0.25, 0.5, 0.75], new Sample(4, 1));
971
+ angular.order; // 4
972
+ angular.frameCount; // 4
973
+ angular.cells[0][1].magnitude(); // 4
974
+ angular.spectrum(1).get().size(); // 4
975
+ ```
976
+
977
+ ### 13.7 Trampas
978
+
979
+ **La exactitud es exacta.** `phaseAt(i)` y `phaseAt(i + n)` son el mismo doble, no dos valores
980
+ dentro de una tolerancia. Un anillo construido acumulando `2π / n` por elemento fallaría esto; este
981
+ reduce primero el índice en `bigint`.
982
+
983
+ **`frequency` no es un paso de fase.** Es la velocidad de rotación ambiente, vale cero por defecto,
984
+ y rotar todos los fasores el mismo ángulo no puede romper el patrón intrínseco. Si quiere que las
985
+ posiciones avancen unas respecto de otras, use `rate`, no `frequency`.
986
+
987
+ **`purity()` debe mirar los dos bins fundamentales.** Un anillo que gira al revés sigue siendo las
988
+ raíces `n`-ésimas de la unidad; solo `c₁` lo declararía impuro.
989
+
990
+ **`n < 2` es un caso límite definido.** `purity()` devuelve `0` e `isBalanced()` devuelve `false`;
991
+ un anillo vacío devuelve opcionales vacíos en los métodos de muestreo.
992
+
993
+ **Un span que comparte un factor con `n` colapsa la órbita.** Seis elementos con `span: 2` visitan
994
+ solo tres fases distintas, y el anillo deja de estar equilibrado.
995
+
996
+ **`resultant` es invariante a la rotación, no al tiempo.** La rotación ambiente lo deja igual; las
997
+ `rate` por elemento lo cambian a medida que el anillo se desfasa.
998
+
999
+ **El anillo es un conjunto, y `analyse` empareja por identidad.** Los duplicados se descartan al
1000
+ construir, y un objeto estructuralmente igual a un elemento del anillo pero que no es la misma
1001
+ referencia se cuenta como perdido.
1002
+
1003
+ **`toSpectrum()` es complejo.** Sus bins no son conjugados-simétricos, así que los ayudantes
1004
+ unilaterales de `Spectrum` no aplican. Lea los bins.
1005
+
1006
+ **`windingAt` trunca hacia cero, y `errorIntegralAt` integra la fase.** Media vuelta de deslizamiento
1007
+ reporta `0n`; la integral de error está en vueltas · segundos, no es una tasa.
1008
+
1009
+ ```typescript
1010
+ import { SynchronousRing, useHertz } from "semantic-typescript";
1011
+
1012
+ const collapsed = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 2 });
1013
+ collapsed.phaseAt(1n).get().toDegreeValue(); // 120
1014
+ collapsed.phaseAt(3n).get().toDegreeValue(); // 0
1015
+ Number(collapsed.purity().toFixed(4)); // 0
1016
+
1017
+ const falling = new SynchronousRing(["a", "b", "c", "d"], {
1018
+ frequency: useHertz(2),
1019
+ rate: () => useHertz(0.5),
1020
+ });
1021
+ falling.jetAt(0n, 2).get().slip; // -1.5
1022
+ falling.windingAt(0n, 1).get(); // -1n
1023
+ falling.windingAt(0n, 0.5).get(); // 0n
1024
+
1025
+ const objects = new SynchronousRing([{ key: 1 }, { key: 2 }]);
1026
+ objects.analyse([{ key: 1 }]).missed; // 1 — identity, not structure
1027
+ ```
1028
+
1029
+ ---
1030
+
1031
+ <a id="14-signal-analysis"></a>
1032
+ ## 14. Análisis de señales: ventanas, espectros y tiempo
1033
+
1034
+ El capítulo 10 trataba un anillo como un ciclo de fases. Este capítulo es su contraparte temporal:
1035
+ un registro real que lleva, además de sus muestras, los dos contextos que necesita toda fórmula
1036
+ espectral — el plan de muestreo que convierte un índice de bin en hercios, y la ventana que fija a
1037
+ la vez la ganancia coherente y el ancho de banda de ruido.
1038
+
1039
+ El objeto valor es `SynchronousSignalCollectable`, y su gemelo en cola es
1040
+ `AsynchronousSignalCollectable`. Ambos exponen la misma superficie `SignalCollectable`:
1041
+ veintitrés métodos, cada uno delegando en exactamente una fábrica `useSynchronous<Method>` de
1042
+ `synchronous/collectors`. La separación es deliberada. La clase es una fachada documentada, la
1043
+ fábrica posee la acumulación, y el constructor calcula una sola vez, a esta longitud,
1044
+ `sum(w) / count` y `count * sum(w^2) / sum(w)^2`. Quien tenga que recordar «dividir por la
1045
+ ganancia coherente» acabará olvidándolo, y los 6,02 decibelios resultantes parecen una medición
1046
+ plausible.
1047
+
1048
+ ### 14.1 Una señal lleva su propia calibración
1049
+
1050
+ Una señal se construye desde un iterable y una tasa, desde un `Frame` existente, o desde el
1051
+ constructor con un `Sample` explícito. La ventana por defecto es la ventana de Hann periódica;
1052
+ toda análisis que no nombre una la usa, y todo análisis acepta una sustitución. Los accesores de
1053
+ metadatos leen en tiempo constante un estado fijado en la construcción, así que una señal puede
1054
+ pasarse de mano en mano sin que el contexto derive.
1055
+
1056
+ | Llamada | Devuelve | Significado |
1057
+ | :--- | :--- | :--- |
1058
+ | `SynchronousSignalCollectable.useRate(values, rate, window?)` | señal | duración `count / rate` |
1059
+ | `SynchronousSignalCollectable.useFrame(frame, window?)` | señal | conserva el plan del frame |
1060
+ | `new SynchronousSignalCollectable(plan, values, window?)` | señal | el número de tramas debe igualar el de muestras |
1061
+ | `getSample()` / `getValues()` / `getWindow()` | plan, muestras, ventana | el contexto congelado |
1062
+ | `getRate()` | `number` | muestras por segundo |
1063
+ | `getNyquist()` / `getResolution()` | `Frequency` | `rate / 2` y `rate / count` |
1064
+ | `getCoherentGain()` / `getNoiseBandwidth()` | `number` | recalculados a esta longitud exacta |
1065
+ | `size()` / `at(index)` / `toFrame()` / `equals(other)` / `toString()` | — | semántica de valor |
1066
+
1067
+ ```typescript
1068
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1069
+
1070
+ const signal = SynchronousSignalCollectable.useRate(
1071
+ Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 64)),
1072
+ 64,
1073
+ );
1074
+
1075
+ signal.size(); // 64n
1076
+ signal.getRate(); // 64
1077
+ signal.getNyquist().getHertz(); // 32
1078
+ signal.getResolution().getHertz(); // 1
1079
+ signal.getCoherentGain(); // 0.5
1080
+ signal.getNoiseBandwidth(); // 1.5
1081
+ ```
1082
+
1083
+ `getResolution()` es el espaciado entre bins, no el poder de resolución: vale `rate / count`
1084
+ porque la transformada tiene `count` bins, y rellenar con ceros cambia el espaciado sin cambiar
1085
+ qué puede separar la ventana (sección 14.8).
1086
+
1087
+ ### 14.2 Los veintitrés métodos
1088
+
1089
+ Los métodos forman seis familias. Siete son lecturas espectrales, dos son transformadas
1090
+ tiempo–frecuencia, cuatro son magnitudes analíticas, tres son estadísticos de secuencia, cinco son
1091
+ descriptores de forma, y dos devuelven otra señal.
1092
+
1093
+ | Método | Devuelve | Convención |
1094
+ | :--- | :--- | :--- |
1095
+ | `spectrum(size?, window?)` | `Spectrum` | crudo y sin normalizar, rejilla compleja completa |
1096
+ | `oneSided(size?, window?)` | `Bin[]` | mitad conjugada plegada, DC y Nyquist sin duplicar |
1097
+ | `amplitude(window?)` | `number[]` | `2·abs(X) / (count · CG)`, DC y Nyquist una sola vez |
1098
+ | `power(window?)` | `number[]` | `A² / 2` por bin interior, `A²` en DC y Nyquist |
1099
+ | `decibel(window?)` | `number[]` | `20·log10(amplitud)` |
1100
+ | `powerDecibel(window?)` | `number[]` | `10·log10(potencia)` |
1101
+ | `powerSpectralDensity(frameSize?, hopSize?, window?)` | `SpectralDensity` | media de Welch, `abs(X)² / (rate · Σw²)` |
1102
+ | `shortTimeFourierTransform(frameSize, hopSize, window?)` | `Spectrum[]` | bordes extendidos, tiempos en el centro de trama |
1103
+ | `spectrogram(frameSize, hopSize, window?)` | `Spectrogram` | las mismas columnas con eje de tiempo |
1104
+ | `analyticSignal()` | `Complex[]` | par de Hilbert `x + i·H(x)` |
1105
+ | `instantaneousPhase()` | `Angle[]` | plegada en `[0, 2π)` |
1106
+ | `instantaneousFrequency(rate?, gate?)` | `number[]` | diferencia de fase desenvuelta, con puerta de envolvente |
1107
+ | `envelope()` | `number[]` | `abs(z[n])`, circular en los bordes |
1108
+ | `zeroCrossingRate(perSecond?)` | `number` | cruces por par de muestras adyacentes |
1109
+ | `spectralFlux(frameSize?, hopSize?, window?)` | `number[]` | distancia de norma unitaria entre tramas |
1110
+ | `autocorrelation(maximumLag?, normalization?)` | `number[]` | coeficiente por defecto, `ρ[0] = 1` |
1111
+ | `spectralCentroid(window?)` | `Optional<Frequency>` | frecuencia media ponderada por potencia |
1112
+ | `spectralBandwidth(window?)` | `Optional<Frequency>` | desviación típica ponderada por potencia |
1113
+ | `spectralRolloff(ratio?, window?)` | `Optional<Frequency>` | primer bin que alcanza `ratio` de la potencia |
1114
+ | `spectralFlatness(window?)` | `number` | media geométrica sobre aritmética de la potencia |
1115
+ | `signalToNoiseRatio(frameSize?, hopSize?, window?, floorQuantile?)` | `Optional<number>` | suelo mediano, `10·log10` |
1116
+ | `detrended(mode?)` | señal | `"none"`, `"constant"` (defecto), `"linear"` |
1117
+ | `windowed(window?)` | señal | aplica una ventana y la adopta |
1118
+
1119
+ Los argumentos opcionales no son decoración. `frameSize` vale `min(256, size)` por defecto, el
1120
+ salto media trama redondeada hacia abajo, `instantaneousFrequency` pone la puerta en `1E-3` de la
1121
+ envolvente de pico, y `spectralRolloff` usa `0.85`. Cada valor por defecto es el que elige una
1122
+ implementación de referencia cuando el parámetro se nombra pero no se pasa.
1123
+
1124
+ ### 14.3 Amplitud, potencia y densidad
1125
+
1126
+ Hacen falta tres normalizaciones porque se hacen tres preguntas distintas.
1127
+
1128
+ `amplitude()` responde «qué tamaño tiene el seno en este bin»: `2·abs(X[k]) / (count · CG)` para
1129
+ un bin interior y `abs(X[k]) / (count · CG)` en DC y Nyquist. El factor dos pliega la mitad
1130
+ conjugada; la ganancia coherente quita la ventana. Omitir cualquiera de los dos hace que un seno de
1131
+ amplitud unidad se lea `0.5`, es decir, menos 6,02 decibelios.
1132
+
1133
+ `power()` responde «qué cuadrado medio tendría un seno en este bin»: `A² / 2` para un bin interior
1134
+ y `A²` en DC y Nyquist, porque estos dos no oscilan. Es la convención de pico, y deliberadamente no
1135
+ cumple Parseval: la ventana reparte cada línea por su lóbulo principal, así que la suma de todos los
1136
+ bins supera el cuadrado medio en el ancho de banda de ruido equivalente — `1.5` para Hann.
1137
+
1138
+ `powerSpectralDensity()` responde «cuánta potencia por hercio»: `abs(X)² / (rate · Σw²)`,
1139
+ promediada sobre segmentos solapados. Solo la suma de los pesos al cuadrado mantiene insesgado un
1140
+ suelo de ruido blanco; dividir por `count` lee `0.375` para ruido unitario, dividir por
1141
+ `count · CG²` lee `1.5`.
1142
+
1143
+ ```typescript
1144
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1145
+
1146
+ const round = (value: number): number => Number(value.toFixed(4));
1147
+ const tone = SynchronousSignalCollectable.useRate(
1148
+ Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1149
+ 256,
1150
+ );
1151
+
1152
+ round(tone.amplitude()[32]); // 1
1153
+ round(tone.power()[32]); // 0.5
1154
+ round(tone.power().reduce((sum, power) => sum + power, 0)); // 0.75
1155
+ round(tone.powerSpectralDensity(64n, 32n).totalPower()); // 0.5
1156
+ tone.powerSpectralDensity(64n, 32n).getSegments(); // 7n
1157
+ ```
1158
+
1159
+ El par de totales es la comprobación que conviene recordar: la suma en convención de pico vale
1160
+ `0.75` mientras que la densidad integra `0.5`, y la razón es exactamente el ancho de banda de
1161
+ ruido. La densidad es además la única de las tres que sobrevive a un cambio de longitud de trama.
1162
+
1163
+ ### 14.4 Niveles: las dos convenciones de decibelio
1164
+
1165
+ Una amplitud y una potencia no comparten factor, así que tampoco comparten logaritmo.
1166
+ `amplitudeDecibel` aplica `20·log10` y es correcto para una tensión, un desplazamiento o una
1167
+ presión sonora; `powerDecibel` aplica `10·log10` y es correcto para una potencia, una energía, un
1168
+ cuadrado medio o una densidad. Los nombres de método de la señal siguen la misma división.
1169
+
1170
+ ```typescript
1171
+ import { amplitudeDecibel, powerDecibel, SynchronousSignalCollectable } from "semantic-typescript";
1172
+
1173
+ const round = (value: number): number => Number(value.toFixed(4));
1174
+ const tone = SynchronousSignalCollectable.useRate(
1175
+ Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1176
+ 256,
1177
+ );
1178
+
1179
+ amplitudeDecibel(0.25); // -12.0412
1180
+ powerDecibel(0.25); // -6.0206
1181
+ round(tone.decibel()[32]); // 0
1182
+ round(tone.powerDecibel()[32]); // -3.0103
1183
+ ```
1184
+
1185
+ Las dos lecturas del mismo tono son correctas: su amplitud es uno (cero decibelios) y su cuadrado
1186
+ medio es un medio (menos 3,01 decibelios). Pasar una potencia por la convención de amplitud
1187
+ duplicaría el nivel en decibelios, convirtiendo el menos 6,02 correcto en menos 12,04.
1188
+
1189
+ ### 14.5 El plegado unilateral
1190
+
1191
+ Una señal real cumple `X[count − k] = conj(X[k])`, así que la mitad de frecuencias negativas no
1192
+ lleva información independiente. El plegado duplica cada bin interior y deja los dos bins
1193
+ autoconjugados — DC y, para longitud par, Nyquist — exactamente como los produjo la transformada.
1194
+ Esos dos son su propio espejo; duplicarlos inventa energía y reporta exactamente 6,02 decibelios de
1195
+ más.
1196
+
1197
+ ```typescript
1198
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1199
+
1200
+ const round = (value: number): number => Number(value.toFixed(4));
1201
+ const edge = SynchronousSignalCollectable.useRate(
1202
+ Array.from({ length: 16 }, (_unused, index) => 2 + Math.cos(Math.PI * index)),
1203
+ 16,
1204
+ );
1205
+ const bins = edge.oneSided();
1206
+
1207
+ bins.length; // 9
1208
+ round(bins[0].magnitude()); // 16
1209
+ round(bins[8].magnitude()); // 8
1210
+ round(edge.amplitude()[0]); // 2
1211
+ round(edge.amplitude()[8]); // 1
1212
+ ```
1213
+
1214
+ Las magnitudes crudas son `16` y `8` porque los pesos de la ventana suman ocho; dividir por
1215
+ `count · CG = 8` las convierte en las amplitudes calibradas `2` y `1`. Duplicar los bins de borde
1216
+ leería `4` y `2`, y el error nunca lanzaría una excepción.
1217
+
1218
+ ### 14.6 Vistas tiempo–frecuencia
1219
+
1220
+ `shortTimeFourierTransform` y `spectrogram` enmarcan el registro como la transformada de corto
1221
+ tiempo de referencia: media trama de ceros en cada extremo y luego una cola rellenada con ceros
1222
+ hasta completar un número entero de saltos. La primera trama se centra en la primera muestra, la
1223
+ última alcanza la última muestra, y la trama `m` se centra en `m · hop / rate` segundos. Conservar
1224
+ solo las tramas completas descartaría hasta `frameSize − 1` muestras — para 1000 muestras, trama
1225
+ 256 y salto 128 son 104 muestras, alrededor de una décima parte del registro.
1226
+
1227
+ ```typescript
1228
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1229
+
1230
+ const tone = SynchronousSignalCollectable.useRate(
1231
+ Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 4 * index / 64)),
1232
+ 64,
1233
+ );
1234
+ const spectrogram = tone.spectrogram(16n, 8n);
1235
+
1236
+ spectrogram.size(); // 9
1237
+ spectrogram.getFrameSize(); // 16
1238
+ spectrogram.getBoundary(); // "zeros"
1239
+ spectrogram.getTime(0); // 0
1240
+ spectrogram.getTime(6); // 0.75
1241
+ tone.shortTimeFourierTransform(16n, 8n).length; // 9
1242
+ tone.spectralFlux(16n, 8n).length; // 8
1243
+ ```
1244
+
1245
+ Cada columna es un `Spectrum` de `frameSize` bins, exactamente lo que produciría `spectrum()` para
1246
+ la misma trama, y `getTime()` informa centros de trama, no inicios.
1247
+
1248
+ ### 14.7 Ventanas, ganancia coherente y COLA
1249
+
1250
+ Cada forma tiene dos variantes. La simétrica divide por `count − 1`; la periódica (DFT par) divide
1251
+ por `count`, una muestra menos que un periodo completo, de modo que la ventana es continua bajo la
1252
+ extensión periódica que supone la DFT. El análisis y la suma con solapamiento necesitan la forma
1253
+ periódica; el diseño de filtros conserva la simétrica.
1254
+
1255
+ La diferencia es medible y nunca lanza un error. Al cincuenta por ciento de solapamiento, la
1256
+ ventana de Hann periódica suma uno en toda posición; la forma simétrica ondula, `0.84` decibelios
1257
+ con dieciséis muestras, `0.21` con sesenta y cuatro y todavía `0.013` con mil veinticuatro. La
1258
+ clase expone la ganancia coherente y el ancho de banda de ruido equivalente de la ventana que
1259
+ guarda, así que la corrección nunca está codificada a mano: un Hann simétrico lee `0.4375` con
1260
+ ocho muestras, no `0.5`.
1261
+
1262
+ ```typescript
1263
+ import {
1264
+ coherentGain, equivalentNoiseBandwidth, useFlatTopWindow,
1265
+ useHannPeriodicWindow, useHannWindow,
1266
+ } from "semantic-typescript";
1267
+
1268
+ const round = (value: number): number => Number(value.toFixed(4));
1269
+
1270
+ round(useHannWindow(1, 4)); // 0.75
1271
+ round(useHannPeriodicWindow(1, 4)); // 0.5
1272
+ coherentGain(useHannPeriodicWindow, 16); // 0.5
1273
+ equivalentNoiseBandwidth(useHannPeriodicWindow, 16); // 1.5
1274
+ useFlatTopWindow(0, 1024); // -0.000421
1275
+ Array.from({ length: 1024 }, (_unused, i) => useFlatTopWindow(i, 1024)).filter(w => w < 0).length; // 578
1276
+ ```
1277
+
1278
+ La fila del flat-top no es un error de la tabla. Una ventana flat-top es parcialmente negativa por
1279
+ definición — aquí 578 de 1024 pesos — y recortar esos pesos a cero perturbaría el lóbulo principal
1280
+ y destruiría la baja pérdida de scalloping para la que existe la ventana. La biblioteca devuelve la
1281
+ serie sin cambios.
1282
+
1283
+ ### 14.8 Trampas
1284
+
1285
+ **Rellenar con ceros interpola, nunca resuelve.** Una longitud de transformada mayor que el número
1286
+ de muestras muestrea la misma DTFT con más densidad: la *posición* del pico se afina y el espaciado
1287
+ entre bins pasa a `rate / size`, pero la capacidad de separar dos componentes sigue gobernada por
1288
+ la longitud y la forma de la ventana. Un tono exactamente entre dos bins lee `82.1093` y `80.8598`
1289
+ en la rejilla sin relleno, y `128` en su propia frecuencia en la rejilla rellenada.
1290
+
1291
+ ```typescript
1292
+ import { SynchronousSignalCollectable, useRectangularWindow } from "semantic-typescript";
1293
+
1294
+ const round = (value: number): number => Number(value.toFixed(4));
1295
+ const tone = SynchronousSignalCollectable.useRate(
1296
+ Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 100.5 * index / 256)),
1297
+ 256,
1298
+ );
1299
+ const flat = tone.spectrum(256n, useRectangularWindow);
1300
+ const padded = tone.spectrum(1024n, useRectangularWindow);
1301
+
1302
+ flat.getResolution().getHertz(); // 1
1303
+ padded.getResolution().getHertz(); // 0.25
1304
+ round(flat.magnitudeAt(100).get()); // 82.1093
1305
+ round(flat.magnitudeAt(101).get()); // 80.8598
1306
+ round(padded.magnitudeAt(402).get()); // 128
1307
+ padded.frequencyAt(402).get().getHertz(); // 100.5
1308
+ ```
1309
+
1310
+ Dos tonos iguales separados `0.4` bins siguen fusionados con cualquier relleno: con un registro de
1311
+ 256 puntos y una ventana de Hann, rellenar hasta 8192 produce un solo máximo local, no dos.
1312
+
1313
+ **La transformada de Hilbert es circular.** `analyticSignal`, `envelope` e `instantaneousPhase`
1314
+ tratan la primera y la última muestra como vecinas, así que un registro cuya periodo no divide su
1315
+ longitud queda contaminado en ambos extremos. El centro del registro es fiable; los extremos no.
1316
+
1317
+ ```typescript
1318
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1319
+
1320
+ const round = (value: number): number => Number(value.toFixed(4));
1321
+ const edge = SynchronousSignalCollectable.useRate(
1322
+ Array.from({ length: 512 }, (_unused, index) => Math.cos(2 * Math.PI * 8.3 * index / 512)),
1323
+ 512,
1324
+ );
1325
+ const envelope = edge.envelope();
1326
+
1327
+ round(envelope.slice(8, -8).reduce((sum, value) => sum + value, 0) / 496); // 1.0025
1328
+ round(Math.max(...envelope.slice(0, 8))); // 1.2592
1329
+ round(Math.min(...envelope.slice(-8))); // 0.1314
1330
+ ```
1331
+
1332
+ **Una envolvente que se anula no define una fase.** Donde la envolvente se desploma, la diferencia
1333
+ de dos ángulos de ruido de redondeo puede llegar a media vuelta, lo que se lee como la mitad de la
1334
+ tasa de muestreo. La puerta es lo que mantiene significativo el número, y el valor por defecto es
1335
+ `1E-3` de la envolvente de pico.
1336
+
1337
+ ```typescript
1338
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1339
+
1340
+ const round = (value: number): number => Number(value.toFixed(4));
1341
+ const beat = SynchronousSignalCollectable.useRate(
1342
+ Array.from({ length: 4096 }, (_unused, index) =>
1343
+ Math.cos(2 * Math.PI * 100 * index / 2048) + Math.cos(2 * Math.PI * 102 * index / 2048)),
1344
+ 2048,
1345
+ );
1346
+
1347
+ round(Math.max(...beat.instantaneousFrequency(2048, 0).map(Math.abs))); // 920.218
1348
+ round(Math.max(...beat.instantaneousFrequency().map(Math.abs))); // 101
1349
+ ```
1350
+
1351
+ **Una correlación sin normalizar no es una similitud.** La suma cruda `r[k]` crece con la longitud
1352
+ del registro, así que dos longitudes no son comparables. La regla por defecto `"coefficient"`
1353
+ divide por `r[0]` y mantiene cada valor en `[-1, 1]`; `"unbiased"` divide por `count − k` y paga
1354
+ con una varianza que crece con el desfase.
1355
+
1356
+ ```typescript
1357
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1358
+
1359
+ const round = (value: number): number => Number(value.toFixed(4));
1360
+ const periodic = SynchronousSignalCollectable.useRate(
1361
+ Array.from({ length: 1024 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 1024)),
1362
+ 1024,
1363
+ );
1364
+ const rho = periodic.autocorrelation(200n);
1365
+
1366
+ round(rho[0]); // 1
1367
+ round(rho[128]); // 0.875
1368
+ round(rho[64]); // -0.9375
1369
+ round(periodic.autocorrelation(200n, "none")[0]); // 512
1370
+ ```
1371
+
1372
+ Una lectura más merece advertencia: `spectralFlux` normaliza cada trama antes de compararla, así
1373
+ que un cambio puro de nivel produce flujo cero. Su primer y último valor son artefactos de borde de
1374
+ la frontera de ceros, no ataques.
1375
+
1376
+ ---
1377
+
1378
+ <a id="15-license"></a>
1379
+ ## 15. Licencia
1380
+
1381
+ MIT © Eloy Kim