semantic-typescript 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +587 -574
- package/dist/asynchronous/collectable.js +10 -4
- package/dist/optional.js +0 -1
- package/dist/readonly-map.d.ts +1 -0
- package/dist/readonly-map.js +26 -0
- package/dist/synchronous/collectable.js +2 -1
- package/package.json +1 -1
- package/readme.cn.md +1271 -1271
- package/readme.de.md +1397 -1397
- package/readme.es.md +1381 -1381
- package/readme.fr.md +1390 -1390
- package/readme.jp.md +1300 -1300
- package/readme.kr.md +1297 -1297
- package/readme.md +1442 -1442
- package/readme.ru.md +1378 -1378
- package/readme.tw.md +1271 -1271
package/readme.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
|