semantic-typescript 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.ru.md CHANGED
@@ -1,1378 +1,1378 @@
1
- # **Semantic-TypeScript**
2
-
3
- **Поток, проиндексированный.** Ваши данные под точным контролем.
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.es.md) · Русский
6
-
7
- Библиотека типобезопасной потоковой обработки, объединяющая **генераторы JavaScript**, **Java Stream** и
8
- **адресацию данных по индексу**. Каждый элемент несёт `bigint`-индекс, каждый конвейер ленив, и
9
- каждая терминальная операция — явный выбор.
10
-
11
- ```bash
12
- npm install semantic-typescript
13
- ```
14
-
15
- - Нулевые зависимости времени выполнения, чистый ESM, поставляется со своими типами.
16
- - 684 регрессионных тестов: `npm test`.
17
- - Полная история изменений: [CHANGELOG.md](./CHANGELOG.md).
18
-
19
- ---
20
-
21
- ## Оглавление
22
-
23
- 1. [Философия проектирования](#1-design-philosophy)
24
- 2. [Архитектура](#2-architecture)
25
- 3. [Быстрый старт](#3-quick-start)
26
- 4. [Фабричные функции](#4-factory-functions)
27
- 5. [Операторы преобразования](#5-transformation-operators)
28
- 6. [Коллектораблы и терминальные операции](#6-collectables-and-terminal-operations)
29
- 7. [Статистика и окна](#7-statistics-and-windows)
30
- 8. [Модель индексов](#8-the-index-model)
31
- 9. [Асинхронная модель](#9-the-asynchronous-model)
32
- 10. [Периодический анализ: кольца, поля и оценки](#10-periodic-analysis-rings-fields-and-scores)
33
- 11. [Вспомогательные функции](#11-utility-helpers)
34
- 12. [Производительность и подводные камни](#12-performance-and-pitfalls)
35
- 13. [Фазовые кольца: точный цикл](#13-phase-rings)
36
- 14. [Анализ сигналов: окна, спектры и время](#14-signal-analysis)
37
- 15. [Лицензия](#15-license)
38
-
39
- ---
40
-
41
- <a id="1-design-philosophy"></a>
42
- ## 1. Философия проектирования
43
-
44
- Три идеи, один конвейер:
45
-
46
- **Индекс — это данные.** Каждый элемент адресуется через `bigint`. Обращение, ротация, перемешивание и
47
- разбиение — это индексная арифметика, а не операции с буфером. Ничего не материализуется, пока этого не
48
- потребует коллекторабл.
49
-
50
- **Лень — значение по умолчанию.** Фабрика оборачивает генератор; оператор оборачивает другой генератор.
51
- Ни один элемент не запрашивается, пока не выполнится терминальная операция, поэтому бесконечный источник
52
- вполне допустим — лишь бы что-то ниже по потоку его остановило.
53
-
54
- **Терминалы явны.** `map` возвращает `Semantic`, а не массив. Значения появляются только тогда, когда вы
55
- называете коллекторабл — `toUnordered()`, `toOrdered()`, `toNumericStatistics()`, `toWindow()` — и каждый
56
- из них сообщает, чего он стоит и какой порядок порождает.
57
-
58
- Та же дисциплина действует и в математической половине библиотеки. `Complex`, `Angle` или
59
- `Frequency` — это неизменяемое значение, инварианты которого проверяются при построении; `Optional`
60
- заменяет `null`; а фазовое кольцо точно сообщает, сколько элементов оно содержит, какой у него период и
61
- насколько оно сбалансировано.
62
-
63
- ---
64
-
65
- <a id="2-architecture"></a>
66
- ## 2. Архитектура
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
- | Слой | Модуль | Ответственность |
76
- | :--- | :--- | :--- |
77
- | Источники | `synchronous/collectors`, `asynchronous/collectors` | Оборачивают итерируемое, генератор, промис, блоб, цель событий |
78
- | Конвейеры | `synchronous/semantic`, `asynchronous/semantic` | Ленивые операторы и переписывание индексов |
79
- | Коллектораблы | `synchronous/semantic`, `asynchronous/semantic` | Упорядоченные, неупорядоченные, статистические, оконные и пользовательские терминалы |
80
- | Коллекторы | `synchronous/collector`, `asynchronous/collector` | Примитивы свёртывания, из которых построен каждый терминал |
81
- | Математика | `mathematics/*` | Углы, комплексы, частоты, спектры, окна, оценки, эпициклы, фазовые кольца |
82
- | Структуры | `structure/*` | Списки, множества, отображения, деревья, леса, кольца, поля |
83
- | Ошибки | `structure/error` | Типизированная иерархия `SemanticError` и её брендовые стражи |
84
-
85
- Синхронное и асинхронное семейства используют один словарь: те же имена операторов, та же семантика
86
- индексов, те же коллектораблы. Асинхронные варианты просто возвращают промисы и ставят записи в очередь,
87
- так что чтение, выданное после записи, всегда её наблюдает.
88
-
89
- ### 2.1 Что этот выпуск перенёс и что сломал
90
-
91
- Три модуля сменили расположение или форму, и ряд значений по умолчанию изменил смысл. Каждое старое поведение по-прежнему достижимо через явную опцию; ничего не удалено без замены.
92
-
93
- | Изменение | Раньше | Теперь |
94
- | :--- | :--- | :--- |
95
- | Леса | `structure/tree/rootedForest`, `structure/tree/unionFindForest` | `structure/forest/rootedForest`, `structure/forest/unionFindForest`; `tree/index.ts` больше их не реэкспортирует |
96
- | Ошибки | разрозненные нативные ошибки | иерархия из пятнадцати классов в `structure/error.ts`, по одному брендовому стражу на класс и псевдоним `SemanticRangeError` |
97
- | Опции оценок | фиксированные определения | `mathematics/quantile.ts` экспортирует `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator` и `VarianceOptions` |
98
- | `quantile` | Хайндман–Фан тип 1 | тип 7 — R `quantile`, NumPy `linear`, Excel `PERCENTILE.INC` |
99
- | `skewness` / `kurtosis` | `g1` / `g2` | скорректированные на смещение `G1` / `G2` |
100
- | `variance` / `standardDeviation` | `ddof = 0` | `ddof = 1`, обёрнутые в `Optional` |
101
- | `decibel` | одна функция `20·log10` | удалена; `amplitudeDecibel` (20·log10) и `powerDecibel` (10·log10) |
102
- | `Spectrum.flatness()` | амплитуды по обеим половинам | мощности по односторонней половине |
103
- | `Spectrum.bandEnergy()` | сырая двусторонняя сумма | по умолчанию односторонняя, со свёрткой сопряжённой половины |
104
- | `shortTimeFourierTransform` / `spectrogram` | симметричное окно Ханна, только полные кадры | периодическое окно Ханна, `boundary: "zeros"`, `padded: true` |
105
-
106
- Главы 6, 7 и 10 описывают замены; измеренные последствия — в [CHANGELOG.md](./CHANGELOG.md).
107
-
108
- ---
109
-
110
- <a id="3-quick-start"></a>
111
- ## 3. Быстрый старт
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. Фабричные функции
160
-
161
- Построение всегда O(1) — фабрика лишь оборачивает генератор. Таблица указывает стоимость
162
- *потребления* `n` элементов, а также дополнительную память, которая нужна самому источнику.
163
-
164
- | Фабрика | Сигнатура (результат) | Время (потребление n) | Память (источник) | Примечания |
165
- | :--- | :--- | :--- | :--- | :--- |
166
- | `useOf` | `useOf<E>(...elements) → SynchronousSemantic<E>` | O(n) | O(n) | Индексы 0..n−1; spread-аргумент уже материализован |
167
- | `useFrom` | `useFrom<E>(Iterable<E>) → SynchronousSemantic<E>`<br>`useFrom<E>(AsyncIterable<E>) → AsynchronousSemantic<E>` | O(n) | O(1) | Оборачивает любое итерируемое; итерируется один раз при сборе |
168
- | `useRange` | `useRange<N>(start, end, step?) → SynchronousSemantic<N>` | O(n) | O(1) | `end` исключается; `step` должен быть ненулевым; `number` и `bigint` оба работают |
169
- | `useFill` | `useFill<E>(element \| supplier, count) → SynchronousSemantic<E>` | O(n) | O(1) | Поставщик вызывается один раз на элемент |
170
- | `useEmpty` | `useEmpty<E>() → SynchronousSemantic<E>` | O(1) | O(1) | Нейтральный элемент для `concat` |
171
- | `useText` | `useText(text, delimiter?)` | O(n) | O(n) для текста | Итерирует **кодовые точки**, поэтому эмодзи и иероглифы CJK остаются единичными элементами |
172
- | `useAttribute` | `useAttribute<T>(object)` | O(v + e) | O(d + v) | Рекурсивно обходит вложенные объекты с защитой от циклов |
173
- | `useGenerate` | `useGenerate<E>(supplier, interrupt)` | O(n) × стоимость поставщика | O(1) | **Должен** иметь останавливающий `interrupt` или нижестоящий `limit` |
174
- | `useIterate` | `useIterate<E>(Iterable<E>)` / `(AsyncIterable<E>)` | O(n) за сборку | O(1) | Воспроизводит итератор при каждой сборке |
175
- | `useInterval` | `useInterval(period, delay?) → AsynchronousSemantic<number>` | n × period (реальное время) | O(1) | Порождает истекшее время |
176
- | `usePromise` | `usePromise<T>(promise) → AsynchronousSemantic<T>` | O(1) | O(1) | Один элемент с индексом `0n` |
177
- | `useBlob` | `useBlob(blob, chunk?) → AsynchronousSemantic<Uint8Array>` | O(размер blob) | O(chunk) на копию | Размер чанка по умолчанию — 64 КиБ |
178
- | `useSubscription` | `useSubscription(target, subscriber, event) → AsynchronousSemantic<Event>` | один элемент на событие | O(1) | `limit()` / `takeWhile()` определяют границу отписки |
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. Операторы преобразования
194
-
195
- Каждый оператор возвращает новую `Semantic`; ни один не трогает элемент, пока не запустится коллекторабл.
196
- Время — это дополнительная стоимость **на элемент**; память — то, что оператор удерживает, пока идёт конвейер.
197
-
198
- | Оператор | Назначение | Эффект для индекса | Время | Память |
199
- | :--- | :--- | :--- | :--- | :--- |
200
- | `map(mapper)` | 1:1 преобразование | передаётся | O(1) | O(1) |
201
- | `filter(predicate)` | оставляет подходящие элементы | передаётся | O(1) | O(1) |
202
- | `flat(mapper)` / `flatMap(mapper)` | 1:N развёртывание | переиндексация с 0 | O(1) на порождённый элемент | O(1) |
203
- | `distinct(keyExtractor?)` | удаляет повторяющиеся ключи, оставляет первый | передаётся | O(1) в среднем | O(n) |
204
- | `peek(consumer)` | побочный эффект, элемент проходит сквозь | передаётся | O(1) | O(1) |
205
- | `limit(count)` | берёт первые *n* | переиндексация с 0 | O(1) | O(1) |
206
- | `skip(count)` | пропускает первые *n* | передаётся | O(1) | O(1) |
207
- | `sub(start, end)` | позиционный срез `[start, end)` | передаётся | O(1) | O(1) |
208
- | `takeWhile(predicate)` | берёт, пока предикат истинен | передаётся | O(1) | O(1) |
209
- | `dropWhile(predicate)` | пропускает, пока предикат истинен | переиндексация с 0 | O(1) | O(1) |
210
- | `concat(other)` | добавляет семантику или итерируемое | продолжает индекс | O(1) | O(1) |
211
- | `redirect(redirector)` | вычисляет новый индекс как угодно | **ваш** | O(1) | O(1) |
212
- | `reverse()` | `redirect((_e, i) => -i - 1n)` | инвертирован и смещён | O(1) | O(1) |
213
- | `translate(offset \| mapper)` | сдвигает каждый индекс | `i + offset` | O(1) | O(1) |
214
- | `shuffle(mapper?)` | рандомизирует индекс | случайный | O(1) | O(1) |
215
- | `pipe(conversion)` | передаёт генератор пользовательской функции | на ваше усмотрение | ваш | ваш |
216
- | `semantic()` | поднимает коллекторабл обратно в семантику | неизменён | O(1) | O(1) |
217
-
218
- **`reverse`, `translate` и `shuffle` только переписывают индексы.** Ничего не буферизуется и не вращается, пока
219
- `toOrdered()` не нормализует индексы по модулю количества элементов:
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. Коллектораблы и терминальные операции
232
-
233
- ### 6.1 Коллектораблы
234
-
235
- Коллекторабл владеет и **порядком**, и **учётом индексов**. `n` — это количество элементов.
236
-
237
- | Коллекторабл | Структура | Время | Порядок | Используйте когда |
238
- | :--- | :--- | :--- | :--- | :--- |
239
- | `toUnordered()` | `Map<bigint, E>` с индексом в качестве ключа | O(n) | порядок вставки | максимальная пропускная способность; сырые индексы сохраняются нетронутыми |
240
- | `toOrdered()` | массив, отсортированный по индексу элемента | O(n log n) | по нормализованному индексу | обращение или ротация должны вступить в силу |
241
- | `toOrdered(comparator)` | сортировка по вашему компаратору | O(n log n) | по вашему компаратору | отсортированные представления |
242
- | `toNumericStatistics()` | буфер, отсортированный по значению | O(n log n) | по значению | статистика по `number` |
243
- | `toBigintStatistics()` | буфер, отсортированный по значению | O(n log n) | по значению | статистика по `bigint` |
244
- | `toWindow()` | буфер, отсортированный по значению | O(n log n) | по значению | для `slide` / `tumble` |
245
- | `toCollectable(mapper)` | то, что вернёт маппер | — | — | пользовательские коллектораблы |
246
-
247
- ### 6.2 Терминалы
248
-
249
- Каждый терминал живёт на **коллекторабле**, а не на `Semantic`. Именно это и означает «терминалы явные» на практике: конвейер ничего не производит, пока не назван коллекторабл, а `toCollectable()` без маппера — самый дешёвый способ его назвать.
250
-
251
- | Терминал | Возвращает | Время | Примечания |
252
- | :--- | :--- | :--- | :--- |
253
- | `toArray()` / `toSet()` | `Array<E>` / `Set<E>` | O(n) | Опустошает буфер; `toSet` дедуплицирует по идентичности |
254
- | `toMap(key, value?)` | `Map<K, V>` | O(n) в среднем | Побеждает последняя запись |
255
- | `count()` / `isEmpty()` | `bigint` / `boolean` | O(1) | Буфер уже знает |
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) | Пустой поток → пустой `Optional` |
259
- | `group(classifier)` / `groupBy(key, value)` | `Map<K, E[]>` | O(n) в среднем | — |
260
- | `partition(count)` | `Array<Array<E>>` | O(n) | Round-robin по индексу |
261
- | `partitionBy(classifier)` | `Array<Array<E>>` | O(n) | Номер бакета берётся из классификатора |
262
- | `anyMatch` / `allMatch` / `nonMatch` | `boolean` | O(n) в худшем, короткое замыкание | — |
263
- | `findAny` / `findFirst` / `findLast` | `Optional<E>` | O(1)–O(n) | Позиционные в материализованном порядке |
264
- | `findAt(k)` / `findNegativeAt(k)` | `Optional<E>` | O(k) / O(n−k) | Считает с начала / с конца |
265
- | `findMaximum(cmp?)` / `findMinimum(cmp?)` | `Optional<E>` | O(n) | По умолчанию — структурное сравнение |
266
- | `frequency()` | `Map<E, bigint>` | O(n) в среднем | Только коллектораблы статистики |
267
- | `write(stream, accumulator?)` | `WritableStream<S>` | O(n) | Терминал, подобный IO |
268
- | `collect(collector)` | `R` | O(n) | Общее свёртывание, лежащее в основе каждого терминала выше |
269
-
270
- ### 6.3 Структурные коллекторы
271
-
272
- `toArray`, `toSet` и `toMap` завершают конвейер контейнером JavaScript. Ещё двенадцать терминалов завершают его одной из собственных структур библиотеки, так что результат несёт инварианты, которые гарантирует эта структура, а не те, что случайно есть у массива. Они доступны на каждом коллекторабле, включая голый коллекторабл из `toCollectable()`.
273
-
274
- | Терминал | Возвращает | Порядок | Стоимость |
275
- | :--- | :--- | :--- | :--- |
276
- | `toArray()` | `Array<E>` | порядок обхода | O(n) |
277
- | `toSet()` | `Set<E>` | порядок вставки, SameValueZero | O(n) в среднем |
278
- | `toMap(key, value?)` | `Map<K, V>` | порядок вставки; побеждает последняя запись | O(n) в среднем |
279
- | `toOrderedSet(comparator?)` | `SynchronousOrderedSet<E>` | по возрастанию компаратора | O(n²) в худшем случае |
280
- | `toUnorderedSet()` | `SynchronousUnorderedSet<E>` | не определён (хеш-бакеты) | O(n) ожидаемо |
281
- | `toOrderedMap(key, value?)` | `SynchronousOrderedMap<K, V>` | порядок вставки; побеждает последняя запись | O(n) ожидаемо |
282
- | `toUnorderedMap(key, value?)` | `SynchronousUnorderedMap<K, V>` | не определён | O(n) ожидаемо |
283
- | `toList()` | `SynchronousList<E>` | порядок обхода, дубликаты сохраняются | O(n) |
284
- | `toRing(span?)` | `SynchronousRing<E>` | порядок обхода задаёт позиции на кольце | O(n) |
285
- | `toLRUSet(capacity)` | `SynchronousUnorderedLRUSet<E>` | по давности; выживают только последние `capacity` | O(n²) в худшем случае |
286
- | `toLRUMap(capacity, key, value?)` | `SynchronousUnorderedLRUMap<K, V>` | по давности | O(n) ожидаемо |
287
- | `toSkipSet(comparator?)` | `SynchronousSkipSet<E>` | по возрастанию компаратора, с навигацией | O(n²) в худшем случае |
288
- | `toSkipMap(key, value?)` | `SynchronousSkipMap<K, V>` | по компаратору ключей, с навигацией | O(n) ожидаемо |
289
- | `toBinarySearchTree(comparator?)` | `SynchronousBinarySearchTree<E>` | по возрастанию компаратора, сбалансировано по высоте | O(n log n) |
290
- | `toRootedForest()` | `SynchronousRootedForest<E>` | порядок обхода становится структурой | O(n) |
291
-
292
- Тип результата следует за типом элемента самого коллекторабла, поэтому ни один терминал не принимает аргумент типа: `SynchronousCollectable<E>` сразу отдаёт `Array<E>`, `SynchronousList<E>` или `SynchronousOrderedSet<E>`. Остаются только деловые аргументы — компаратор `toOrderedSet` / `toSkipSet` / `toBinarySearchTree`, ёмкость пары LRU, диапазон `toRing` и извлекатели ключа и значения семейства отображений.
293
-
294
- Кольцо — это множество, поэтому повторяющиеся элементы сохраняют первую позицию; все остальные структуры выше сохраняют дубликаты, если их собственный контракт не говорит иного (`toOrderedSet`, `toUnorderedSet` и `toSkipSet` — множества; `toBinarySearchTree` и `toList` — нет).
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
- Асинхронные коллектораблы предоставляют те же терминалы и разрешают каждый в promise, поэтому форма вызова приобретает только `await`.
310
-
311
- ---
312
-
313
- <a id="7-statistics-and-windows"></a>
314
- ## 7. Статистика и окна
315
-
316
- ### 7.1 Поверхность статистики
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
- Оба статистических семейства предоставляют одну и ту же поверхность — `summate`, `average`, `median`, `mode`, `variance`, `standardDeviation`, `frequency`, `findMaximum`, `findMinimum` — с `bigint`-арифметикой для `toBigintStatistics()`. Меры разброса и семейство квантилей возвращают `Optional`, потому что выборка может оказаться слишком мала для их определения; извлекайте их через `.get()`, `.orElse(…)` или `.isPresent()`.
328
-
329
- ### 7.2 Оценки и значения по умолчанию
330
-
331
- Значения по умолчанию — это определения, которые используют R, NumPy, pandas и Excel, поэтому вычисленное здесь значение можно сравнивать с этими инструментами без поправочного коэффициента.
332
-
333
- | Статистика | По умолчанию | Раньше | Вернуть старое значение через |
334
- | :--- | :--- | :--- | :--- |
335
- | `quantile(probability)` | Хайндман–Фан тип 7 | тип 1 | `{ method: "type-1" }` |
336
- | `interquartileRange()` | Хайндман–Фан тип 7 | тип 1 | `{ method: "type-1" }` |
337
- | `skewness()` | скорректированный `G1` | `g1` | `{ estimator: "g1" }` |
338
- | `kurtosis()` | скорректированный избыточный `G2` | `g2` | `{ estimator: "g2" }` |
339
- | `variance()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
340
- | `standardDeviation()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
341
-
342
- Все девять типов Хайндмана–Фана доступны как `"type-1"` … `"type-9"`, а `{ excess: false }` возвращает тройку к эксцессу и сообщает неизбыточный четвёртый стандартизованный момент. Типы опций — `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator`, `VarianceOptions` — экспортируются из `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` и `G2` не определены при менее чем трёх и четырёх наблюдениях, а выборочные дисперсия и стандартное отклонение — при менее чем двух при `ddof = 1`. Это сообщается пустым `Optional`, а не вводящим в заблуждение нулём:
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
- Представления bigint принимают те же опции и округляют каждый результат до целого `bigint`.
371
-
372
- ### 7.3 Окна
373
-
374
- Окна заново выставляют буферизованные элементы как конвейер конвейеров, поэтому окно можно потребить, не материализуя его сначала в массив. Обе формы включают последнее частичное окно.
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. Модель индексов
388
-
389
- Индекс — это `bigint`, прикреплённый к каждому элементу. Операторы либо **передают** его, либо
390
- **переиндексируют** с нуля, либо позволяют вам его **вычислить**.
391
-
392
- Упорядоченный сбор нормализует каждый индекс по модулю количества элементов, а затем сортирует по результату.
393
- Именно поэтому `translate(7)` на пяти элементах эквивалентен `translate(2)`: `7 ≡ 2 (mod 5)`.
394
-
395
- Неупорядоченный сбор сохраняет тот индекс, который произвёл конвейер, и возвращает элементы в порядке
396
- прибытия. Отрицательные индексы допустимы и после нормализации просто сортируются раньше неотрицательных.
397
-
398
- ---
399
-
400
- <a id="9-the-asynchronous-model"></a>
401
- ## 9. Асинхронная модель
402
-
403
- `AsynchronousSemantic` повторяет синхронную поверхность, возвращая промисы. Записи ставятся в очередь,
404
- поэтому чтение, выданное после записи, всегда её наблюдает.
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
- Фабрики, асинхронные по своей природе, — `usePromise`, `useBlob`, `useInterval`,
415
- `useSubscription` — возвращают `AsynchronousSemantic` напрямую, а `useFrom` / `useIterate`
416
- переходят в асинхронный режим, когда им передают `AsyncIterable`.
417
-
418
- ---
419
-
420
- <a id="10-periodic-analysis-rings-fields-and-scores"></a>
421
- ## 10. Периодический анализ: кольца, поля и оценки
422
-
423
- Математическая половина библиотеки трактует цикл как полноценный объект, а не как таблицу
424
- поиска. Три конструкции работают вместе.
425
-
426
- ### 10.1 Кольцо — это одно целое число
427
-
428
- `Ring` — это **n-кратное кольцо**: количество элементов, позиционный период и число различных
429
- фаз — одно и то же целое `n`. Фаза позиции вычисляется из её точного остатка, поэтому
430
- кольцо периодично до бита:
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` — это **окружающая** (ambient) скорость вращения, а не шаг фазы между позициями: среда
445
- сдвигает каждый фазор (phasor) на один и тот же угол, поэтому она никогда не может нарушить внутренний
446
- узор. `span` задаёт, сколько оборотов занимает кольцо.
447
-
448
- `purity()` — это доля энергии, переносимая основной модой,
449
- `max(|c₁|², |c_{n−1}|²) / (n · Σ|z|²)`. Она равна `1` ровно тогда, когда кольцо с точностью до
450
- поворота совпадает с корнями `n`-й степени из единицы, и стоит `O(n)`, потому что нужны всего два бина.
451
- Проверяются оба направления, поскольку кольцо может обходить окружность в любую сторону.
452
-
453
- ### 10.2 Поле — это ограниченная оценка
454
-
455
- `PhaseField` отображает относительную фазу в оценку из `[-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
- Оценка — это усечённый ряд косинусов, `a₀ + Σ wₘ·cos(2πmΔ)`, и конструктор обеспечивает
468
- `|a₀| + Σ|wₘ| ≤ 1`. По неравенству треугольника оценка никогда не покидает `[-1, 1]` — граница является
469
- **следствием конструкции**, а не ограничением, наложенным после.
470
-
471
- Причина, по которой поле является рядом, а не таблицей: таблица из `n` точек точно воспроизводит свои
472
- собственные отсчёты, но её тригонометрический интерполянт даёт выбросы между ними. Таблица из пяти
473
- точек со значениями `[+1, +0.5, −1, −0.5, 0]` интерполируется в диапазон `[−1.0428, 1.0041]`, что уже
474
- нарушает обещание. Ограниченный ряд не может.
475
-
476
- ### 10.3 Анализ измеряет данные относительно каждого элемента
477
-
478
- Каждый элемент кольца — это опорная фаза. `analyse` отыскивает каждое наблюдение на кольце и
479
- измеряет его относительно каждой опоры:
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
- Каждая строка опоры сообщает `count`, среднюю относительную фазу `mean` и среднюю результирующую длину
491
- `resultant`. Одно тождество стоит запомнить: **`resultant` одинаков для каждой опоры**,
492
- потому что смена базовой линии лишь поворачивает все относительные фазы на константу. От опоры к опоре
493
- различаются только счётчик и среднее.
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 Связанные инструменты
501
-
502
- | Вызов | Отвечает | Стоимость |
503
- | :--- | :--- | :--- |
504
- | `purity()` | остаётся ли кольцо корнями `n`-й степени из единицы? | O(n) |
505
- | `resultant(t?)` | насколько фазоры сконцентрированы прямо сейчас? | O(n) |
506
- | `analyse(data)` | как эти данные соотносятся с каждым элементом? | O(n · m) |
507
- | `toSpectrum()` | кольцо как спектр, волновое число на оси частот | O(n²) |
508
- | `useAngularSpectrum(ring, instants, plan)` | плоскость `(ω, n)` во времени | O(n² · frames) |
509
-
510
- Модель дополняют ещё два примитива: `combineRings(a, b)` соединяет два кольца по их наименьшему общему
511
- кратному (обратный образ, pullback, а не произведение — пары, которые не могут разделить момент,
512
- просто отсутствуют), а `rate()` даёт каждому элементу свою внутреннюю частоту, чтобы кольцо могло
513
- дрейфовать относительно среды. `jetAt`, `windingAt` и `errorIntegralAt` показывают дифференциальное состояние дрейфа.
514
-
515
- ### 10.5 Спектральные окна
516
-
517
- Построения колец — одна половина аналитической поверхности; другая работает с дискретизированными сигналами. Её самый переиспользуемый примитив — окно, и у каждой формы здесь две версии. Симметричная версия делит кадр на `count − 1`; периодическая (DFT-чётная) делит его на `count`, на один отсчёт меньше полного периода, поэтому спад непрерывен при периодическом продолжении, которое предполагает 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
- | Форма | Симметричная | Периодическая | Примечания |
527
- | :--- | :--- | :--- | :--- |
528
- | Прямоугольная | `useRectangularWindow` | — | без спада; нет периодического различия |
529
- | Ханна | `useHannWindow` | `useHannPeriodicWindow` | вариант по умолчанию для коротковременного преобразования |
530
- | Хэмминга | `useHammingWindow` | `useHammingPeriodicWindow` | — |
531
- | Блэкмана | `useBlackmanWindow` | `useBlackmanPeriodicWindow` | — |
532
- | Наттолла | `useNuttallWindow` | `useNuttallPeriodicWindow` | четырёхчленный Блэкман–Харрис |
533
- | Плосковершинная | `useFlatTopWindow` | `useFlatTopPeriodicWindow` | точность амплитуды важнее разрешения |
534
- | Кайзера | `useKaiserWindow(beta)` | `useKaiserPeriodicWindow(beta)` | `beta` задаёт ширину главного лепестка |
535
-
536
- Используйте периодическую версию для анализа и overlap-add: при полукадровом шаге периодическое окно Ханна суммируется в `1.000000` при любой длине, тогда как симметричное колеблется на `0.841 dB` при `N = 16` и ещё на `0.013 dB` при `N = 1024`. Симметричную версию оставьте для проектирования фильтров, где кадр не является одним периодом спада.
537
-
538
- Плосковершинные окна возвращают **отрицательные веса** у краёв кадра, потому что плосковершинное окно именно таково. Они возвращаются без обрезки, поскольку обрезка искажает главный лепесток и уничтожает малые потери на «гребешке», ради которых окно и существует:
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 Уровни, плоскостность и энергия полосы
547
-
548
- `decibel` исчез как единая функция, потому что амплитуда и мощность не делят один множитель. `amplitudeDecibel` применяет `20·log10` к отношению типа напряжения; `powerDecibel` применяет `10·log10` к мощности, энергии, среднему квадрату или спектральной плотности мощности:
549
-
550
- ```typescript
551
- amplitudeDecibel(0.25); // -12.0412
552
- powerDecibel(0.25); // -6.0206
553
- ```
554
-
555
- `Bin.decibel` и `Spectrum.decibelOf` — уровни амплитуды; `Spectrum.powerDecibelOf` — соответствующий уровень мощности. Подача мощности в амплитудную форму сообщает `20·log10(power)` и удваивает уровень в децибелах.
556
-
557
- Два измерения спектра изменили чтение:
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()` сравнивает геометрическое и арифметическое средние `power()` по односторонней половине спектра, а не среднее амплитуд по обеим половинам, и чисто визуальное удвоение амплитуды из `oneSided()` намеренно не применяется. `bandEnergy()` по умолчанию использует одностороннее чтение, свёртывая сопряжённую половину: DC и Найквист считаются один раз, а каждый внутренний бин — дважды; полоса, покрывающая всю ось, тогда удовлетворяет равенству Парсеваля, `Σ|X|² = N·Σx²`. Передайте `"two-sided"` для комплексного спектра, чьи бины уже независимы.
571
-
572
- ### 10.7 Коротковременный анализ
573
-
574
- `Sampler.shortTimeFourierTransform` и `Sampler.spectrogram` следуют соглашениям `stft` из SciPy: периодическое окно Ханна, граница, заполненная нулями с обоих концов, и хвост, заполненный нулями.
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
- Расширение центрирует первый кадр на первом отсчёте, а заполнение хвоста делает последние отсчёты достижимыми: 64 отсчёта при кадре 16 и шаге 8 дают 9 кадров вместо 7, а 1000 отсчётов при кадре 256 и шаге 128 дают 9 вместо 6. Время кадра — это центр, а не начало: `(k·hop + (frameSize mod 2)/2)/rate` для расширенного представления и `(k·hop + frameSize/2)/rate` для нерасширенного, а `toFrame()` срезает расширение перед обратным преобразованием.
588
-
589
- ---
590
-
591
- <a id="11-utility-helpers"></a>
592
- ## 11. Вспомогательные функции
593
-
594
- ### 11.1 Иерархия ошибок
595
-
596
- `structure/error.ts` экспортирует иерархию из пятнадцати классов для ошибок, возбуждаемых *вокруг* конвейера — вашим собственным кодом, собственным коллектором или адаптером:
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
- Каждый класс фиксирует `name`, пересылает нативную `cause`, несёт необязательный мешок `context` и сериализуется через `toJSON()`; у каждого есть одноимённый брендовый страж.
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` — тот же класс, что и экспортируемый `RangeError`, поэтому `instanceof` выполняется для обоих имён; импорт `RangeError` затеняет глобальный конструктор во всём модуле, и именно поэтому существует псевдоним. Ни один из этих классов не возбуждается собственными проверками аргументов библиотеки — там по-прежнему возбуждаются нативные `TypeError` и `RangeError`; иерархия — это словарь для слоя выше.
629
-
630
- ### 11.2 Таблица помощников
631
-
632
- | Помощник | Назначение |
633
- | :--- | :--- |
634
- | `useCompare(a, b)` | Тотальное упорядочение для смешанных примитивов |
635
- | `useToNumber` / `useToBigInt` | Преобразование чисел без потерь с проверкой диапазона |
636
- | `useRandom` | Случайные помощники с начальным значением или без него |
637
- | `useTraverse` | Структурный обход вложенных объектов и массивов |
638
- | `useStringify` | Сериализация JSON, переживающая циклические ссылки |
639
- | `useHash` / `maskOf` / `register` / `unregister` | Структурное хеширование и реестр стратегий хеша |
640
- | `invalidate` / `validate` | Стражные проверки, порождающие ошибку единой формы |
641
- | `Optional<T>` | Замена `null` в библиотеке: `Optional.of(x)` / `Optional.empty()` |
642
-
643
- ### 11.3 `useTraverse`
644
-
645
- `useTraverse(target, callback)` разворачивает любой объект в последовательность его **непустых листьев**. Он спускается во вложенные объекты, обходит итерируемые значения по позициям, пропускает `null` и `undefined` и никогда не входит повторно в уже посещённый объект — поэтому граф объектов с циклом завершается, а не переполняет стек.
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
- Обратный вызов выполняется один раз на лист и получает собственный ключ листа (его индекс, если лист пришёл из итерируемого значения), значение и путь к нему. Возврат `false` прекращает **весь** обход, а не только текущую ветвь:
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
- Цикл безопасен по той же причине:
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
- **Каждый обратный вызов получает собственный снимок пути.** Обход держит внутри единственный рабочий массив и в момент передачи листа копирует его в новый массив, замороженный через `Object.freeze`. Снимок после этого не меняется, поэтому его можно сохранять как есть; два обратных вызова никогда не делят один массив, а повторный обход не задевает уже выданные снимки. Его тип — `ReadonlyArray`, поэтому запись в него будет ошибкой компиляции, а у замороженного массива она вдобавок бросит `TypeError` во время выполнения. Такая надёжность стоит одной аллокации `O(depth)` на каждый сообщённый лист; ветвь, не сообщающая ничего, лишнего не выделяет.
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` — это тот же обход в виде фабрики: она сообщает те же листья парами `{ key, value }`, поэтому правила и стоимость совпадают.
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. Производительность и подводные камни
707
-
708
- **У лени есть цена на каждой ступени.** Каждый оператор добавляет переход через генератор. Для плотных
709
- числовых циклов по массивам в памяти обычный цикл `for` побьёт любой конвейер; конвейер выигрывает, когда
710
- источник ленив, бесконечен или конвейер достаточно длинен, чтобы дисциплина индексов окупилась.
711
-
712
- **`toOrdered()` сортирует; `toUnordered()` — нет.** Если кажется, что обращение «не сработало», коллектор был
713
- неупорядоченным — переписывание индексов становится порядком только тогда, когда индексы нормализованы.
714
-
715
- **`useGenerate` без `interrupt` никогда не завершается.** Всегда передавайте останавливающий предикат или
716
- нижестоящий `limit` / `takeWhile`.
717
-
718
- **`Ring` — это множество с порядком вставки.** Элементы уникальны и сохраняют порядок, в котором были
719
- впервые добавлены. Повторный `add` возвращает `false`, а `set`, который ввёл бы дубликат, бросает
720
- исключение. Именно это позволяет фазе быть функцией *элемента*, а не той позиции, которую ему случилось
721
- занимать.
722
-
723
- **`Spectrum` предполагает вещественный сигнал в своих односторонних помощниках.** `oneSided()`, `getNyquist()` и
724
- `centroid()` предполагают сопряжённую симметрию. Пространственный спектр фазового кольца комплексен и в
725
- общем случае не симметричен — кольцо, которое обходит окружность в обратную сторону, помещает всю свою
726
- энергию в бин `n − 1`, за пределы Найквиста. Читайте бины напрямую.
727
-
728
- **Структурные коллекторы — это терминалы, а не представления.** Каждый опустошает конвейер, а затем строит полную структуру, поэтому стоит O(n) времени и O(n) памяти и не может работать на бесконечном источнике. Используйте его в конце конвейера и никогда внутри цикла по конвейеру.
729
-
730
- **`variance()` и его собратья возвращают `Optional`.** `variance().get()` на слишком малой выборке бросает исключение; используйте `.orElse(0)` или проверяйте `.isPresent()`, когда размер выборки не в вашей власти. То же относится к `standardDeviation`, `quantile`, `interquartileRange`, `skewness` и `kurtosis`.
731
-
732
- **Периодические окна — это окна анализа.** `useHannWindow` и другие симметричные формы предназначены для проектирования фильтров; коротковременному преобразованию нужны `useHannPeriodicWindow` и его родня, иначе сумма overlap-add будет колебаться на каждом стыке кадров.
733
-
734
- ---
735
-
736
- <a id="13-phase-rings"></a>
737
- ## 13. Фазовые кольца: точный цикл
738
-
739
- Глава 10 представила кольцо как модель цикла и использовала его, чтобы измерять набор данных
740
- относительно каждой фазы. Эта глава даёт полную поверхность: как позиция становится углом, что
741
- меняет и что не меняет частота среды, и инструменты, читающие кольцо относительно времени,
742
- данных и другого кольца.
743
-
744
- `SynchronousRing` — это позиционный список, под которым лежит круговое двусвязным кольцом, у которого
745
- голова и хвост соединены. Одновременно это *множество* с порядком вставки: ни один элемент не
746
- появляется дважды, а отклонённая запись не трогает ни связи, ни отсчёты. Кольцом, а не просто
747
- списком, его делает то, что фаза позиции — чистая функция её остатка по модулю текущей длины;
748
- поэтому кольцо периодично до бита, и его баланс не может дрейфовать.
749
-
750
- ### 13.1 Кольцо — это одно целое, вычисленное точно
751
-
752
- Кольцо несёт `n` элементов, и число элементов, позиционный период и число различных фаз — одно
753
- и то же целое. Позиция `i` несёт
754
-
755
- `phase + span · 2π · (((i mod n) + n) mod n) / n`
756
-
757
- причём остаток сокращается в `bigint`, прежде чем стать `number`. Именно поэтому `phaseAt(i)` и
758
- `phaseAt(i + n)` *тождественны*, а не просто близки: второй вызов сокращается к тому же целому,
759
- вычисляет то же произведение и даёт тот же double. Индекс далеко за точным целым диапазоном
760
- `number` всё равно попадает в нужную позицию, а отрицательный индекс идёт по кольцу назад.
761
-
762
- | Вызов | Возвращает | Смысл |
763
- | :--- | :--- | :--- |
764
- | `new SynchronousRing(source, options?)` | кольцо | `source` — любой итерируемый объект, дубликаты отбрасываются |
765
- | `getPhase()` / `getFrequency()` / `getSpan()` | `Angle`, `Frequency`, `number` | три параметра конструкции |
766
- | `size()` | `bigint` | то самое `n`, оно же период |
767
- | `phaseAt(index)` | `Optional<Angle>` | только собственная фаза; время, скорость и частота игнорируются |
768
- | `elementAt(index)` | `Optional<RingElement>` | элемент, собственная фаза, частота среды, фазор |
769
- | `frequencyAt(index)` | `Optional<Frequency>` | скорость среды, пусто для пустого кольца |
770
- | `toComplex()` / `toComplex(index)` | `Complex[]` / `Optional<Complex>` | собственные фазоры |
771
-
772
- `RingOptions` добавляет две функции на элемент: `magnitude(element, index)` задаёт длину фазора
773
- и по умолчанию равна единице, а `rate(element, index)` даёт элементу собственную частоту и по
774
- умолчанию равна нулю, то есть фиксированному маркеру.
775
-
776
- ```typescript
777
- import { SynchronousRing } from "semantic-typescript";
778
-
779
- const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
780
-
781
- ring.size(); // 6n
782
- ring.phaseAt(0n).get().toDegreeValue(); // 0
783
- ring.phaseAt(1n).get().toDegreeValue(); // 60
784
- ring.phaseAt(-1n).get().toDegreeValue(); // 300
785
- ring.phaseAt(6n).get().getValue() === ring.phaseAt(0n).get().getValue(); // true
786
- ring.phaseAt(10n ** 20n).get().toDegreeValue(); // 240
787
- ```
788
-
789
- `phaseAt(-1n)` на кольце из шести элементов равно `300`, а `10n ** 20n` по модулю шести даёт
790
- `4`, поэтому читается как `240`. Ни один случай не накапливает произведение с плавающей точкой —
791
- именно это сохраняет тождество точным при смене представления индекса.
792
-
793
- ### 13.2 Частота среды и состояние в момент времени
794
-
795
- `frequency` — это **скорость вращения среды**, в которую кольцо погружено, а не шаг фазы между
796
- позициями. Среда сдвигает фазу каждого элемента на один и тот же угол, поэтому не может изменить
797
- собственный узор, не может сломать баланс и не может изменить ни один результат `phaseAt`. По
798
- умолчанию она равна `0 Hz`.
799
-
800
- `relativeAt(index, time)` вычитает этот общий угол, оставляя координату, по которой оценивается
801
- `PhaseField`. `viewAt` и `viewsAt` возвращают полное состояние в момент времени, включая
802
- собственную `rate` элемента, а дифференциальные аксессоры вскрывают дрейф:
803
-
804
- | Вызов | Формула | Читает |
805
- | :--- | :--- | :--- |
806
- | `relativeAt(index, time)` | `собственная + rate · t − среда · t` | фазу, которую видит среда |
807
- | `viewAt(index, time)` / `viewsAt(time)` | то же плюс элемент, индекс, время, rate, фазор | полное состояние |
808
- | `jetAt(index, time)` | фаза в `t`, своя rate, `slip = rate − среда` | дифференциальное состояние |
809
- | `windingAt(index, time)` | `trunc(slip · t)` | целые обороты скольжения, усечённые к нулю |
810
- | `errorIntegralAt(index, time, origin?)` | `∫ относительной фазы dt`, в оборотах · секундах | как долго элемент опережает |
811
- | `resultant(time?)` | `abs(Σ w · e^{iθ}) / Σ w` | концентрация фаз, не зависит от вращения среды |
812
- | `analyse(data, time?)` | счётчики и средние по каждой опоре | данные, измеренные относительно каждого элемента |
813
- | `scoreAt(index, time, field)` / `scoresAt(time, field)` | поле, выбранное по относительной фазе | ограниченная оценка на позицию |
814
-
815
- ```typescript
816
- import { SynchronousRing, useHertz } from "semantic-typescript";
817
-
818
- const moving = new SynchronousRing(["a", "b", "c", "d", "e", "f"], {
819
- frequency: useHertz(1),
820
- });
821
-
822
- moving.getFrequency().getHertz(); // 1
823
- moving.phaseAt(1n).get().toDegreeValue(); // 60 — intrinsic
824
- moving.relativeAt(1n, 0.25).get().toDegreeValue(); // 330 — 60 − 90
825
- moving.viewAt(1n, 0.25).get().relative.toDegreeValue(); // 330
826
- moving.jetAt(1n, 0.25).get().slip; // -1
827
- moving.purity(); // 1 — rotation cannot break it
828
- ```
829
-
830
- При `1 Hz` среда за `0.25 s` проходит четверть оборота, поэтому относительная фаза позиции один
831
- равна `60 − 90 = 330` градусам, а собственная фаза остаётся `60`. Джет сообщает `slip` `-1` герц,
832
- потому что сам элемент не движется, а `purity()` по-прежнему `1`: поворот всех фазоров на один
833
- угол не является деформацией.
834
-
835
- ### 13.3 Чистота и баланс
836
-
837
- `purity()` — доля энергии кольца, которую несёт основная мода:
838
-
839
- `max(abs(c₁)², abs(c_{n−1})²) / (n · Σ abs(z)²)`
840
-
841
- Проверяются оба основных бина, потому что кольцо может обходить круг в любую сторону. Кольцо с
842
- `span: 5` на шести элементах шагает на `300` градусов, то есть на `−60`: оно идёт назад, кладёт
843
- всю энергию в `c₅`, и формула, смотрящая только на `c₁`, объявила бы его нечистым. Стоимость
844
- остаётся `O(n)`, потому что нужны лишь эти два бина и полная энергия.
845
-
846
- Граничные случаи — часть контракта. Кольцо из менее чем двух элементов не имеет основной моды и
847
- сообщает `purity() === 0` и `isBalanced() === false` независимо от содержимого. Кольцо, у
848
- которого все фазоры нулевой длины, не имеет энергии и тоже сообщает ноль. `isBalanced(epsilon?)`
849
- равно `1 − purity() <= epsilon` с допуском `1E-9` по умолчанию.
850
-
851
- ```typescript
852
- import { SynchronousRing } from "semantic-typescript";
853
-
854
- const forward = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
855
- const backward = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 5 });
856
- const weighted = new SynchronousRing(["a", "b", "c", "d"], {
857
- magnitude: element => element === "a" ? 1 : 0,
858
- });
859
-
860
- forward.purity(); // 1
861
- backward.phaseAt(1n).get().toDegreeValue(); // 300 — it runs backwards
862
- backward.purity(); // 1 — the energy sits in c₅
863
- new SynchronousRing(["only"]).purity(); // 0
864
- new SynchronousRing(["only"]).isBalanced(); // false
865
- weighted.purity(); // 0.25
866
- ```
867
-
868
- ### 13.4 Результирующая длина и симметричные компоненты
869
-
870
- `resultant(time?)` — средняя результирующая длина: модуль суммы фазоров, делённый на сумму
871
- модулей. В отличие от `purity`, спрашивающей, является ли кольцо по-прежнему корнями `n`-й
872
- степени из единицы, `resultant` спрашивает, насколько фазоры сосредоточены сейчас. Сбалансированное
873
- кольцо сообщает `0`; кольцо, все элементы которого имеют один угол, сообщило бы `1`, но семантика
874
- множества запрещает дубликаты, поэтому на практике `1` появляется, когда `magnitude` обнуляет все
875
- элементы, кроме одного.
876
-
877
- `zeroSequence()` — средний фазор, то есть `c₀ / n`. `sequences()` возвращает все `n` симметричных
878
- компонент, начиная с низшего порядка, прямым преобразованием. `toSpectrum()` упаковывает их в
879
- `Spectrum`, бин `j` которого сообщает `j` циклов на оборот кольца, так что ось частот — это ось
880
- волновых чисел.
881
-
882
- ```typescript
883
- import { SynchronousRing } from "semantic-typescript";
884
-
885
- const round = (value: number): number => Number(value.toFixed(4));
886
- const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
887
-
888
- round(ring.resultant().get()); // 0
889
- round(ring.zeroSequence().get().magnitude()); // 0
890
- ring.sequences().map(component => round(component.magnitude())); // [0, 6, 0, 0, 0, 0]
891
- ring.toSpectrum().get().size(); // 6
892
- ring.toSpectrum().get().getResolution().getHertz(); // 1
893
- ```
894
-
895
- Компоненты комплексны и вообще **не сопряжённо-симметричны**: сбалансированное кольцо кладёт всю
896
- энергию в бин один, а идущее назад — в бин `n − 1`. Односторонние помощники `Spectrum`
897
- предполагают вещественный сигнал; читайте бины напрямую, а не сворачивайте их.
898
-
899
- ### 13.5 Анализ данных относительно каждого элемента
900
-
901
- `analyse(data, time?)` сопоставляет каждое наблюдение кольцу по тождеству и измеряет его
902
- относительно каждого элемента как опорной фазы. Наблюдения, которых кольцо не содержит,
903
- считаются `missed`, а не угадываются, потому что кольцо — единственный источник фаз. Каждая
904
- строка опоры сообщает `count`, среднюю относительную фазу `mean` и среднюю результирующую длину
905
- `resultant`.
906
-
907
- Одно тождество стоит запомнить: **`resultant` одинаков для каждой опоры**. Смена базовой линии
908
- поворачивает все относительные фазы на константу, а поворот не может изменить результирующую
909
- длину. От опоры к опоре различаются счётчик и среднее.
910
-
911
- ```typescript
912
- import { SynchronousRing } from "semantic-typescript";
913
-
914
- const round = (value: number): number => Number(value.toFixed(4));
915
- const ring = new SynchronousRing(["a", "b", "c", "d", "e"]);
916
- const report = ring.analyse(["a", "c", "c", "a", "zz", "e"]);
917
-
918
- report.matched; // 5
919
- report.missed; // 1
920
- report.references.map(row => row.count); // [2, 0, 2, 0, 1]
921
- report.references.map(row => round(row.resultant)); // [0.1453, 0.1453, 0.1453, 0.1453, 0.1453]
922
- ```
923
-
924
- Оценка — обратная сторона той же монеты. `PhaseField` — ограниченный ряд косинусов (раздел
925
- 10.2), и `scoreAt`/`scoresAt` выбирают его по относительной фазе, так что одно и то же кольцо
926
- можно оценивать относительно выравнивания, гармонического узора или любого другого поля, не
927
- меняя само кольцо.
928
-
929
- ```typescript
930
- import { SynchronousRing, useField } from "semantic-typescript";
931
-
932
- const round = (value: number): number => Number(value.toFixed(4));
933
- const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
934
- const cosine = useField([1]);
935
-
936
- round(ring.scoreAt(0n, 0, cosine).get()); // 1
937
- round(ring.scoreAt(1n, 0, cosine).get()); // 0.5
938
- ring.scoresAt(0, cosine).map(score => round(score)); // [1, 0.5, -0.5, -1, -0.5, 0.5]
939
- ```
940
-
941
- ### 13.6 Объединение колец и угловой спектр
942
-
943
- `combineRings(left, right)` соединяет два кольца по наименьшему общему кратному, а не по
944
- произведению. Момент `i` объединённого кольца читает `i mod m` левого кольца и `i mod n` правого,
945
- поэтому объединённое кольцо замыкается через `lcm(m, n)` моментов. Пары, которые никогда не могут
946
- встретиться — те, чьи позиции расходятся по модулю `gcd(m, n)`, — просто отсутствуют; поэтому
947
- результат является обратным образом, а не произведением, и `aligns(left, right)` сообщает,
948
- представима ли данная пара.
949
-
950
- `useAngularSpectrum(ring, instants, plan)` строит плоскость `(ω, n)`: каждый момент даёт кадр
951
- фазоров, кадр `k` моды `j` — это `j`-я симметричная компонента этого кадра, а `cells[frame][order]`
952
- — коэффициент. Взять один столбец — получить временной ряд одной пространственной моды, а
953
- `spectrum(order)` — её временной спектр.
954
-
955
- ```typescript
956
- import { Sample, SynchronousRing, combineRings, useAngularSpectrum } from "semantic-typescript";
957
-
958
- const left = new SynchronousRing(["a", "b", "c", "d"]);
959
- const right = new SynchronousRing(["x", "y", "z", "w", "v", "u"]);
960
- const joined = combineRings(left, right);
961
-
962
- joined.period; // 12n
963
- joined.divisor; // 2n
964
- joined.ring.size(); // 12n
965
- joined.aligns(0n, 0n); // true
966
- joined.aligns(1n, 0n); // false
967
- joined.ring.get(0n).join("-"); // a-x
968
-
969
- const angular = useAngularSpectrum(left, [0, 0.25, 0.5, 0.75], new Sample(4, 1));
970
- angular.order; // 4
971
- angular.frameCount; // 4
972
- angular.cells[0][1].magnitude(); // 4
973
- angular.spectrum(1).get().size(); // 4
974
- ```
975
-
976
- ### 13.7 Ловушки
977
-
978
- **Точность есть точность.** `phaseAt(i)` и `phaseAt(i + n)` — один и тот же double, а не два
979
- значения в пределах допуска. Кольцо, накапливающее `2π / n` на элемент, этого не выполнит; это
980
- сначала сокращает индекс в `bigint`.
981
-
982
- **`frequency` — не шаг фазы.** Это скорость вращения среды, по умолчанию ноль, и поворот всех
983
- фазоров на один угол не может сломать собственный узор. Если нужно, чтобы позиции смещались
984
- относительно друг друга, используйте `rate`, а не `frequency`.
985
-
986
- **`purity()` обязана смотреть оба основных бина.** Кольцо, идущее назад, всё ещё остаётся корнями
987
- `n`-й степени из единицы; только по `c₁` оно выглядело бы нечистым.
988
-
989
- **`n < 2` — определённый граничный случай.** `purity()` возвращает `0`, а `isBalanced()` —
990
- `false`; пустое кольцо возвращает пустые `Optional` из методов выборки.
991
-
992
- **Span, делящий множитель с `n`, схлопывает орбиту.** Шесть элементов с `span: 2` посещают лишь
993
- три различные фазы, и кольцо перестаёт быть сбалансированным.
994
-
995
- **`resultant` инвариантен к вращению, но не ко времени.** Вращение среды его не меняет; `rate`
996
- каждого элемента меняет его по мере расфазировки.
997
-
998
- **Кольцо — множество, а `analyse` сопоставляет по тождеству.** Дубликаты отбрасываются при
999
- создании, а объект, структурно равный элементу кольца, но не являющийся той же ссылкой,
1000
- считается пропущенным.
1001
-
1002
- **`toSpectrum()` комплексен.** Его бины не сопряжённо-симметричны, поэтому односторонние
1003
- помощники `Spectrum` неприменимы. Читайте бины.
1004
-
1005
- **`windingAt` усекает к нулю, а `errorIntegralAt` интегрирует фазу.** Половина оборота скольжения
1006
- сообщается как `0n`; интеграл ошибки — в оборотах · секундах, а не скорость.
1007
-
1008
- ```typescript
1009
- import { SynchronousRing, useHertz } from "semantic-typescript";
1010
-
1011
- const collapsed = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 2 });
1012
- collapsed.phaseAt(1n).get().toDegreeValue(); // 120
1013
- collapsed.phaseAt(3n).get().toDegreeValue(); // 0
1014
- Number(collapsed.purity().toFixed(4)); // 0
1015
-
1016
- const falling = new SynchronousRing(["a", "b", "c", "d"], {
1017
- frequency: useHertz(2),
1018
- rate: () => useHertz(0.5),
1019
- });
1020
- falling.jetAt(0n, 2).get().slip; // -1.5
1021
- falling.windingAt(0n, 1).get(); // -1n
1022
- falling.windingAt(0n, 0.5).get(); // 0n
1023
-
1024
- const objects = new SynchronousRing([{ key: 1 }, { key: 2 }]);
1025
- objects.analyse([{ key: 1 }]).missed; // 1 — identity, not structure
1026
- ```
1027
-
1028
- ---
1029
-
1030
- <a id="14-signal-analysis"></a>
1031
- ## 14. Анализ сигналов: окна, спектры и время
1032
-
1033
- Глава 10 рассматривала кольцо как цикл фаз. Эта глава — его временной аналог: вещественная
1034
- запись, которая несёт, помимо отсчётов, два контекста, нужных любой спектральной формуле, —
1035
- план дискретизации, превращающий номер бина в герцы, и окно, задающее одновременно
1036
- когерентное усиление и эквивалентную шумовую полосу.
1037
-
1038
- Объект-значение — `SynchronousSignalCollectable`, его очередь-двойник —
1039
- `AsynchronousSignalCollectable`. Оба предоставляют одну и ту же поверхность `SignalCollectable`:
1040
- двадцать три метода, каждый делегирует ровно одной фабрике `useSynchronous<Method>` в
1041
- `synchronous/collectors`. Разделение намеренно. Класс — документированный фасад, фабрика
1042
- владеет накоплением, а конструктор один раз при данной длине вычисляет `sum(w) / count` и
1043
- `count * sum(w^2) / sum(w)^2`. Тот, кому нужно помнить «разделить на когерентное усиление»,
1044
- рано или поздно забудет, и полученные 6,02 децибела выглядят как правдоподобное измерение.
1045
-
1046
- ### 14.1 Сигнал несёт собственную калибровку
1047
-
1048
- Сигнал строится из итерируемого объекта и частоты, из существующего `Frame` или из конструктора
1049
- с явным `Sample`. Окно по умолчанию — периодическое окно Ханна; любой анализ, не назвавший окно,
1050
- использует его, и любой анализ принимает замену. Аксессоры метаданных читают зафиксированное при
1051
- создании состояние за постоянное время, поэтому сигнал можно передавать, не теряя контекст.
1052
-
1053
- | Вызов | Возвращает | Смысл |
1054
- | :--- | :--- | :--- |
1055
- | `SynchronousSignalCollectable.useRate(values, rate, window?)` | сигнал | длительность `count / rate` |
1056
- | `SynchronousSignalCollectable.useFrame(frame, window?)` | сигнал | сохраняет план кадра |
1057
- | `new SynchronousSignalCollectable(plan, values, window?)` | сигнал | число кадров должно совпадать с числом отсчётов |
1058
- | `getSample()` / `getValues()` / `getWindow()` | план, отсчёты, окно | замороженный контекст |
1059
- | `getRate()` | `number` | отсчётов в секунду |
1060
- | `getNyquist()` / `getResolution()` | `Frequency` | `rate / 2` и `rate / count` |
1061
- | `getCoherentGain()` / `getNoiseBandwidth()` | `number` | пересчитываются при этой длине |
1062
- | `size()` / `at(index)` / `toFrame()` / `equals(other)` / `toString()` | — | семантика значения |
1063
-
1064
- ```typescript
1065
- import { SynchronousSignalCollectable } from "semantic-typescript";
1066
-
1067
- const signal = SynchronousSignalCollectable.useRate(
1068
- Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 64)),
1069
- 64,
1070
- );
1071
-
1072
- signal.size(); // 64n
1073
- signal.getRate(); // 64
1074
- signal.getNyquist().getHertz(); // 32
1075
- signal.getResolution().getHertz(); // 1
1076
- signal.getCoherentGain(); // 0.5
1077
- signal.getNoiseBandwidth(); // 1.5
1078
- ```
1079
-
1080
- `getResolution()` — это шаг между бинами, а не разрешающая способность: он равен `rate / count`,
1081
- потому что в преобразовании `count` бинов, а дополнение нулями меняет шаг, но не то, что окно
1082
- способно разделить (раздел 14.8).
1083
-
1084
- ### 14.2 Двадцать три метода
1085
-
1086
- Методы образуют шесть семейств. Семь — спектральные отсчёты, два — время-частотные
1087
- преобразования, четыре — аналитические величины, три — статистики последовательности, пять —
1088
- дескрипторы формы, и два возвращают другой сигнал.
1089
-
1090
- | Метод | Возвращает | Соглашение |
1091
- | :--- | :--- | :--- |
1092
- | `spectrum(size?, window?)` | `Spectrum` | сырое и ненормированное, полная комплексная сетка |
1093
- | `oneSided(size?, window?)` | `Bin[]` | сопряжённая половина свёрнута, DC и Найквист не удваиваются |
1094
- | `amplitude(window?)` | `number[]` | `2·abs(X) / (count · CG)`, DC и Найквист один раз |
1095
- | `power(window?)` | `number[]` | `A² / 2` для внутреннего бина, `A²` при DC и Найквисте |
1096
- | `decibel(window?)` | `number[]` | `20·log10(амплитуда)` |
1097
- | `powerDecibel(window?)` | `number[]` | `10·log10(мощность)` |
1098
- | `powerSpectralDensity(frameSize?, hopSize?, window?)` | `SpectralDensity` | среднее Уэлча, `abs(X)² / (rate · Σw²)` |
1099
- | `shortTimeFourierTransform(frameSize, hopSize, window?)` | `Spectrum[]` | расширенные края, время по центрам кадров |
1100
- | `spectrogram(frameSize, hopSize, window?)` | `Spectrogram` | те же столбцы с осью времени |
1101
- | `analyticSignal()` | `Complex[]` | пара Гильберта `x + i·H(x)` |
1102
- | `instantaneousPhase()` | `Angle[]` | свёрнута в `[0, 2π)` |
1103
- | `instantaneousFrequency(rate?, gate?)` | `number[]` | развёрнутая разность фаз с порогом по огибающей |
1104
- | `envelope()` | `number[]` | `abs(z[n])`, на краях циклическая |
1105
- | `zeroCrossingRate(perSecond?)` | `number` | переходов через ноль на пару соседних отсчётов |
1106
- | `spectralFlux(frameSize?, hopSize?, window?)` | `number[]` | расстояние нормированных по норме кадров |
1107
- | `autocorrelation(maximumLag?, normalization?)` | `number[]` | по умолчанию коэффициент, `ρ[0] = 1` |
1108
- | `spectralCentroid(window?)` | `Optional<Frequency>` | средняя частота, взвешенная мощностью |
1109
- | `spectralBandwidth(window?)` | `Optional<Frequency>` | стандартное отклонение, взвешенное мощностью |
1110
- | `spectralRolloff(ratio?, window?)` | `Optional<Frequency>` | первый бин, достигающий `ratio` мощности |
1111
- | `spectralFlatness(window?)` | `number` | геометрическое среднее к арифметическому по мощности |
1112
- | `signalToNoiseRatio(frameSize?, hopSize?, window?, floorQuantile?)` | `Optional<number>` | медианный шумовой пол, `10·log10` |
1113
- | `detrended(mode?)` | сигнал | `"none"`, `"constant"` (по умолчанию), `"linear"` |
1114
- | `windowed(window?)` | сигнал | применяет окно и принимает его |
1115
-
1116
- Необязательные аргументы — не украшение. `frameSize` по умолчанию `min(256, size)`, шаг —
1117
- половина кадра с округлением вниз, `instantaneousFrequency` ставит порог `1E-3` от пиковой
1118
- огибающей, а `spectralRolloff` использует `0.85`. Каждое значение по умолчанию — то, что
1119
- выбирает эталонная реализация, когда параметр назван, но не передан.
1120
-
1121
- ### 14.3 Амплитуда, мощность и плотность
1122
-
1123
- Нужны три нормировки, потому что задаются три разных вопроса.
1124
-
1125
- `amplitude()` отвечает «как велик синус на этом бине»: `2·abs(X[k]) / (count · CG)` для
1126
- внутреннего бина и `abs(X[k]) / (count · CG)` при DC и Найквисте. Множитель два сворачивает
1127
- сопряжённую половину; когерентное усиление убирает окно. Если опустить любое из двух, синус
1128
- единичной амплитуды прочитается как `0.5`, то есть минус 6,02 децибела.
1129
-
1130
- `power()` отвечает «какой средний квадрат имел бы синус на этом бине»: `A² / 2` для внутреннего
1131
- бина и `A²` при DC и Найквисте, потому что эти два не колеблются. Это пиковая конвенция, и она
1132
- намеренно не удовлетворяет теореме Парсеваля: окно размазывает каждую линию по главному
1133
- лепестку, поэтому сумма всех бинов превышает средний квадрат на эквивалентную шумовую полосу —
1134
- `1.5` для Ханна.
1135
-
1136
- `powerSpectralDensity()` отвечает «сколько мощности на герц»: `abs(X)² / (rate · Σw²)`, усредняя
1137
- по перекрывающимся сегментам. Только сумма квадратов весов оставляет пол белого шума
1138
- несмещённым; деление на `count` даёт `0.375` для единичного шума, деление на `count · CG²` даёт
1139
- `1.5`.
1140
-
1141
- ```typescript
1142
- import { SynchronousSignalCollectable } from "semantic-typescript";
1143
-
1144
- const round = (value: number): number => Number(value.toFixed(4));
1145
- const tone = SynchronousSignalCollectable.useRate(
1146
- Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1147
- 256,
1148
- );
1149
-
1150
- round(tone.amplitude()[32]); // 1
1151
- round(tone.power()[32]); // 0.5
1152
- round(tone.power().reduce((sum, power) => sum + power, 0)); // 0.75
1153
- round(tone.powerSpectralDensity(64n, 32n).totalPower()); // 0.5
1154
- tone.powerSpectralDensity(64n, 32n).getSegments(); // 7n
1155
- ```
1156
-
1157
- Пара итогов — та проверка, которую стоит запомнить: сумма в пиковой конвенции равна `0.75`, а
1158
- плотность интегрируется в `0.5`, и отношение равно ровно шумовой полосе. К тому же плотность —
1159
- единственная из трёх, кто выживает при смене длины кадра.
1160
-
1161
- ### 14.4 Уровни: две конвенции децибела
1162
-
1163
- Амплитуда и мощность не делят множитель, значит, не делят и логарифм. `amplitudeDecibel`
1164
- применяет `20·log10` и годится для напряжения, смещения или звукового давления;
1165
- `powerDecibel` применяет `10·log10` и годится для мощности, энергии, среднего квадрата или
1166
- плотности. Имена методов сигнала следуют тому же разделению.
1167
-
1168
- ```typescript
1169
- import { amplitudeDecibel, powerDecibel, SynchronousSignalCollectable } from "semantic-typescript";
1170
-
1171
- const round = (value: number): number => Number(value.toFixed(4));
1172
- const tone = SynchronousSignalCollectable.useRate(
1173
- Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1174
- 256,
1175
- );
1176
-
1177
- amplitudeDecibel(0.25); // -12.0412
1178
- powerDecibel(0.25); // -6.0206
1179
- round(tone.decibel()[32]); // 0
1180
- round(tone.powerDecibel()[32]); // -3.0103
1181
- ```
1182
-
1183
- Оба отсчёта одного тона верны: его амплитуда равна единице (ноль децибел), а средний квадрат —
1184
- половине (минус 3,01 децибела). Если подать мощность в амплитудную конвенцию, уровень в
1185
- децибелах удвоится, и правильные минус 6,02 станут минус 12,04.
1186
-
1187
- ### 14.5 Одностороннее свёртывание
1188
-
1189
- Вещественный сигнал удовлетворяет `X[count − k] = conj(X[k])`, поэтому половина отрицательных
1190
- частот не несёт независимой информации. Свёртывание удваивает каждый внутренний бин и оставляет
1191
- два самосопряжённых бина — DC и, при чётной длине, Найквиста — ровно такими, какими их создало
1192
- преобразование. Эти два — собственное зеркало; удвоение изобретает энергию и даёт ровно на 6,02
1193
- децибела больше.
1194
-
1195
- ```typescript
1196
- import { SynchronousSignalCollectable } from "semantic-typescript";
1197
-
1198
- const round = (value: number): number => Number(value.toFixed(4));
1199
- const edge = SynchronousSignalCollectable.useRate(
1200
- Array.from({ length: 16 }, (_unused, index) => 2 + Math.cos(Math.PI * index)),
1201
- 16,
1202
- );
1203
- const bins = edge.oneSided();
1204
-
1205
- bins.length; // 9
1206
- round(bins[0].magnitude()); // 16
1207
- round(bins[8].magnitude()); // 8
1208
- round(edge.amplitude()[0]); // 2
1209
- round(edge.amplitude()[8]); // 1
1210
- ```
1211
-
1212
- Сырые величины равны `16` и `8`, потому что веса окна суммируются в восемь; деление на
1213
- `count · CG = 8` превращает их в калиброванные амплитуды `2` и `1`. Удвоение краевых бинов дало
1214
- бы `4` и `2`, и эта ошибка никогда не вызвала бы исключения.
1215
-
1216
- ### 14.6 Время-частотные представления
1217
-
1218
- `shortTimeFourierTransform` и `spectrogram` нарезают запись так же, как эталонное
1219
- кратковременное преобразование: по половине кадра нулей с каждого конца, затем хвост,
1220
- дополненный нулями до целого числа шагов. Первый кадр центрирован на первом отсчёте, последний
1221
- достигает последнего отсчёта, а кадр `m` центрирован на `m · hop / rate` секундах. Сохранение
1222
- только полных кадров выбросило бы до `frameSize − 1` отсчётов — для 1000 отсчётов, кадра 256 и
1223
- шага 128 это 104 отсчёта, около десятой части записи.
1224
-
1225
- ```typescript
1226
- import { SynchronousSignalCollectable } from "semantic-typescript";
1227
-
1228
- const tone = SynchronousSignalCollectable.useRate(
1229
- Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 4 * index / 64)),
1230
- 64,
1231
- );
1232
- const spectrogram = tone.spectrogram(16n, 8n);
1233
-
1234
- spectrogram.size(); // 9
1235
- spectrogram.getFrameSize(); // 16
1236
- spectrogram.getBoundary(); // "zeros"
1237
- spectrogram.getTime(0); // 0
1238
- spectrogram.getTime(6); // 0.75
1239
- tone.shortTimeFourierTransform(16n, 8n).length; // 9
1240
- tone.spectralFlux(16n, 8n).length; // 8
1241
- ```
1242
-
1243
- Каждый столбец — это `Spectrum` на `frameSize` бинов, ровно то, что дал бы `spectrum()` для того
1244
- же кадра, а `getTime()` сообщает центры кадров, а не их начала.
1245
-
1246
- ### 14.7 Окна, когерентное усиление и COLA
1247
-
1248
- У каждой формы две версии. Симметричная делит на `count − 1`; периодическая (чётная по ДПФ) делит
1249
- на `count`, на один отсчёт меньше полного периода, поэтому окно непрерывно при периодическом
1250
- продолжении, которое предполагает ДПФ. Анализу и суммированию с перекрытием нужна
1251
- периодическая форма; проектирование фильтров сохраняет симметричную.
1252
-
1253
- Разница измерима и никогда не вызывает ошибки. При пятидесяти процентах перекрытия
1254
- периодическое окно Ханна суммируется в единицу в каждой позиции; симметричная форма пульсирует:
1255
- `0.84` децибела при шестнадцати отсчётах, `0.21` при шестидесяти четырёх и всё ещё `0.013` при
1256
- тысяче двадцати четырёх. Класс предоставляет когерентное усиление и эквивалентную шумовую полосу
1257
- своего окна, поэтому поправка никогда не зашита: симметричный Ханн при восьми отсчётах читается
1258
- как `0.4375`, а не `0.5`.
1259
-
1260
- ```typescript
1261
- import {
1262
- coherentGain, equivalentNoiseBandwidth, useFlatTopWindow,
1263
- useHannPeriodicWindow, useHannWindow,
1264
- } from "semantic-typescript";
1265
-
1266
- const round = (value: number): number => Number(value.toFixed(4));
1267
-
1268
- round(useHannWindow(1, 4)); // 0.75
1269
- round(useHannPeriodicWindow(1, 4)); // 0.5
1270
- coherentGain(useHannPeriodicWindow, 16); // 0.5
1271
- equivalentNoiseBandwidth(useHannPeriodicWindow, 16); // 1.5
1272
- useFlatTopWindow(0, 1024); // -0.000421
1273
- Array.from({ length: 1024 }, (_unused, i) => useFlatTopWindow(i, 1024)).filter(w => w < 0).length; // 578
1274
- ```
1275
-
1276
- Строка flat-top — не ошибка таблицы. Окно flat-top по определению частично отрицательно — здесь
1277
- 578 из 1024 весов, — и обрезание этих весов до нуля исказило бы главный лепесток и уничтожило
1278
- бы малые потери scalloping, ради которых окно существует. Библиотека возвращает ряд без
1279
- изменений.
1280
-
1281
- ### 14.8 Ловушки
1282
-
1283
- **Дополнение нулями интерполирует, но никогда не разрешает.** Длина преобразования больше числа
1284
- отсчётов лишь плотнее дискретизирует ту же ДПФ-непрерывную функцию: *положение* пика уточняется,
1285
- а шаг бинов становится `rate / size`, но способность разделить две компоненты по-прежнему
1286
- определяется длиной и формой окна. Тон ровно между двумя бинами читается как `82.1093` и
1287
- `80.8598` на сетке без дополнения и как `128` на своей частоте на дополненной сетке.
1288
-
1289
- ```typescript
1290
- import { SynchronousSignalCollectable, useRectangularWindow } from "semantic-typescript";
1291
-
1292
- const round = (value: number): number => Number(value.toFixed(4));
1293
- const tone = SynchronousSignalCollectable.useRate(
1294
- Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 100.5 * index / 256)),
1295
- 256,
1296
- );
1297
- const flat = tone.spectrum(256n, useRectangularWindow);
1298
- const padded = tone.spectrum(1024n, useRectangularWindow);
1299
-
1300
- flat.getResolution().getHertz(); // 1
1301
- padded.getResolution().getHertz(); // 0.25
1302
- round(flat.magnitudeAt(100).get()); // 82.1093
1303
- round(flat.magnitudeAt(101).get()); // 80.8598
1304
- round(padded.magnitudeAt(402).get()); // 128
1305
- padded.frequencyAt(402).get().getHertz(); // 100.5
1306
- ```
1307
-
1308
- Два равных тона с разносом `0.4` бина остаются слитыми при любом дополнении: при записи из 256
1309
- точек и окне Ханна дополнение до 8192 даёт один локальный максимум, а не два.
1310
-
1311
- **Преобразование Гильберта циклично.** `analyticSignal`, `envelope` и `instantaneousPhase`
1312
- считают первый и последний отсчёты соседями, поэтому запись, период которой не делит её длину,
1313
- загрязнена на обоих концах. Середина записи надёжна, края — нет.
1314
-
1315
- ```typescript
1316
- import { SynchronousSignalCollectable } from "semantic-typescript";
1317
-
1318
- const round = (value: number): number => Number(value.toFixed(4));
1319
- const edge = SynchronousSignalCollectable.useRate(
1320
- Array.from({ length: 512 }, (_unused, index) => Math.cos(2 * Math.PI * 8.3 * index / 512)),
1321
- 512,
1322
- );
1323
- const envelope = edge.envelope();
1324
-
1325
- round(envelope.slice(8, -8).reduce((sum, value) => sum + value, 0) / 496); // 1.0025
1326
- round(Math.max(...envelope.slice(0, 8))); // 1.2592
1327
- round(Math.min(...envelope.slice(-8))); // 0.1314
1328
- ```
1329
-
1330
- **Исчезающая огибающая не определяет фазу.** Там, где огибающая рушится, разность двух углов
1331
- шума округления может достигать пол-оборота, что читается как половина частоты дискретизации.
1332
- Порог — именно то, что сохраняет числу смысл, и по умолчанию он равен `1E-3` от пиковой
1333
- огибающей.
1334
-
1335
- ```typescript
1336
- import { SynchronousSignalCollectable } from "semantic-typescript";
1337
-
1338
- const round = (value: number): number => Number(value.toFixed(4));
1339
- const beat = SynchronousSignalCollectable.useRate(
1340
- Array.from({ length: 4096 }, (_unused, index) =>
1341
- Math.cos(2 * Math.PI * 100 * index / 2048) + Math.cos(2 * Math.PI * 102 * index / 2048)),
1342
- 2048,
1343
- );
1344
-
1345
- round(Math.max(...beat.instantaneousFrequency(2048, 0).map(Math.abs))); // 920.218
1346
- round(Math.max(...beat.instantaneousFrequency().map(Math.abs))); // 101
1347
- ```
1348
-
1349
- **Ненормированная корреляция — не мера сходства.** Сырая сумма `r[k]` растёт с длиной записи,
1350
- поэтому две длины несравнимы. Правило по умолчанию `"coefficient"` делит на `r[0]` и держит все
1351
- значения в `[-1, 1]`; `"unbiased"` делит на `count − k` и платит дисперсией, растущей с задержкой.
1352
-
1353
- ```typescript
1354
- import { SynchronousSignalCollectable } from "semantic-typescript";
1355
-
1356
- const round = (value: number): number => Number(value.toFixed(4));
1357
- const periodic = SynchronousSignalCollectable.useRate(
1358
- Array.from({ length: 1024 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 1024)),
1359
- 1024,
1360
- );
1361
- const rho = periodic.autocorrelation(200n);
1362
-
1363
- round(rho[0]); // 1
1364
- round(rho[128]); // 0.875
1365
- round(rho[64]); // -0.9375
1366
- round(periodic.autocorrelation(200n, "none")[0]); // 512
1367
- ```
1368
-
1369
- Ещё один отсчёт заслуживает предупреждения: `spectralFlux` нормирует каждый кадр перед
1370
- сравнением, поэтому чистое изменение уровня даёт нулевой поток. Его первое и последнее значения —
1371
- краевые артефакты нулевой границы, а не атаки.
1372
-
1373
- ---
1374
-
1375
- <a id="15-license"></a>
1376
- ## 15. Лицензия
1377
-
1378
- MIT © Eloy Kim
1
+ # **Semantic-TypeScript**
2
+
3
+ **Поток, проиндексированный.** Ваши данные под точным контролем.
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.es.md) · Русский
6
+
7
+ Библиотека типобезопасной потоковой обработки, объединяющая **генераторы JavaScript**, **Java Stream** и
8
+ **адресацию данных по индексу**. Каждый элемент несёт `bigint`-индекс, каждый конвейер ленив, и
9
+ каждая терминальная операция — явный выбор.
10
+
11
+ ```bash
12
+ npm install semantic-typescript
13
+ ```
14
+
15
+ - Нулевые зависимости времени выполнения, чистый ESM, поставляется со своими типами.
16
+ - 684 регрессионных тестов: `npm test`.
17
+ - Полная история изменений: [CHANGELOG.md](./CHANGELOG.md).
18
+
19
+ ---
20
+
21
+ ## Оглавление
22
+
23
+ 1. [Философия проектирования](#1-design-philosophy)
24
+ 2. [Архитектура](#2-architecture)
25
+ 3. [Быстрый старт](#3-quick-start)
26
+ 4. [Фабричные функции](#4-factory-functions)
27
+ 5. [Операторы преобразования](#5-transformation-operators)
28
+ 6. [Коллектораблы и терминальные операции](#6-collectables-and-terminal-operations)
29
+ 7. [Статистика и окна](#7-statistics-and-windows)
30
+ 8. [Модель индексов](#8-the-index-model)
31
+ 9. [Асинхронная модель](#9-the-asynchronous-model)
32
+ 10. [Периодический анализ: кольца, поля и оценки](#10-periodic-analysis-rings-fields-and-scores)
33
+ 11. [Вспомогательные функции](#11-utility-helpers)
34
+ 12. [Производительность и подводные камни](#12-performance-and-pitfalls)
35
+ 13. [Фазовые кольца: точный цикл](#13-phase-rings)
36
+ 14. [Анализ сигналов: окна, спектры и время](#14-signal-analysis)
37
+ 15. [Лицензия](#15-license)
38
+
39
+ ---
40
+
41
+ <a id="1-design-philosophy"></a>
42
+ ## 1. Философия проектирования
43
+
44
+ Три идеи, один конвейер:
45
+
46
+ **Индекс — это данные.** Каждый элемент адресуется через `bigint`. Обращение, ротация, перемешивание и
47
+ разбиение — это индексная арифметика, а не операции с буфером. Ничего не материализуется, пока этого не
48
+ потребует коллекторабл.
49
+
50
+ **Лень — значение по умолчанию.** Фабрика оборачивает генератор; оператор оборачивает другой генератор.
51
+ Ни один элемент не запрашивается, пока не выполнится терминальная операция, поэтому бесконечный источник
52
+ вполне допустим — лишь бы что-то ниже по потоку его остановило.
53
+
54
+ **Терминалы явны.** `map` возвращает `Semantic`, а не массив. Значения появляются только тогда, когда вы
55
+ называете коллекторабл — `toUnordered()`, `toOrdered()`, `toNumericStatistics()`, `toWindow()` — и каждый
56
+ из них сообщает, чего он стоит и какой порядок порождает.
57
+
58
+ Та же дисциплина действует и в математической половине библиотеки. `Complex`, `Angle` или
59
+ `Frequency` — это неизменяемое значение, инварианты которого проверяются при построении; `Optional`
60
+ заменяет `null`; а фазовое кольцо точно сообщает, сколько элементов оно содержит, какой у него период и
61
+ насколько оно сбалансировано.
62
+
63
+ ---
64
+
65
+ <a id="2-architecture"></a>
66
+ ## 2. Архитектура
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
+ | Слой | Модуль | Ответственность |
76
+ | :--- | :--- | :--- |
77
+ | Источники | `synchronous/collectors`, `asynchronous/collectors` | Оборачивают итерируемое, генератор, промис, блоб, цель событий |
78
+ | Конвейеры | `synchronous/semantic`, `asynchronous/semantic` | Ленивые операторы и переписывание индексов |
79
+ | Коллектораблы | `synchronous/semantic`, `asynchronous/semantic` | Упорядоченные, неупорядоченные, статистические, оконные и пользовательские терминалы |
80
+ | Коллекторы | `synchronous/collector`, `asynchronous/collector` | Примитивы свёртывания, из которых построен каждый терминал |
81
+ | Математика | `mathematics/*` | Углы, комплексы, частоты, спектры, окна, оценки, эпициклы, фазовые кольца |
82
+ | Структуры | `structure/*` | Списки, множества, отображения, деревья, леса, кольца, поля |
83
+ | Ошибки | `structure/error` | Типизированная иерархия `SemanticError` и её брендовые стражи |
84
+
85
+ Синхронное и асинхронное семейства используют один словарь: те же имена операторов, та же семантика
86
+ индексов, те же коллектораблы. Асинхронные варианты просто возвращают промисы и ставят записи в очередь,
87
+ так что чтение, выданное после записи, всегда её наблюдает.
88
+
89
+ ### 2.1 Что этот выпуск перенёс и что сломал
90
+
91
+ Три модуля сменили расположение или форму, и ряд значений по умолчанию изменил смысл. Каждое старое поведение по-прежнему достижимо через явную опцию; ничего не удалено без замены.
92
+
93
+ | Изменение | Раньше | Теперь |
94
+ | :--- | :--- | :--- |
95
+ | Леса | `structure/tree/rootedForest`, `structure/tree/unionFindForest` | `structure/forest/rootedForest`, `structure/forest/unionFindForest`; `tree/index.ts` больше их не реэкспортирует |
96
+ | Ошибки | разрозненные нативные ошибки | иерархия из пятнадцати классов в `structure/error.ts`, по одному брендовому стражу на класс и псевдоним `SemanticRangeError` |
97
+ | Опции оценок | фиксированные определения | `mathematics/quantile.ts` экспортирует `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator` и `VarianceOptions` |
98
+ | `quantile` | Хайндман–Фан тип 1 | тип 7 — R `quantile`, NumPy `linear`, Excel `PERCENTILE.INC` |
99
+ | `skewness` / `kurtosis` | `g1` / `g2` | скорректированные на смещение `G1` / `G2` |
100
+ | `variance` / `standardDeviation` | `ddof = 0` | `ddof = 1`, обёрнутые в `Optional` |
101
+ | `decibel` | одна функция `20·log10` | удалена; `amplitudeDecibel` (20·log10) и `powerDecibel` (10·log10) |
102
+ | `Spectrum.flatness()` | амплитуды по обеим половинам | мощности по односторонней половине |
103
+ | `Spectrum.bandEnergy()` | сырая двусторонняя сумма | по умолчанию односторонняя, со свёрткой сопряжённой половины |
104
+ | `shortTimeFourierTransform` / `spectrogram` | симметричное окно Ханна, только полные кадры | периодическое окно Ханна, `boundary: "zeros"`, `padded: true` |
105
+
106
+ Главы 6, 7 и 10 описывают замены; измеренные последствия — в [CHANGELOG.md](./CHANGELOG.md).
107
+
108
+ ---
109
+
110
+ <a id="3-quick-start"></a>
111
+ ## 3. Быстрый старт
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. Фабричные функции
160
+
161
+ Построение всегда O(1) — фабрика лишь оборачивает генератор. Таблица указывает стоимость
162
+ *потребления* `n` элементов, а также дополнительную память, которая нужна самому источнику.
163
+
164
+ | Фабрика | Сигнатура (результат) | Время (потребление n) | Память (источник) | Примечания |
165
+ | :--- | :--- | :--- | :--- | :--- |
166
+ | `useOf` | `useOf<E>(...elements) → SynchronousSemantic<E>` | O(n) | O(n) | Индексы 0..n−1; spread-аргумент уже материализован |
167
+ | `useFrom` | `useFrom<E>(Iterable<E>) → SynchronousSemantic<E>`<br>`useFrom<E>(AsyncIterable<E>) → AsynchronousSemantic<E>` | O(n) | O(1) | Оборачивает любое итерируемое; итерируется один раз при сборе |
168
+ | `useRange` | `useRange<N>(start, end, step?) → SynchronousSemantic<N>` | O(n) | O(1) | `end` исключается; `step` должен быть ненулевым; `number` и `bigint` оба работают |
169
+ | `useFill` | `useFill<E>(element \| supplier, count) → SynchronousSemantic<E>` | O(n) | O(1) | Поставщик вызывается один раз на элемент |
170
+ | `useEmpty` | `useEmpty<E>() → SynchronousSemantic<E>` | O(1) | O(1) | Нейтральный элемент для `concat` |
171
+ | `useText` | `useText(text, delimiter?)` | O(n) | O(n) для текста | Итерирует **кодовые точки**, поэтому эмодзи и иероглифы CJK остаются единичными элементами |
172
+ | `useAttribute` | `useAttribute<T>(object)` | O(v + e) | O(d + v) | Рекурсивно обходит вложенные объекты с защитой от циклов |
173
+ | `useGenerate` | `useGenerate<E>(supplier, interrupt)` | O(n) × стоимость поставщика | O(1) | **Должен** иметь останавливающий `interrupt` или нижестоящий `limit` |
174
+ | `useIterate` | `useIterate<E>(Iterable<E>)` / `(AsyncIterable<E>)` | O(n) за сборку | O(1) | Воспроизводит итератор при каждой сборке |
175
+ | `useInterval` | `useInterval(period, delay?) → AsynchronousSemantic<number>` | n × period (реальное время) | O(1) | Порождает истекшее время |
176
+ | `usePromise` | `usePromise<T>(promise) → AsynchronousSemantic<T>` | O(1) | O(1) | Один элемент с индексом `0n` |
177
+ | `useBlob` | `useBlob(blob, chunk?) → AsynchronousSemantic<Uint8Array>` | O(размер blob) | O(chunk) на копию | Размер чанка по умолчанию — 64 КиБ |
178
+ | `useSubscription` | `useSubscription(target, subscriber, event) → AsynchronousSemantic<Event>` | один элемент на событие | O(1) | `limit()` / `takeWhile()` определяют границу отписки |
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. Операторы преобразования
194
+
195
+ Каждый оператор возвращает новую `Semantic`; ни один не трогает элемент, пока не запустится коллекторабл.
196
+ Время — это дополнительная стоимость **на элемент**; память — то, что оператор удерживает, пока идёт конвейер.
197
+
198
+ | Оператор | Назначение | Эффект для индекса | Время | Память |
199
+ | :--- | :--- | :--- | :--- | :--- |
200
+ | `map(mapper)` | 1:1 преобразование | передаётся | O(1) | O(1) |
201
+ | `filter(predicate)` | оставляет подходящие элементы | передаётся | O(1) | O(1) |
202
+ | `flat(mapper)` / `flatMap(mapper)` | 1:N развёртывание | переиндексация с 0 | O(1) на порождённый элемент | O(1) |
203
+ | `distinct(keyExtractor?)` | удаляет повторяющиеся ключи, оставляет первый | передаётся | O(1) в среднем | O(n) |
204
+ | `peek(consumer)` | побочный эффект, элемент проходит сквозь | передаётся | O(1) | O(1) |
205
+ | `limit(count)` | берёт первые *n* | переиндексация с 0 | O(1) | O(1) |
206
+ | `skip(count)` | пропускает первые *n* | передаётся | O(1) | O(1) |
207
+ | `sub(start, end)` | позиционный срез `[start, end)` | передаётся | O(1) | O(1) |
208
+ | `takeWhile(predicate)` | берёт, пока предикат истинен | передаётся | O(1) | O(1) |
209
+ | `dropWhile(predicate)` | пропускает, пока предикат истинен | переиндексация с 0 | O(1) | O(1) |
210
+ | `concat(other)` | добавляет семантику или итерируемое | продолжает индекс | O(1) | O(1) |
211
+ | `redirect(redirector)` | вычисляет новый индекс как угодно | **ваш** | O(1) | O(1) |
212
+ | `reverse()` | `redirect((_e, i) => -i - 1n)` | инвертирован и смещён | O(1) | O(1) |
213
+ | `translate(offset \| mapper)` | сдвигает каждый индекс | `i + offset` | O(1) | O(1) |
214
+ | `shuffle(mapper?)` | рандомизирует индекс | случайный | O(1) | O(1) |
215
+ | `pipe(conversion)` | передаёт генератор пользовательской функции | на ваше усмотрение | ваш | ваш |
216
+ | `semantic()` | поднимает коллекторабл обратно в семантику | неизменён | O(1) | O(1) |
217
+
218
+ **`reverse`, `translate` и `shuffle` только переписывают индексы.** Ничего не буферизуется и не вращается, пока
219
+ `toOrdered()` не нормализует индексы по модулю количества элементов:
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. Коллектораблы и терминальные операции
232
+
233
+ ### 6.1 Коллектораблы
234
+
235
+ Коллекторабл владеет и **порядком**, и **учётом индексов**. `n` — это количество элементов.
236
+
237
+ | Коллекторабл | Структура | Время | Порядок | Используйте когда |
238
+ | :--- | :--- | :--- | :--- | :--- |
239
+ | `toUnordered()` | `Map<bigint, E>` с индексом в качестве ключа | O(n) | порядок вставки | максимальная пропускная способность; сырые индексы сохраняются нетронутыми |
240
+ | `toOrdered()` | массив, отсортированный по индексу элемента | O(n log n) | по нормализованному индексу | обращение или ротация должны вступить в силу |
241
+ | `toOrdered(comparator)` | сортировка по вашему компаратору | O(n log n) | по вашему компаратору | отсортированные представления |
242
+ | `toNumericStatistics()` | буфер, отсортированный по значению | O(n log n) | по значению | статистика по `number` |
243
+ | `toBigintStatistics()` | буфер, отсортированный по значению | O(n log n) | по значению | статистика по `bigint` |
244
+ | `toWindow()` | буфер, отсортированный по значению | O(n log n) | по значению | для `slide` / `tumble` |
245
+ | `toCollectable(mapper)` | то, что вернёт маппер | — | — | пользовательские коллектораблы |
246
+
247
+ ### 6.2 Терминалы
248
+
249
+ Каждый терминал живёт на **коллекторабле**, а не на `Semantic`. Именно это и означает «терминалы явные» на практике: конвейер ничего не производит, пока не назван коллекторабл, а `toCollectable()` без маппера — самый дешёвый способ его назвать.
250
+
251
+ | Терминал | Возвращает | Время | Примечания |
252
+ | :--- | :--- | :--- | :--- |
253
+ | `toArray()` / `toSet()` | `Array<E>` / `Set<E>` | O(n) | Опустошает буфер; `toSet` дедуплицирует по идентичности |
254
+ | `toMap(key, value?)` | `Map<K, V>` | O(n) в среднем | Побеждает последняя запись |
255
+ | `count()` / `isEmpty()` | `bigint` / `boolean` | O(1) | Буфер уже знает |
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) | Пустой поток → пустой `Optional` |
259
+ | `group(classifier)` / `groupBy(key, value)` | `Map<K, E[]>` | O(n) в среднем | — |
260
+ | `partition(count)` | `Array<Array<E>>` | O(n) | Round-robin по индексу |
261
+ | `partitionBy(classifier)` | `Array<Array<E>>` | O(n) | Номер бакета берётся из классификатора |
262
+ | `anyMatch` / `allMatch` / `nonMatch` | `boolean` | O(n) в худшем, короткое замыкание | — |
263
+ | `findAny` / `findFirst` / `findLast` | `Optional<E>` | O(1)–O(n) | Позиционные в материализованном порядке |
264
+ | `findAt(k)` / `findNegativeAt(k)` | `Optional<E>` | O(k) / O(n−k) | Считает с начала / с конца |
265
+ | `findMaximum(cmp?)` / `findMinimum(cmp?)` | `Optional<E>` | O(n) | По умолчанию — структурное сравнение |
266
+ | `frequency()` | `Map<E, bigint>` | O(n) в среднем | Только коллектораблы статистики |
267
+ | `write(stream, accumulator?)` | `WritableStream<S>` | O(n) | Терминал, подобный IO |
268
+ | `collect(collector)` | `R` | O(n) | Общее свёртывание, лежащее в основе каждого терминала выше |
269
+
270
+ ### 6.3 Структурные коллекторы
271
+
272
+ `toArray`, `toSet` и `toMap` завершают конвейер контейнером JavaScript. Ещё двенадцать терминалов завершают его одной из собственных структур библиотеки, так что результат несёт инварианты, которые гарантирует эта структура, а не те, что случайно есть у массива. Они доступны на каждом коллекторабле, включая голый коллекторабл из `toCollectable()`.
273
+
274
+ | Терминал | Возвращает | Порядок | Стоимость |
275
+ | :--- | :--- | :--- | :--- |
276
+ | `toArray()` | `Array<E>` | порядок обхода | O(n) |
277
+ | `toSet()` | `Set<E>` | порядок вставки, SameValueZero | O(n) в среднем |
278
+ | `toMap(key, value?)` | `Map<K, V>` | порядок вставки; побеждает последняя запись | O(n) в среднем |
279
+ | `toOrderedSet(comparator?)` | `SynchronousOrderedSet<E>` | по возрастанию компаратора | O(n²) в худшем случае |
280
+ | `toUnorderedSet()` | `SynchronousUnorderedSet<E>` | не определён (хеш-бакеты) | O(n) ожидаемо |
281
+ | `toOrderedMap(key, value?)` | `SynchronousOrderedMap<K, V>` | порядок вставки; побеждает последняя запись | O(n) ожидаемо |
282
+ | `toUnorderedMap(key, value?)` | `SynchronousUnorderedMap<K, V>` | не определён | O(n) ожидаемо |
283
+ | `toList()` | `SynchronousList<E>` | порядок обхода, дубликаты сохраняются | O(n) |
284
+ | `toRing(span?)` | `SynchronousRing<E>` | порядок обхода задаёт позиции на кольце | O(n) |
285
+ | `toLRUSet(capacity)` | `SynchronousUnorderedLRUSet<E>` | по давности; выживают только последние `capacity` | O(n²) в худшем случае |
286
+ | `toLRUMap(capacity, key, value?)` | `SynchronousUnorderedLRUMap<K, V>` | по давности | O(n) ожидаемо |
287
+ | `toSkipSet(comparator?)` | `SynchronousSkipSet<E>` | по возрастанию компаратора, с навигацией | O(n²) в худшем случае |
288
+ | `toSkipMap(key, value?)` | `SynchronousSkipMap<K, V>` | по компаратору ключей, с навигацией | O(n) ожидаемо |
289
+ | `toBinarySearchTree(comparator?)` | `SynchronousBinarySearchTree<E>` | по возрастанию компаратора, сбалансировано по высоте | O(n log n) |
290
+ | `toRootedForest()` | `SynchronousRootedForest<E>` | порядок обхода становится структурой | O(n) |
291
+
292
+ Тип результата следует за типом элемента самого коллекторабла, поэтому ни один терминал не принимает аргумент типа: `SynchronousCollectable<E>` сразу отдаёт `Array<E>`, `SynchronousList<E>` или `SynchronousOrderedSet<E>`. Остаются только деловые аргументы — компаратор `toOrderedSet` / `toSkipSet` / `toBinarySearchTree`, ёмкость пары LRU, диапазон `toRing` и извлекатели ключа и значения семейства отображений.
293
+
294
+ Кольцо — это множество, поэтому повторяющиеся элементы сохраняют первую позицию; все остальные структуры выше сохраняют дубликаты, если их собственный контракт не говорит иного (`toOrderedSet`, `toUnorderedSet` и `toSkipSet` — множества; `toBinarySearchTree` и `toList` — нет).
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
+ Асинхронные коллектораблы предоставляют те же терминалы и разрешают каждый в promise, поэтому форма вызова приобретает только `await`.
310
+
311
+ ---
312
+
313
+ <a id="7-statistics-and-windows"></a>
314
+ ## 7. Статистика и окна
315
+
316
+ ### 7.1 Поверхность статистики
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
+ Оба статистических семейства предоставляют одну и ту же поверхность — `summate`, `average`, `median`, `mode`, `variance`, `standardDeviation`, `frequency`, `findMaximum`, `findMinimum` — с `bigint`-арифметикой для `toBigintStatistics()`. Меры разброса и семейство квантилей возвращают `Optional`, потому что выборка может оказаться слишком мала для их определения; извлекайте их через `.get()`, `.orElse(…)` или `.isPresent()`.
328
+
329
+ ### 7.2 Оценки и значения по умолчанию
330
+
331
+ Значения по умолчанию — это определения, которые используют R, NumPy, pandas и Excel, поэтому вычисленное здесь значение можно сравнивать с этими инструментами без поправочного коэффициента.
332
+
333
+ | Статистика | По умолчанию | Раньше | Вернуть старое значение через |
334
+ | :--- | :--- | :--- | :--- |
335
+ | `quantile(probability)` | Хайндман–Фан тип 7 | тип 1 | `{ method: "type-1" }` |
336
+ | `interquartileRange()` | Хайндман–Фан тип 7 | тип 1 | `{ method: "type-1" }` |
337
+ | `skewness()` | скорректированный `G1` | `g1` | `{ estimator: "g1" }` |
338
+ | `kurtosis()` | скорректированный избыточный `G2` | `g2` | `{ estimator: "g2" }` |
339
+ | `variance()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
340
+ | `standardDeviation()` | `ddof = 1` | `ddof = 0` | `{ ddof: 0 }` |
341
+
342
+ Все девять типов Хайндмана–Фана доступны как `"type-1"` … `"type-9"`, а `{ excess: false }` возвращает тройку к эксцессу и сообщает неизбыточный четвёртый стандартизованный момент. Типы опций — `QuantileMethod`, `QuantileOptions`, `SkewnessEstimator`, `KurtosisEstimator`, `VarianceOptions` — экспортируются из `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` и `G2` не определены при менее чем трёх и четырёх наблюдениях, а выборочные дисперсия и стандартное отклонение — при менее чем двух при `ddof = 1`. Это сообщается пустым `Optional`, а не вводящим в заблуждение нулём:
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
+ Представления bigint принимают те же опции и округляют каждый результат до целого `bigint`.
371
+
372
+ ### 7.3 Окна
373
+
374
+ Окна заново выставляют буферизованные элементы как конвейер конвейеров, поэтому окно можно потребить, не материализуя его сначала в массив. Обе формы включают последнее частичное окно.
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. Модель индексов
388
+
389
+ Индекс — это `bigint`, прикреплённый к каждому элементу. Операторы либо **передают** его, либо
390
+ **переиндексируют** с нуля, либо позволяют вам его **вычислить**.
391
+
392
+ Упорядоченный сбор нормализует каждый индекс по модулю количества элементов, а затем сортирует по результату.
393
+ Именно поэтому `translate(7)` на пяти элементах эквивалентен `translate(2)`: `7 ≡ 2 (mod 5)`.
394
+
395
+ Неупорядоченный сбор сохраняет тот индекс, который произвёл конвейер, и возвращает элементы в порядке
396
+ прибытия. Отрицательные индексы допустимы и после нормализации просто сортируются раньше неотрицательных.
397
+
398
+ ---
399
+
400
+ <a id="9-the-asynchronous-model"></a>
401
+ ## 9. Асинхронная модель
402
+
403
+ `AsynchronousSemantic` повторяет синхронную поверхность, возвращая промисы. Записи ставятся в очередь,
404
+ поэтому чтение, выданное после записи, всегда её наблюдает.
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
+ Фабрики, асинхронные по своей природе, — `usePromise`, `useBlob`, `useInterval`,
415
+ `useSubscription` — возвращают `AsynchronousSemantic` напрямую, а `useFrom` / `useIterate`
416
+ переходят в асинхронный режим, когда им передают `AsyncIterable`.
417
+
418
+ ---
419
+
420
+ <a id="10-periodic-analysis-rings-fields-and-scores"></a>
421
+ ## 10. Периодический анализ: кольца, поля и оценки
422
+
423
+ Математическая половина библиотеки трактует цикл как полноценный объект, а не как таблицу
424
+ поиска. Три конструкции работают вместе.
425
+
426
+ ### 10.1 Кольцо — это одно целое число
427
+
428
+ `Ring` — это **n-кратное кольцо**: количество элементов, позиционный период и число различных
429
+ фаз — одно и то же целое `n`. Фаза позиции вычисляется из её точного остатка, поэтому
430
+ кольцо периодично до бита:
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` — это **окружающая** (ambient) скорость вращения, а не шаг фазы между позициями: среда
445
+ сдвигает каждый фазор (phasor) на один и тот же угол, поэтому она никогда не может нарушить внутренний
446
+ узор. `span` задаёт, сколько оборотов занимает кольцо.
447
+
448
+ `purity()` — это доля энергии, переносимая основной модой,
449
+ `max(|c₁|², |c_{n−1}|²) / (n · Σ|z|²)`. Она равна `1` ровно тогда, когда кольцо с точностью до
450
+ поворота совпадает с корнями `n`-й степени из единицы, и стоит `O(n)`, потому что нужны всего два бина.
451
+ Проверяются оба направления, поскольку кольцо может обходить окружность в любую сторону.
452
+
453
+ ### 10.2 Поле — это ограниченная оценка
454
+
455
+ `PhaseField` отображает относительную фазу в оценку из `[-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
+ Оценка — это усечённый ряд косинусов, `a₀ + Σ wₘ·cos(2πmΔ)`, и конструктор обеспечивает
468
+ `|a₀| + Σ|wₘ| ≤ 1`. По неравенству треугольника оценка никогда не покидает `[-1, 1]` — граница является
469
+ **следствием конструкции**, а не ограничением, наложенным после.
470
+
471
+ Причина, по которой поле является рядом, а не таблицей: таблица из `n` точек точно воспроизводит свои
472
+ собственные отсчёты, но её тригонометрический интерполянт даёт выбросы между ними. Таблица из пяти
473
+ точек со значениями `[+1, +0.5, −1, −0.5, 0]` интерполируется в диапазон `[−1.0428, 1.0041]`, что уже
474
+ нарушает обещание. Ограниченный ряд не может.
475
+
476
+ ### 10.3 Анализ измеряет данные относительно каждого элемента
477
+
478
+ Каждый элемент кольца — это опорная фаза. `analyse` отыскивает каждое наблюдение на кольце и
479
+ измеряет его относительно каждой опоры:
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
+ Каждая строка опоры сообщает `count`, среднюю относительную фазу `mean` и среднюю результирующую длину
491
+ `resultant`. Одно тождество стоит запомнить: **`resultant` одинаков для каждой опоры**,
492
+ потому что смена базовой линии лишь поворачивает все относительные фазы на константу. От опоры к опоре
493
+ различаются только счётчик и среднее.
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 Связанные инструменты
501
+
502
+ | Вызов | Отвечает | Стоимость |
503
+ | :--- | :--- | :--- |
504
+ | `purity()` | остаётся ли кольцо корнями `n`-й степени из единицы? | O(n) |
505
+ | `resultant(t?)` | насколько фазоры сконцентрированы прямо сейчас? | O(n) |
506
+ | `analyse(data)` | как эти данные соотносятся с каждым элементом? | O(n · m) |
507
+ | `toSpectrum()` | кольцо как спектр, волновое число на оси частот | O(n²) |
508
+ | `useAngularSpectrum(ring, instants, plan)` | плоскость `(ω, n)` во времени | O(n² · frames) |
509
+
510
+ Модель дополняют ещё два примитива: `combineRings(a, b)` соединяет два кольца по их наименьшему общему
511
+ кратному (обратный образ, pullback, а не произведение — пары, которые не могут разделить момент,
512
+ просто отсутствуют), а `rate()` даёт каждому элементу свою внутреннюю частоту, чтобы кольцо могло
513
+ дрейфовать относительно среды. `jetAt`, `windingAt` и `errorIntegralAt` показывают дифференциальное состояние дрейфа.
514
+
515
+ ### 10.5 Спектральные окна
516
+
517
+ Построения колец — одна половина аналитической поверхности; другая работает с дискретизированными сигналами. Её самый переиспользуемый примитив — окно, и у каждой формы здесь две версии. Симметричная версия делит кадр на `count − 1`; периодическая (DFT-чётная) делит его на `count`, на один отсчёт меньше полного периода, поэтому спад непрерывен при периодическом продолжении, которое предполагает 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
+ | Форма | Симметричная | Периодическая | Примечания |
527
+ | :--- | :--- | :--- | :--- |
528
+ | Прямоугольная | `useRectangularWindow` | — | без спада; нет периодического различия |
529
+ | Ханна | `useHannWindow` | `useHannPeriodicWindow` | вариант по умолчанию для коротковременного преобразования |
530
+ | Хэмминга | `useHammingWindow` | `useHammingPeriodicWindow` | — |
531
+ | Блэкмана | `useBlackmanWindow` | `useBlackmanPeriodicWindow` | — |
532
+ | Наттолла | `useNuttallWindow` | `useNuttallPeriodicWindow` | четырёхчленный Блэкман–Харрис |
533
+ | Плосковершинная | `useFlatTopWindow` | `useFlatTopPeriodicWindow` | точность амплитуды важнее разрешения |
534
+ | Кайзера | `useKaiserWindow(beta)` | `useKaiserPeriodicWindow(beta)` | `beta` задаёт ширину главного лепестка |
535
+
536
+ Используйте периодическую версию для анализа и overlap-add: при полукадровом шаге периодическое окно Ханна суммируется в `1.000000` при любой длине, тогда как симметричное колеблется на `0.841 dB` при `N = 16` и ещё на `0.013 dB` при `N = 1024`. Симметричную версию оставьте для проектирования фильтров, где кадр не является одним периодом спада.
537
+
538
+ Плосковершинные окна возвращают **отрицательные веса** у краёв кадра, потому что плосковершинное окно именно таково. Они возвращаются без обрезки, поскольку обрезка искажает главный лепесток и уничтожает малые потери на «гребешке», ради которых окно и существует:
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 Уровни, плоскостность и энергия полосы
547
+
548
+ `decibel` исчез как единая функция, потому что амплитуда и мощность не делят один множитель. `amplitudeDecibel` применяет `20·log10` к отношению типа напряжения; `powerDecibel` применяет `10·log10` к мощности, энергии, среднему квадрату или спектральной плотности мощности:
549
+
550
+ ```typescript
551
+ amplitudeDecibel(0.25); // -12.0412
552
+ powerDecibel(0.25); // -6.0206
553
+ ```
554
+
555
+ `Bin.decibel` и `Spectrum.decibelOf` — уровни амплитуды; `Spectrum.powerDecibelOf` — соответствующий уровень мощности. Подача мощности в амплитудную форму сообщает `20·log10(power)` и удваивает уровень в децибелах.
556
+
557
+ Два измерения спектра изменили чтение:
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()` сравнивает геометрическое и арифметическое средние `power()` по односторонней половине спектра, а не среднее амплитуд по обеим половинам, и чисто визуальное удвоение амплитуды из `oneSided()` намеренно не применяется. `bandEnergy()` по умолчанию использует одностороннее чтение, свёртывая сопряжённую половину: DC и Найквист считаются один раз, а каждый внутренний бин — дважды; полоса, покрывающая всю ось, тогда удовлетворяет равенству Парсеваля, `Σ|X|² = N·Σx²`. Передайте `"two-sided"` для комплексного спектра, чьи бины уже независимы.
571
+
572
+ ### 10.7 Коротковременный анализ
573
+
574
+ `Sampler.shortTimeFourierTransform` и `Sampler.spectrogram` следуют соглашениям `stft` из SciPy: периодическое окно Ханна, граница, заполненная нулями с обоих концов, и хвост, заполненный нулями.
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
+ Расширение центрирует первый кадр на первом отсчёте, а заполнение хвоста делает последние отсчёты достижимыми: 64 отсчёта при кадре 16 и шаге 8 дают 9 кадров вместо 7, а 1000 отсчётов при кадре 256 и шаге 128 дают 9 вместо 6. Время кадра — это центр, а не начало: `(k·hop + (frameSize mod 2)/2)/rate` для расширенного представления и `(k·hop + frameSize/2)/rate` для нерасширенного, а `toFrame()` срезает расширение перед обратным преобразованием.
588
+
589
+ ---
590
+
591
+ <a id="11-utility-helpers"></a>
592
+ ## 11. Вспомогательные функции
593
+
594
+ ### 11.1 Иерархия ошибок
595
+
596
+ `structure/error.ts` экспортирует иерархию из пятнадцати классов для ошибок, возбуждаемых *вокруг* конвейера — вашим собственным кодом, собственным коллектором или адаптером:
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
+ Каждый класс фиксирует `name`, пересылает нативную `cause`, несёт необязательный мешок `context` и сериализуется через `toJSON()`; у каждого есть одноимённый брендовый страж.
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` — тот же класс, что и экспортируемый `RangeError`, поэтому `instanceof` выполняется для обоих имён; импорт `RangeError` затеняет глобальный конструктор во всём модуле, и именно поэтому существует псевдоним. Ни один из этих классов не возбуждается собственными проверками аргументов библиотеки — там по-прежнему возбуждаются нативные `TypeError` и `RangeError`; иерархия — это словарь для слоя выше.
629
+
630
+ ### 11.2 Таблица помощников
631
+
632
+ | Помощник | Назначение |
633
+ | :--- | :--- |
634
+ | `useCompare(a, b)` | Тотальное упорядочение для смешанных примитивов |
635
+ | `useToNumber` / `useToBigInt` | Преобразование чисел без потерь с проверкой диапазона |
636
+ | `useRandom` | Случайные помощники с начальным значением или без него |
637
+ | `useTraverse` | Структурный обход вложенных объектов и массивов |
638
+ | `useStringify` | Сериализация JSON, переживающая циклические ссылки |
639
+ | `useHash` / `maskOf` / `register` / `unregister` | Структурное хеширование и реестр стратегий хеша |
640
+ | `invalidate` / `validate` | Стражные проверки, порождающие ошибку единой формы |
641
+ | `Optional<T>` | Замена `null` в библиотеке: `Optional.of(x)` / `Optional.empty()` |
642
+
643
+ ### 11.3 `useTraverse`
644
+
645
+ `useTraverse(target, callback)` разворачивает любой объект в последовательность его **непустых листьев**. Он спускается во вложенные объекты, обходит итерируемые значения по позициям, пропускает `null` и `undefined` и никогда не входит повторно в уже посещённый объект — поэтому граф объектов с циклом завершается, а не переполняет стек.
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
+ Обратный вызов выполняется один раз на лист и получает собственный ключ листа (его индекс, если лист пришёл из итерируемого значения), значение и путь к нему. Возврат `false` прекращает **весь** обход, а не только текущую ветвь:
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
+ Цикл безопасен по той же причине:
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
+ **Каждый обратный вызов получает собственный снимок пути.** Обход держит внутри единственный рабочий массив и в момент передачи листа копирует его в новый массив, замороженный через `Object.freeze`. Снимок после этого не меняется, поэтому его можно сохранять как есть; два обратных вызова никогда не делят один массив, а повторный обход не задевает уже выданные снимки. Его тип — `ReadonlyArray`, поэтому запись в него будет ошибкой компиляции, а у замороженного массива она вдобавок бросит `TypeError` во время выполнения. Такая надёжность стоит одной аллокации `O(depth)` на каждый сообщённый лист; ветвь, не сообщающая ничего, лишнего не выделяет.
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` — это тот же обход в виде фабрики: она сообщает те же листья парами `{ key, value }`, поэтому правила и стоимость совпадают.
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. Производительность и подводные камни
707
+
708
+ **У лени есть цена на каждой ступени.** Каждый оператор добавляет переход через генератор. Для плотных
709
+ числовых циклов по массивам в памяти обычный цикл `for` побьёт любой конвейер; конвейер выигрывает, когда
710
+ источник ленив, бесконечен или конвейер достаточно длинен, чтобы дисциплина индексов окупилась.
711
+
712
+ **`toOrdered()` сортирует; `toUnordered()` — нет.** Если кажется, что обращение «не сработало», коллектор был
713
+ неупорядоченным — переписывание индексов становится порядком только тогда, когда индексы нормализованы.
714
+
715
+ **`useGenerate` без `interrupt` никогда не завершается.** Всегда передавайте останавливающий предикат или
716
+ нижестоящий `limit` / `takeWhile`.
717
+
718
+ **`Ring` — это множество с порядком вставки.** Элементы уникальны и сохраняют порядок, в котором были
719
+ впервые добавлены. Повторный `add` возвращает `false`, а `set`, который ввёл бы дубликат, бросает
720
+ исключение. Именно это позволяет фазе быть функцией *элемента*, а не той позиции, которую ему случилось
721
+ занимать.
722
+
723
+ **`Spectrum` предполагает вещественный сигнал в своих односторонних помощниках.** `oneSided()`, `getNyquist()` и
724
+ `centroid()` предполагают сопряжённую симметрию. Пространственный спектр фазового кольца комплексен и в
725
+ общем случае не симметричен — кольцо, которое обходит окружность в обратную сторону, помещает всю свою
726
+ энергию в бин `n − 1`, за пределы Найквиста. Читайте бины напрямую.
727
+
728
+ **Структурные коллекторы — это терминалы, а не представления.** Каждый опустошает конвейер, а затем строит полную структуру, поэтому стоит O(n) времени и O(n) памяти и не может работать на бесконечном источнике. Используйте его в конце конвейера и никогда внутри цикла по конвейеру.
729
+
730
+ **`variance()` и его собратья возвращают `Optional`.** `variance().get()` на слишком малой выборке бросает исключение; используйте `.orElse(0)` или проверяйте `.isPresent()`, когда размер выборки не в вашей власти. То же относится к `standardDeviation`, `quantile`, `interquartileRange`, `skewness` и `kurtosis`.
731
+
732
+ **Периодические окна — это окна анализа.** `useHannWindow` и другие симметричные формы предназначены для проектирования фильтров; коротковременному преобразованию нужны `useHannPeriodicWindow` и его родня, иначе сумма overlap-add будет колебаться на каждом стыке кадров.
733
+
734
+ ---
735
+
736
+ <a id="13-phase-rings"></a>
737
+ ## 13. Фазовые кольца: точный цикл
738
+
739
+ Глава 10 представила кольцо как модель цикла и использовала его, чтобы измерять набор данных
740
+ относительно каждой фазы. Эта глава даёт полную поверхность: как позиция становится углом, что
741
+ меняет и что не меняет частота среды, и инструменты, читающие кольцо относительно времени,
742
+ данных и другого кольца.
743
+
744
+ `SynchronousRing` — это позиционный список, под которым лежит круговое двусвязным кольцом, у которого
745
+ голова и хвост соединены. Одновременно это *множество* с порядком вставки: ни один элемент не
746
+ появляется дважды, а отклонённая запись не трогает ни связи, ни отсчёты. Кольцом, а не просто
747
+ списком, его делает то, что фаза позиции — чистая функция её остатка по модулю текущей длины;
748
+ поэтому кольцо периодично до бита, и его баланс не может дрейфовать.
749
+
750
+ ### 13.1 Кольцо — это одно целое, вычисленное точно
751
+
752
+ Кольцо несёт `n` элементов, и число элементов, позиционный период и число различных фаз — одно
753
+ и то же целое. Позиция `i` несёт
754
+
755
+ `phase + span · 2π · (((i mod n) + n) mod n) / n`
756
+
757
+ причём остаток сокращается в `bigint`, прежде чем стать `number`. Именно поэтому `phaseAt(i)` и
758
+ `phaseAt(i + n)` *тождественны*, а не просто близки: второй вызов сокращается к тому же целому,
759
+ вычисляет то же произведение и даёт тот же double. Индекс далеко за точным целым диапазоном
760
+ `number` всё равно попадает в нужную позицию, а отрицательный индекс идёт по кольцу назад.
761
+
762
+ | Вызов | Возвращает | Смысл |
763
+ | :--- | :--- | :--- |
764
+ | `new SynchronousRing(source, options?)` | кольцо | `source` — любой итерируемый объект, дубликаты отбрасываются |
765
+ | `getPhase()` / `getFrequency()` / `getSpan()` | `Angle`, `Frequency`, `number` | три параметра конструкции |
766
+ | `size()` | `bigint` | то самое `n`, оно же период |
767
+ | `phaseAt(index)` | `Optional<Angle>` | только собственная фаза; время, скорость и частота игнорируются |
768
+ | `elementAt(index)` | `Optional<RingElement>` | элемент, собственная фаза, частота среды, фазор |
769
+ | `frequencyAt(index)` | `Optional<Frequency>` | скорость среды, пусто для пустого кольца |
770
+ | `toComplex()` / `toComplex(index)` | `Complex[]` / `Optional<Complex>` | собственные фазоры |
771
+
772
+ `RingOptions` добавляет две функции на элемент: `magnitude(element, index)` задаёт длину фазора
773
+ и по умолчанию равна единице, а `rate(element, index)` даёт элементу собственную частоту и по
774
+ умолчанию равна нулю, то есть фиксированному маркеру.
775
+
776
+ ```typescript
777
+ import { SynchronousRing } from "semantic-typescript";
778
+
779
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
780
+
781
+ ring.size(); // 6n
782
+ ring.phaseAt(0n).get().toDegreeValue(); // 0
783
+ ring.phaseAt(1n).get().toDegreeValue(); // 60
784
+ ring.phaseAt(-1n).get().toDegreeValue(); // 300
785
+ ring.phaseAt(6n).get().getValue() === ring.phaseAt(0n).get().getValue(); // true
786
+ ring.phaseAt(10n ** 20n).get().toDegreeValue(); // 240
787
+ ```
788
+
789
+ `phaseAt(-1n)` на кольце из шести элементов равно `300`, а `10n ** 20n` по модулю шести даёт
790
+ `4`, поэтому читается как `240`. Ни один случай не накапливает произведение с плавающей точкой —
791
+ именно это сохраняет тождество точным при смене представления индекса.
792
+
793
+ ### 13.2 Частота среды и состояние в момент времени
794
+
795
+ `frequency` — это **скорость вращения среды**, в которую кольцо погружено, а не шаг фазы между
796
+ позициями. Среда сдвигает фазу каждого элемента на один и тот же угол, поэтому не может изменить
797
+ собственный узор, не может сломать баланс и не может изменить ни один результат `phaseAt`. По
798
+ умолчанию она равна `0 Hz`.
799
+
800
+ `relativeAt(index, time)` вычитает этот общий угол, оставляя координату, по которой оценивается
801
+ `PhaseField`. `viewAt` и `viewsAt` возвращают полное состояние в момент времени, включая
802
+ собственную `rate` элемента, а дифференциальные аксессоры вскрывают дрейф:
803
+
804
+ | Вызов | Формула | Читает |
805
+ | :--- | :--- | :--- |
806
+ | `relativeAt(index, time)` | `собственная + rate · t − среда · t` | фазу, которую видит среда |
807
+ | `viewAt(index, time)` / `viewsAt(time)` | то же плюс элемент, индекс, время, rate, фазор | полное состояние |
808
+ | `jetAt(index, time)` | фаза в `t`, своя rate, `slip = rate − среда` | дифференциальное состояние |
809
+ | `windingAt(index, time)` | `trunc(slip · t)` | целые обороты скольжения, усечённые к нулю |
810
+ | `errorIntegralAt(index, time, origin?)` | `∫ относительной фазы dt`, в оборотах · секундах | как долго элемент опережает |
811
+ | `resultant(time?)` | `abs(Σ w · e^{iθ}) / Σ w` | концентрация фаз, не зависит от вращения среды |
812
+ | `analyse(data, time?)` | счётчики и средние по каждой опоре | данные, измеренные относительно каждого элемента |
813
+ | `scoreAt(index, time, field)` / `scoresAt(time, field)` | поле, выбранное по относительной фазе | ограниченная оценка на позицию |
814
+
815
+ ```typescript
816
+ import { SynchronousRing, useHertz } from "semantic-typescript";
817
+
818
+ const moving = new SynchronousRing(["a", "b", "c", "d", "e", "f"], {
819
+ frequency: useHertz(1),
820
+ });
821
+
822
+ moving.getFrequency().getHertz(); // 1
823
+ moving.phaseAt(1n).get().toDegreeValue(); // 60 — intrinsic
824
+ moving.relativeAt(1n, 0.25).get().toDegreeValue(); // 330 — 60 − 90
825
+ moving.viewAt(1n, 0.25).get().relative.toDegreeValue(); // 330
826
+ moving.jetAt(1n, 0.25).get().slip; // -1
827
+ moving.purity(); // 1 — rotation cannot break it
828
+ ```
829
+
830
+ При `1 Hz` среда за `0.25 s` проходит четверть оборота, поэтому относительная фаза позиции один
831
+ равна `60 − 90 = 330` градусам, а собственная фаза остаётся `60`. Джет сообщает `slip` `-1` герц,
832
+ потому что сам элемент не движется, а `purity()` по-прежнему `1`: поворот всех фазоров на один
833
+ угол не является деформацией.
834
+
835
+ ### 13.3 Чистота и баланс
836
+
837
+ `purity()` — доля энергии кольца, которую несёт основная мода:
838
+
839
+ `max(abs(c₁)², abs(c_{n−1})²) / (n · Σ abs(z)²)`
840
+
841
+ Проверяются оба основных бина, потому что кольцо может обходить круг в любую сторону. Кольцо с
842
+ `span: 5` на шести элементах шагает на `300` градусов, то есть на `−60`: оно идёт назад, кладёт
843
+ всю энергию в `c₅`, и формула, смотрящая только на `c₁`, объявила бы его нечистым. Стоимость
844
+ остаётся `O(n)`, потому что нужны лишь эти два бина и полная энергия.
845
+
846
+ Граничные случаи — часть контракта. Кольцо из менее чем двух элементов не имеет основной моды и
847
+ сообщает `purity() === 0` и `isBalanced() === false` независимо от содержимого. Кольцо, у
848
+ которого все фазоры нулевой длины, не имеет энергии и тоже сообщает ноль. `isBalanced(epsilon?)`
849
+ равно `1 − purity() <= epsilon` с допуском `1E-9` по умолчанию.
850
+
851
+ ```typescript
852
+ import { SynchronousRing } from "semantic-typescript";
853
+
854
+ const forward = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
855
+ const backward = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 5 });
856
+ const weighted = new SynchronousRing(["a", "b", "c", "d"], {
857
+ magnitude: element => element === "a" ? 1 : 0,
858
+ });
859
+
860
+ forward.purity(); // 1
861
+ backward.phaseAt(1n).get().toDegreeValue(); // 300 — it runs backwards
862
+ backward.purity(); // 1 — the energy sits in c₅
863
+ new SynchronousRing(["only"]).purity(); // 0
864
+ new SynchronousRing(["only"]).isBalanced(); // false
865
+ weighted.purity(); // 0.25
866
+ ```
867
+
868
+ ### 13.4 Результирующая длина и симметричные компоненты
869
+
870
+ `resultant(time?)` — средняя результирующая длина: модуль суммы фазоров, делённый на сумму
871
+ модулей. В отличие от `purity`, спрашивающей, является ли кольцо по-прежнему корнями `n`-й
872
+ степени из единицы, `resultant` спрашивает, насколько фазоры сосредоточены сейчас. Сбалансированное
873
+ кольцо сообщает `0`; кольцо, все элементы которого имеют один угол, сообщило бы `1`, но семантика
874
+ множества запрещает дубликаты, поэтому на практике `1` появляется, когда `magnitude` обнуляет все
875
+ элементы, кроме одного.
876
+
877
+ `zeroSequence()` — средний фазор, то есть `c₀ / n`. `sequences()` возвращает все `n` симметричных
878
+ компонент, начиная с низшего порядка, прямым преобразованием. `toSpectrum()` упаковывает их в
879
+ `Spectrum`, бин `j` которого сообщает `j` циклов на оборот кольца, так что ось частот — это ось
880
+ волновых чисел.
881
+
882
+ ```typescript
883
+ import { SynchronousRing } from "semantic-typescript";
884
+
885
+ const round = (value: number): number => Number(value.toFixed(4));
886
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
887
+
888
+ round(ring.resultant().get()); // 0
889
+ round(ring.zeroSequence().get().magnitude()); // 0
890
+ ring.sequences().map(component => round(component.magnitude())); // [0, 6, 0, 0, 0, 0]
891
+ ring.toSpectrum().get().size(); // 6
892
+ ring.toSpectrum().get().getResolution().getHertz(); // 1
893
+ ```
894
+
895
+ Компоненты комплексны и вообще **не сопряжённо-симметричны**: сбалансированное кольцо кладёт всю
896
+ энергию в бин один, а идущее назад — в бин `n − 1`. Односторонние помощники `Spectrum`
897
+ предполагают вещественный сигнал; читайте бины напрямую, а не сворачивайте их.
898
+
899
+ ### 13.5 Анализ данных относительно каждого элемента
900
+
901
+ `analyse(data, time?)` сопоставляет каждое наблюдение кольцу по тождеству и измеряет его
902
+ относительно каждого элемента как опорной фазы. Наблюдения, которых кольцо не содержит,
903
+ считаются `missed`, а не угадываются, потому что кольцо — единственный источник фаз. Каждая
904
+ строка опоры сообщает `count`, среднюю относительную фазу `mean` и среднюю результирующую длину
905
+ `resultant`.
906
+
907
+ Одно тождество стоит запомнить: **`resultant` одинаков для каждой опоры**. Смена базовой линии
908
+ поворачивает все относительные фазы на константу, а поворот не может изменить результирующую
909
+ длину. От опоры к опоре различаются счётчик и среднее.
910
+
911
+ ```typescript
912
+ import { SynchronousRing } from "semantic-typescript";
913
+
914
+ const round = (value: number): number => Number(value.toFixed(4));
915
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e"]);
916
+ const report = ring.analyse(["a", "c", "c", "a", "zz", "e"]);
917
+
918
+ report.matched; // 5
919
+ report.missed; // 1
920
+ report.references.map(row => row.count); // [2, 0, 2, 0, 1]
921
+ report.references.map(row => round(row.resultant)); // [0.1453, 0.1453, 0.1453, 0.1453, 0.1453]
922
+ ```
923
+
924
+ Оценка — обратная сторона той же монеты. `PhaseField` — ограниченный ряд косинусов (раздел
925
+ 10.2), и `scoreAt`/`scoresAt` выбирают его по относительной фазе, так что одно и то же кольцо
926
+ можно оценивать относительно выравнивания, гармонического узора или любого другого поля, не
927
+ меняя само кольцо.
928
+
929
+ ```typescript
930
+ import { SynchronousRing, useField } from "semantic-typescript";
931
+
932
+ const round = (value: number): number => Number(value.toFixed(4));
933
+ const ring = new SynchronousRing(["a", "b", "c", "d", "e", "f"]);
934
+ const cosine = useField([1]);
935
+
936
+ round(ring.scoreAt(0n, 0, cosine).get()); // 1
937
+ round(ring.scoreAt(1n, 0, cosine).get()); // 0.5
938
+ ring.scoresAt(0, cosine).map(score => round(score)); // [1, 0.5, -0.5, -1, -0.5, 0.5]
939
+ ```
940
+
941
+ ### 13.6 Объединение колец и угловой спектр
942
+
943
+ `combineRings(left, right)` соединяет два кольца по наименьшему общему кратному, а не по
944
+ произведению. Момент `i` объединённого кольца читает `i mod m` левого кольца и `i mod n` правого,
945
+ поэтому объединённое кольцо замыкается через `lcm(m, n)` моментов. Пары, которые никогда не могут
946
+ встретиться — те, чьи позиции расходятся по модулю `gcd(m, n)`, — просто отсутствуют; поэтому
947
+ результат является обратным образом, а не произведением, и `aligns(left, right)` сообщает,
948
+ представима ли данная пара.
949
+
950
+ `useAngularSpectrum(ring, instants, plan)` строит плоскость `(ω, n)`: каждый момент даёт кадр
951
+ фазоров, кадр `k` моды `j` — это `j`-я симметричная компонента этого кадра, а `cells[frame][order]`
952
+ — коэффициент. Взять один столбец — получить временной ряд одной пространственной моды, а
953
+ `spectrum(order)` — её временной спектр.
954
+
955
+ ```typescript
956
+ import { Sample, SynchronousRing, combineRings, useAngularSpectrum } from "semantic-typescript";
957
+
958
+ const left = new SynchronousRing(["a", "b", "c", "d"]);
959
+ const right = new SynchronousRing(["x", "y", "z", "w", "v", "u"]);
960
+ const joined = combineRings(left, right);
961
+
962
+ joined.period; // 12n
963
+ joined.divisor; // 2n
964
+ joined.ring.size(); // 12n
965
+ joined.aligns(0n, 0n); // true
966
+ joined.aligns(1n, 0n); // false
967
+ joined.ring.get(0n).join("-"); // a-x
968
+
969
+ const angular = useAngularSpectrum(left, [0, 0.25, 0.5, 0.75], new Sample(4, 1));
970
+ angular.order; // 4
971
+ angular.frameCount; // 4
972
+ angular.cells[0][1].magnitude(); // 4
973
+ angular.spectrum(1).get().size(); // 4
974
+ ```
975
+
976
+ ### 13.7 Ловушки
977
+
978
+ **Точность есть точность.** `phaseAt(i)` и `phaseAt(i + n)` — один и тот же double, а не два
979
+ значения в пределах допуска. Кольцо, накапливающее `2π / n` на элемент, этого не выполнит; это
980
+ сначала сокращает индекс в `bigint`.
981
+
982
+ **`frequency` — не шаг фазы.** Это скорость вращения среды, по умолчанию ноль, и поворот всех
983
+ фазоров на один угол не может сломать собственный узор. Если нужно, чтобы позиции смещались
984
+ относительно друг друга, используйте `rate`, а не `frequency`.
985
+
986
+ **`purity()` обязана смотреть оба основных бина.** Кольцо, идущее назад, всё ещё остаётся корнями
987
+ `n`-й степени из единицы; только по `c₁` оно выглядело бы нечистым.
988
+
989
+ **`n < 2` — определённый граничный случай.** `purity()` возвращает `0`, а `isBalanced()` —
990
+ `false`; пустое кольцо возвращает пустые `Optional` из методов выборки.
991
+
992
+ **Span, делящий множитель с `n`, схлопывает орбиту.** Шесть элементов с `span: 2` посещают лишь
993
+ три различные фазы, и кольцо перестаёт быть сбалансированным.
994
+
995
+ **`resultant` инвариантен к вращению, но не ко времени.** Вращение среды его не меняет; `rate`
996
+ каждого элемента меняет его по мере расфазировки.
997
+
998
+ **Кольцо — множество, а `analyse` сопоставляет по тождеству.** Дубликаты отбрасываются при
999
+ создании, а объект, структурно равный элементу кольца, но не являющийся той же ссылкой,
1000
+ считается пропущенным.
1001
+
1002
+ **`toSpectrum()` комплексен.** Его бины не сопряжённо-симметричны, поэтому односторонние
1003
+ помощники `Spectrum` неприменимы. Читайте бины.
1004
+
1005
+ **`windingAt` усекает к нулю, а `errorIntegralAt` интегрирует фазу.** Половина оборота скольжения
1006
+ сообщается как `0n`; интеграл ошибки — в оборотах · секундах, а не скорость.
1007
+
1008
+ ```typescript
1009
+ import { SynchronousRing, useHertz } from "semantic-typescript";
1010
+
1011
+ const collapsed = new SynchronousRing(["a", "b", "c", "d", "e", "f"], { span: 2 });
1012
+ collapsed.phaseAt(1n).get().toDegreeValue(); // 120
1013
+ collapsed.phaseAt(3n).get().toDegreeValue(); // 0
1014
+ Number(collapsed.purity().toFixed(4)); // 0
1015
+
1016
+ const falling = new SynchronousRing(["a", "b", "c", "d"], {
1017
+ frequency: useHertz(2),
1018
+ rate: () => useHertz(0.5),
1019
+ });
1020
+ falling.jetAt(0n, 2).get().slip; // -1.5
1021
+ falling.windingAt(0n, 1).get(); // -1n
1022
+ falling.windingAt(0n, 0.5).get(); // 0n
1023
+
1024
+ const objects = new SynchronousRing([{ key: 1 }, { key: 2 }]);
1025
+ objects.analyse([{ key: 1 }]).missed; // 1 — identity, not structure
1026
+ ```
1027
+
1028
+ ---
1029
+
1030
+ <a id="14-signal-analysis"></a>
1031
+ ## 14. Анализ сигналов: окна, спектры и время
1032
+
1033
+ Глава 10 рассматривала кольцо как цикл фаз. Эта глава — его временной аналог: вещественная
1034
+ запись, которая несёт, помимо отсчётов, два контекста, нужных любой спектральной формуле, —
1035
+ план дискретизации, превращающий номер бина в герцы, и окно, задающее одновременно
1036
+ когерентное усиление и эквивалентную шумовую полосу.
1037
+
1038
+ Объект-значение — `SynchronousSignalCollectable`, его очередь-двойник —
1039
+ `AsynchronousSignalCollectable`. Оба предоставляют одну и ту же поверхность `SignalCollectable`:
1040
+ двадцать три метода, каждый делегирует ровно одной фабрике `useSynchronous<Method>` в
1041
+ `synchronous/collectors`. Разделение намеренно. Класс — документированный фасад, фабрика
1042
+ владеет накоплением, а конструктор один раз при данной длине вычисляет `sum(w) / count` и
1043
+ `count * sum(w^2) / sum(w)^2`. Тот, кому нужно помнить «разделить на когерентное усиление»,
1044
+ рано или поздно забудет, и полученные 6,02 децибела выглядят как правдоподобное измерение.
1045
+
1046
+ ### 14.1 Сигнал несёт собственную калибровку
1047
+
1048
+ Сигнал строится из итерируемого объекта и частоты, из существующего `Frame` или из конструктора
1049
+ с явным `Sample`. Окно по умолчанию — периодическое окно Ханна; любой анализ, не назвавший окно,
1050
+ использует его, и любой анализ принимает замену. Аксессоры метаданных читают зафиксированное при
1051
+ создании состояние за постоянное время, поэтому сигнал можно передавать, не теряя контекст.
1052
+
1053
+ | Вызов | Возвращает | Смысл |
1054
+ | :--- | :--- | :--- |
1055
+ | `SynchronousSignalCollectable.useRate(values, rate, window?)` | сигнал | длительность `count / rate` |
1056
+ | `SynchronousSignalCollectable.useFrame(frame, window?)` | сигнал | сохраняет план кадра |
1057
+ | `new SynchronousSignalCollectable(plan, values, window?)` | сигнал | число кадров должно совпадать с числом отсчётов |
1058
+ | `getSample()` / `getValues()` / `getWindow()` | план, отсчёты, окно | замороженный контекст |
1059
+ | `getRate()` | `number` | отсчётов в секунду |
1060
+ | `getNyquist()` / `getResolution()` | `Frequency` | `rate / 2` и `rate / count` |
1061
+ | `getCoherentGain()` / `getNoiseBandwidth()` | `number` | пересчитываются при этой длине |
1062
+ | `size()` / `at(index)` / `toFrame()` / `equals(other)` / `toString()` | — | семантика значения |
1063
+
1064
+ ```typescript
1065
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1066
+
1067
+ const signal = SynchronousSignalCollectable.useRate(
1068
+ Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 64)),
1069
+ 64,
1070
+ );
1071
+
1072
+ signal.size(); // 64n
1073
+ signal.getRate(); // 64
1074
+ signal.getNyquist().getHertz(); // 32
1075
+ signal.getResolution().getHertz(); // 1
1076
+ signal.getCoherentGain(); // 0.5
1077
+ signal.getNoiseBandwidth(); // 1.5
1078
+ ```
1079
+
1080
+ `getResolution()` — это шаг между бинами, а не разрешающая способность: он равен `rate / count`,
1081
+ потому что в преобразовании `count` бинов, а дополнение нулями меняет шаг, но не то, что окно
1082
+ способно разделить (раздел 14.8).
1083
+
1084
+ ### 14.2 Двадцать три метода
1085
+
1086
+ Методы образуют шесть семейств. Семь — спектральные отсчёты, два — время-частотные
1087
+ преобразования, четыре — аналитические величины, три — статистики последовательности, пять —
1088
+ дескрипторы формы, и два возвращают другой сигнал.
1089
+
1090
+ | Метод | Возвращает | Соглашение |
1091
+ | :--- | :--- | :--- |
1092
+ | `spectrum(size?, window?)` | `Spectrum` | сырое и ненормированное, полная комплексная сетка |
1093
+ | `oneSided(size?, window?)` | `Bin[]` | сопряжённая половина свёрнута, DC и Найквист не удваиваются |
1094
+ | `amplitude(window?)` | `number[]` | `2·abs(X) / (count · CG)`, DC и Найквист один раз |
1095
+ | `power(window?)` | `number[]` | `A² / 2` для внутреннего бина, `A²` при DC и Найквисте |
1096
+ | `decibel(window?)` | `number[]` | `20·log10(амплитуда)` |
1097
+ | `powerDecibel(window?)` | `number[]` | `10·log10(мощность)` |
1098
+ | `powerSpectralDensity(frameSize?, hopSize?, window?)` | `SpectralDensity` | среднее Уэлча, `abs(X)² / (rate · Σw²)` |
1099
+ | `shortTimeFourierTransform(frameSize, hopSize, window?)` | `Spectrum[]` | расширенные края, время по центрам кадров |
1100
+ | `spectrogram(frameSize, hopSize, window?)` | `Spectrogram` | те же столбцы с осью времени |
1101
+ | `analyticSignal()` | `Complex[]` | пара Гильберта `x + i·H(x)` |
1102
+ | `instantaneousPhase()` | `Angle[]` | свёрнута в `[0, 2π)` |
1103
+ | `instantaneousFrequency(rate?, gate?)` | `number[]` | развёрнутая разность фаз с порогом по огибающей |
1104
+ | `envelope()` | `number[]` | `abs(z[n])`, на краях циклическая |
1105
+ | `zeroCrossingRate(perSecond?)` | `number` | переходов через ноль на пару соседних отсчётов |
1106
+ | `spectralFlux(frameSize?, hopSize?, window?)` | `number[]` | расстояние нормированных по норме кадров |
1107
+ | `autocorrelation(maximumLag?, normalization?)` | `number[]` | по умолчанию коэффициент, `ρ[0] = 1` |
1108
+ | `spectralCentroid(window?)` | `Optional<Frequency>` | средняя частота, взвешенная мощностью |
1109
+ | `spectralBandwidth(window?)` | `Optional<Frequency>` | стандартное отклонение, взвешенное мощностью |
1110
+ | `spectralRolloff(ratio?, window?)` | `Optional<Frequency>` | первый бин, достигающий `ratio` мощности |
1111
+ | `spectralFlatness(window?)` | `number` | геометрическое среднее к арифметическому по мощности |
1112
+ | `signalToNoiseRatio(frameSize?, hopSize?, window?, floorQuantile?)` | `Optional<number>` | медианный шумовой пол, `10·log10` |
1113
+ | `detrended(mode?)` | сигнал | `"none"`, `"constant"` (по умолчанию), `"linear"` |
1114
+ | `windowed(window?)` | сигнал | применяет окно и принимает его |
1115
+
1116
+ Необязательные аргументы — не украшение. `frameSize` по умолчанию `min(256, size)`, шаг —
1117
+ половина кадра с округлением вниз, `instantaneousFrequency` ставит порог `1E-3` от пиковой
1118
+ огибающей, а `spectralRolloff` использует `0.85`. Каждое значение по умолчанию — то, что
1119
+ выбирает эталонная реализация, когда параметр назван, но не передан.
1120
+
1121
+ ### 14.3 Амплитуда, мощность и плотность
1122
+
1123
+ Нужны три нормировки, потому что задаются три разных вопроса.
1124
+
1125
+ `amplitude()` отвечает «как велик синус на этом бине»: `2·abs(X[k]) / (count · CG)` для
1126
+ внутреннего бина и `abs(X[k]) / (count · CG)` при DC и Найквисте. Множитель два сворачивает
1127
+ сопряжённую половину; когерентное усиление убирает окно. Если опустить любое из двух, синус
1128
+ единичной амплитуды прочитается как `0.5`, то есть минус 6,02 децибела.
1129
+
1130
+ `power()` отвечает «какой средний квадрат имел бы синус на этом бине»: `A² / 2` для внутреннего
1131
+ бина и `A²` при DC и Найквисте, потому что эти два не колеблются. Это пиковая конвенция, и она
1132
+ намеренно не удовлетворяет теореме Парсеваля: окно размазывает каждую линию по главному
1133
+ лепестку, поэтому сумма всех бинов превышает средний квадрат на эквивалентную шумовую полосу —
1134
+ `1.5` для Ханна.
1135
+
1136
+ `powerSpectralDensity()` отвечает «сколько мощности на герц»: `abs(X)² / (rate · Σw²)`, усредняя
1137
+ по перекрывающимся сегментам. Только сумма квадратов весов оставляет пол белого шума
1138
+ несмещённым; деление на `count` даёт `0.375` для единичного шума, деление на `count · CG²` даёт
1139
+ `1.5`.
1140
+
1141
+ ```typescript
1142
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1143
+
1144
+ const round = (value: number): number => Number(value.toFixed(4));
1145
+ const tone = SynchronousSignalCollectable.useRate(
1146
+ Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1147
+ 256,
1148
+ );
1149
+
1150
+ round(tone.amplitude()[32]); // 1
1151
+ round(tone.power()[32]); // 0.5
1152
+ round(tone.power().reduce((sum, power) => sum + power, 0)); // 0.75
1153
+ round(tone.powerSpectralDensity(64n, 32n).totalPower()); // 0.5
1154
+ tone.powerSpectralDensity(64n, 32n).getSegments(); // 7n
1155
+ ```
1156
+
1157
+ Пара итогов — та проверка, которую стоит запомнить: сумма в пиковой конвенции равна `0.75`, а
1158
+ плотность интегрируется в `0.5`, и отношение равно ровно шумовой полосе. К тому же плотность —
1159
+ единственная из трёх, кто выживает при смене длины кадра.
1160
+
1161
+ ### 14.4 Уровни: две конвенции децибела
1162
+
1163
+ Амплитуда и мощность не делят множитель, значит, не делят и логарифм. `amplitudeDecibel`
1164
+ применяет `20·log10` и годится для напряжения, смещения или звукового давления;
1165
+ `powerDecibel` применяет `10·log10` и годится для мощности, энергии, среднего квадрата или
1166
+ плотности. Имена методов сигнала следуют тому же разделению.
1167
+
1168
+ ```typescript
1169
+ import { amplitudeDecibel, powerDecibel, SynchronousSignalCollectable } from "semantic-typescript";
1170
+
1171
+ const round = (value: number): number => Number(value.toFixed(4));
1172
+ const tone = SynchronousSignalCollectable.useRate(
1173
+ Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 32 * index / 256)),
1174
+ 256,
1175
+ );
1176
+
1177
+ amplitudeDecibel(0.25); // -12.0412
1178
+ powerDecibel(0.25); // -6.0206
1179
+ round(tone.decibel()[32]); // 0
1180
+ round(tone.powerDecibel()[32]); // -3.0103
1181
+ ```
1182
+
1183
+ Оба отсчёта одного тона верны: его амплитуда равна единице (ноль децибел), а средний квадрат —
1184
+ половине (минус 3,01 децибела). Если подать мощность в амплитудную конвенцию, уровень в
1185
+ децибелах удвоится, и правильные минус 6,02 станут минус 12,04.
1186
+
1187
+ ### 14.5 Одностороннее свёртывание
1188
+
1189
+ Вещественный сигнал удовлетворяет `X[count − k] = conj(X[k])`, поэтому половина отрицательных
1190
+ частот не несёт независимой информации. Свёртывание удваивает каждый внутренний бин и оставляет
1191
+ два самосопряжённых бина — DC и, при чётной длине, Найквиста — ровно такими, какими их создало
1192
+ преобразование. Эти два — собственное зеркало; удвоение изобретает энергию и даёт ровно на 6,02
1193
+ децибела больше.
1194
+
1195
+ ```typescript
1196
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1197
+
1198
+ const round = (value: number): number => Number(value.toFixed(4));
1199
+ const edge = SynchronousSignalCollectable.useRate(
1200
+ Array.from({ length: 16 }, (_unused, index) => 2 + Math.cos(Math.PI * index)),
1201
+ 16,
1202
+ );
1203
+ const bins = edge.oneSided();
1204
+
1205
+ bins.length; // 9
1206
+ round(bins[0].magnitude()); // 16
1207
+ round(bins[8].magnitude()); // 8
1208
+ round(edge.amplitude()[0]); // 2
1209
+ round(edge.amplitude()[8]); // 1
1210
+ ```
1211
+
1212
+ Сырые величины равны `16` и `8`, потому что веса окна суммируются в восемь; деление на
1213
+ `count · CG = 8` превращает их в калиброванные амплитуды `2` и `1`. Удвоение краевых бинов дало
1214
+ бы `4` и `2`, и эта ошибка никогда не вызвала бы исключения.
1215
+
1216
+ ### 14.6 Время-частотные представления
1217
+
1218
+ `shortTimeFourierTransform` и `spectrogram` нарезают запись так же, как эталонное
1219
+ кратковременное преобразование: по половине кадра нулей с каждого конца, затем хвост,
1220
+ дополненный нулями до целого числа шагов. Первый кадр центрирован на первом отсчёте, последний
1221
+ достигает последнего отсчёта, а кадр `m` центрирован на `m · hop / rate` секундах. Сохранение
1222
+ только полных кадров выбросило бы до `frameSize − 1` отсчётов — для 1000 отсчётов, кадра 256 и
1223
+ шага 128 это 104 отсчёта, около десятой части записи.
1224
+
1225
+ ```typescript
1226
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1227
+
1228
+ const tone = SynchronousSignalCollectable.useRate(
1229
+ Array.from({ length: 64 }, (_unused, index) => Math.cos(2 * Math.PI * 4 * index / 64)),
1230
+ 64,
1231
+ );
1232
+ const spectrogram = tone.spectrogram(16n, 8n);
1233
+
1234
+ spectrogram.size(); // 9
1235
+ spectrogram.getFrameSize(); // 16
1236
+ spectrogram.getBoundary(); // "zeros"
1237
+ spectrogram.getTime(0); // 0
1238
+ spectrogram.getTime(6); // 0.75
1239
+ tone.shortTimeFourierTransform(16n, 8n).length; // 9
1240
+ tone.spectralFlux(16n, 8n).length; // 8
1241
+ ```
1242
+
1243
+ Каждый столбец — это `Spectrum` на `frameSize` бинов, ровно то, что дал бы `spectrum()` для того
1244
+ же кадра, а `getTime()` сообщает центры кадров, а не их начала.
1245
+
1246
+ ### 14.7 Окна, когерентное усиление и COLA
1247
+
1248
+ У каждой формы две версии. Симметричная делит на `count − 1`; периодическая (чётная по ДПФ) делит
1249
+ на `count`, на один отсчёт меньше полного периода, поэтому окно непрерывно при периодическом
1250
+ продолжении, которое предполагает ДПФ. Анализу и суммированию с перекрытием нужна
1251
+ периодическая форма; проектирование фильтров сохраняет симметричную.
1252
+
1253
+ Разница измерима и никогда не вызывает ошибки. При пятидесяти процентах перекрытия
1254
+ периодическое окно Ханна суммируется в единицу в каждой позиции; симметричная форма пульсирует:
1255
+ `0.84` децибела при шестнадцати отсчётах, `0.21` при шестидесяти четырёх и всё ещё `0.013` при
1256
+ тысяче двадцати четырёх. Класс предоставляет когерентное усиление и эквивалентную шумовую полосу
1257
+ своего окна, поэтому поправка никогда не зашита: симметричный Ханн при восьми отсчётах читается
1258
+ как `0.4375`, а не `0.5`.
1259
+
1260
+ ```typescript
1261
+ import {
1262
+ coherentGain, equivalentNoiseBandwidth, useFlatTopWindow,
1263
+ useHannPeriodicWindow, useHannWindow,
1264
+ } from "semantic-typescript";
1265
+
1266
+ const round = (value: number): number => Number(value.toFixed(4));
1267
+
1268
+ round(useHannWindow(1, 4)); // 0.75
1269
+ round(useHannPeriodicWindow(1, 4)); // 0.5
1270
+ coherentGain(useHannPeriodicWindow, 16); // 0.5
1271
+ equivalentNoiseBandwidth(useHannPeriodicWindow, 16); // 1.5
1272
+ useFlatTopWindow(0, 1024); // -0.000421
1273
+ Array.from({ length: 1024 }, (_unused, i) => useFlatTopWindow(i, 1024)).filter(w => w < 0).length; // 578
1274
+ ```
1275
+
1276
+ Строка flat-top — не ошибка таблицы. Окно flat-top по определению частично отрицательно — здесь
1277
+ 578 из 1024 весов, — и обрезание этих весов до нуля исказило бы главный лепесток и уничтожило
1278
+ бы малые потери scalloping, ради которых окно существует. Библиотека возвращает ряд без
1279
+ изменений.
1280
+
1281
+ ### 14.8 Ловушки
1282
+
1283
+ **Дополнение нулями интерполирует, но никогда не разрешает.** Длина преобразования больше числа
1284
+ отсчётов лишь плотнее дискретизирует ту же ДПФ-непрерывную функцию: *положение* пика уточняется,
1285
+ а шаг бинов становится `rate / size`, но способность разделить две компоненты по-прежнему
1286
+ определяется длиной и формой окна. Тон ровно между двумя бинами читается как `82.1093` и
1287
+ `80.8598` на сетке без дополнения и как `128` на своей частоте на дополненной сетке.
1288
+
1289
+ ```typescript
1290
+ import { SynchronousSignalCollectable, useRectangularWindow } from "semantic-typescript";
1291
+
1292
+ const round = (value: number): number => Number(value.toFixed(4));
1293
+ const tone = SynchronousSignalCollectable.useRate(
1294
+ Array.from({ length: 256 }, (_unused, index) => Math.cos(2 * Math.PI * 100.5 * index / 256)),
1295
+ 256,
1296
+ );
1297
+ const flat = tone.spectrum(256n, useRectangularWindow);
1298
+ const padded = tone.spectrum(1024n, useRectangularWindow);
1299
+
1300
+ flat.getResolution().getHertz(); // 1
1301
+ padded.getResolution().getHertz(); // 0.25
1302
+ round(flat.magnitudeAt(100).get()); // 82.1093
1303
+ round(flat.magnitudeAt(101).get()); // 80.8598
1304
+ round(padded.magnitudeAt(402).get()); // 128
1305
+ padded.frequencyAt(402).get().getHertz(); // 100.5
1306
+ ```
1307
+
1308
+ Два равных тона с разносом `0.4` бина остаются слитыми при любом дополнении: при записи из 256
1309
+ точек и окне Ханна дополнение до 8192 даёт один локальный максимум, а не два.
1310
+
1311
+ **Преобразование Гильберта циклично.** `analyticSignal`, `envelope` и `instantaneousPhase`
1312
+ считают первый и последний отсчёты соседями, поэтому запись, период которой не делит её длину,
1313
+ загрязнена на обоих концах. Середина записи надёжна, края — нет.
1314
+
1315
+ ```typescript
1316
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1317
+
1318
+ const round = (value: number): number => Number(value.toFixed(4));
1319
+ const edge = SynchronousSignalCollectable.useRate(
1320
+ Array.from({ length: 512 }, (_unused, index) => Math.cos(2 * Math.PI * 8.3 * index / 512)),
1321
+ 512,
1322
+ );
1323
+ const envelope = edge.envelope();
1324
+
1325
+ round(envelope.slice(8, -8).reduce((sum, value) => sum + value, 0) / 496); // 1.0025
1326
+ round(Math.max(...envelope.slice(0, 8))); // 1.2592
1327
+ round(Math.min(...envelope.slice(-8))); // 0.1314
1328
+ ```
1329
+
1330
+ **Исчезающая огибающая не определяет фазу.** Там, где огибающая рушится, разность двух углов
1331
+ шума округления может достигать пол-оборота, что читается как половина частоты дискретизации.
1332
+ Порог — именно то, что сохраняет числу смысл, и по умолчанию он равен `1E-3` от пиковой
1333
+ огибающей.
1334
+
1335
+ ```typescript
1336
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1337
+
1338
+ const round = (value: number): number => Number(value.toFixed(4));
1339
+ const beat = SynchronousSignalCollectable.useRate(
1340
+ Array.from({ length: 4096 }, (_unused, index) =>
1341
+ Math.cos(2 * Math.PI * 100 * index / 2048) + Math.cos(2 * Math.PI * 102 * index / 2048)),
1342
+ 2048,
1343
+ );
1344
+
1345
+ round(Math.max(...beat.instantaneousFrequency(2048, 0).map(Math.abs))); // 920.218
1346
+ round(Math.max(...beat.instantaneousFrequency().map(Math.abs))); // 101
1347
+ ```
1348
+
1349
+ **Ненормированная корреляция — не мера сходства.** Сырая сумма `r[k]` растёт с длиной записи,
1350
+ поэтому две длины несравнимы. Правило по умолчанию `"coefficient"` делит на `r[0]` и держит все
1351
+ значения в `[-1, 1]`; `"unbiased"` делит на `count − k` и платит дисперсией, растущей с задержкой.
1352
+
1353
+ ```typescript
1354
+ import { SynchronousSignalCollectable } from "semantic-typescript";
1355
+
1356
+ const round = (value: number): number => Number(value.toFixed(4));
1357
+ const periodic = SynchronousSignalCollectable.useRate(
1358
+ Array.from({ length: 1024 }, (_unused, index) => Math.cos(2 * Math.PI * 8 * index / 1024)),
1359
+ 1024,
1360
+ );
1361
+ const rho = periodic.autocorrelation(200n);
1362
+
1363
+ round(rho[0]); // 1
1364
+ round(rho[128]); // 0.875
1365
+ round(rho[64]); // -0.9375
1366
+ round(periodic.autocorrelation(200n, "none")[0]); // 512
1367
+ ```
1368
+
1369
+ Ещё один отсчёт заслуживает предупреждения: `spectralFlux` нормирует каждый кадр перед
1370
+ сравнением, поэтому чистое изменение уровня даёт нулевой поток. Его первое и последнее значения —
1371
+ краевые артефакты нулевой границы, а не атаки.
1372
+
1373
+ ---
1374
+
1375
+ <a id="15-license"></a>
1376
+ ## 15. Лицензия
1377
+
1378
+ MIT © Eloy Kim