pca-web 0.1.0
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/LICENSE +21 -0
- package/README.md +421 -0
- package/dist/base.d.ts +79 -0
- package/dist/base.d.ts.map +1 -0
- package/dist/base.js +358 -0
- package/dist/base.js.map +1 -0
- package/dist/incremental-pca.d.ts +86 -0
- package/dist/incremental-pca.d.ts.map +1 -0
- package/dist/incremental-pca.js +397 -0
- package/dist/incremental-pca.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/matrix.d.ts +44 -0
- package/dist/matrix.d.ts.map +1 -0
- package/dist/matrix.js +99 -0
- package/dist/matrix.js.map +1 -0
- package/dist/model.d.ts +86 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +132 -0
- package/dist/model.js.map +1 -0
- package/dist/numeric/blas.d.ts +33 -0
- package/dist/numeric/blas.d.ts.map +1 -0
- package/dist/numeric/blas.js +130 -0
- package/dist/numeric/blas.js.map +1 -0
- package/dist/numeric/eigh.d.ts +21 -0
- package/dist/numeric/eigh.d.ts.map +1 -0
- package/dist/numeric/eigh.js +233 -0
- package/dist/numeric/eigh.js.map +1 -0
- package/dist/numeric/lanczos.d.ts +31 -0
- package/dist/numeric/lanczos.d.ts.map +1 -0
- package/dist/numeric/lanczos.js +287 -0
- package/dist/numeric/lanczos.js.map +1 -0
- package/dist/numeric/lu.d.ts +27 -0
- package/dist/numeric/lu.d.ts.map +1 -0
- package/dist/numeric/lu.js +149 -0
- package/dist/numeric/lu.js.map +1 -0
- package/dist/numeric/mle.d.ts +5 -0
- package/dist/numeric/mle.d.ts.map +1 -0
- package/dist/numeric/mle.js +67 -0
- package/dist/numeric/mle.js.map +1 -0
- package/dist/numeric/qr.d.ts +15 -0
- package/dist/numeric/qr.d.ts.map +1 -0
- package/dist/numeric/qr.js +88 -0
- package/dist/numeric/qr.js.map +1 -0
- package/dist/numeric/randomized.d.ts +54 -0
- package/dist/numeric/randomized.d.ts.map +1 -0
- package/dist/numeric/randomized.js +130 -0
- package/dist/numeric/randomized.js.map +1 -0
- package/dist/numeric/rng.d.ts +40 -0
- package/dist/numeric/rng.d.ts.map +1 -0
- package/dist/numeric/rng.js +127 -0
- package/dist/numeric/rng.js.map +1 -0
- package/dist/numeric/special.d.ts +2 -0
- package/dist/numeric/special.d.ts.map +1 -0
- package/dist/numeric/special.js +25 -0
- package/dist/numeric/special.js.map +1 -0
- package/dist/numeric/stats.d.ts +27 -0
- package/dist/numeric/stats.d.ts.map +1 -0
- package/dist/numeric/stats.js +110 -0
- package/dist/numeric/stats.js.map +1 -0
- package/dist/numeric/svd.d.ts +27 -0
- package/dist/numeric/svd.d.ts.map +1 -0
- package/dist/numeric/svd.js +374 -0
- package/dist/numeric/svd.js.map +1 -0
- package/dist/numeric/svdflip.d.ts +9 -0
- package/dist/numeric/svdflip.d.ts.map +1 -0
- package/dist/numeric/svdflip.js +32 -0
- package/dist/numeric/svdflip.js.map +1 -0
- package/dist/pca.d.ts +170 -0
- package/dist/pca.d.ts.map +1 -0
- package/dist/pca.js +649 -0
- package/dist/pca.js.map +1 -0
- package/dist/progress.d.ts +155 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +173 -0
- package/dist/progress.js.map +1 -0
- package/dist/scheduling.d.ts +66 -0
- package/dist/scheduling.d.ts.map +1 -0
- package/dist/scheduling.js +160 -0
- package/dist/scheduling.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +17 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +36 -0
- package/dist/validation.js.map +1 -0
- package/dist/webgpu/df64.d.ts +18 -0
- package/dist/webgpu/df64.d.ts.map +1 -0
- package/dist/webgpu/df64.js +42 -0
- package/dist/webgpu/df64.js.map +1 -0
- package/dist/webgpu/engine.d.ts +74 -0
- package/dist/webgpu/engine.d.ts.map +1 -0
- package/dist/webgpu/engine.js +326 -0
- package/dist/webgpu/engine.js.map +1 -0
- package/dist/webgpu/gpu-pca.d.ts +90 -0
- package/dist/webgpu/gpu-pca.d.ts.map +1 -0
- package/dist/webgpu/gpu-pca.js +407 -0
- package/dist/webgpu/gpu-pca.js.map +1 -0
- package/dist/webgpu/index.d.ts +19 -0
- package/dist/webgpu/index.d.ts.map +1 -0
- package/dist/webgpu/index.js +16 -0
- package/dist/webgpu/index.js.map +1 -0
- package/dist/webgpu/kernels.d.ts +47 -0
- package/dist/webgpu/kernels.d.ts.map +1 -0
- package/dist/webgpu/kernels.js +147 -0
- package/dist/webgpu/kernels.js.map +1 -0
- package/dist/worker/client.d.ts +174 -0
- package/dist/worker/client.d.ts.map +1 -0
- package/dist/worker/client.js +482 -0
- package/dist/worker/client.js.map +1 -0
- package/dist/worker/handler.d.ts +17 -0
- package/dist/worker/handler.d.ts.map +1 -0
- package/dist/worker/handler.js +404 -0
- package/dist/worker/handler.js.map +1 -0
- package/dist/worker/protocol.d.ts +192 -0
- package/dist/worker/protocol.d.ts.map +1 -0
- package/dist/worker/protocol.js +70 -0
- package/dist/worker/protocol.js.map +1 -0
- package/dist/worker/worker.d.ts +14 -0
- package/dist/worker/worker.d.ts.map +1 -0
- package/dist/worker/worker.js +20 -0
- package/dist/worker/worker.js.map +1 -0
- package/package.json +94 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeya Balaji Balasubramanian
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# pca-web
|
|
2
|
+
|
|
3
|
+
Client-side **PCA** and **IncrementalPCA** for the browser and Node.js — a zero-runtime-dependency
|
|
4
|
+
TypeScript reimplementation of scikit-learn 1.9's `sklearn.decomposition.PCA` and
|
|
5
|
+
`IncrementalPCA`, with optional **WebGPU acceleration**.
|
|
6
|
+
|
|
7
|
+
- **scikit-learn is the specification.** Every solver (`full`, `covariance_eigh`, `arpack`,
|
|
8
|
+
`randomized`), every `nComponents` mode (int, fraction, `'mle'`, `null`), whitening, `copy`
|
|
9
|
+
semantics, the auto-solver heuristic, and the deterministic sign convention
|
|
10
|
+
(`svd_flip(u_based_decision=False)`) are ported from the sklearn 1.9.0 source and verified
|
|
11
|
+
against fixtures generated by the real scikit-learn (see [Numerical parity](#numerical-parity-and-tolerances)).
|
|
12
|
+
- Ships as ESM with TypeScript declarations. Node ≥ 20 and evergreen browsers.
|
|
13
|
+
- Zero runtime dependencies. The WebGPU path lives behind the `pca-web/webgpu` subpath so the
|
|
14
|
+
core stays GPU-free for bundlers.
|
|
15
|
+
- Even the random number generator is a bit-exact numpy `RandomState` (MT19937) replica: with
|
|
16
|
+
the same seed, `svdSolver: 'randomized'` reproduces sklearn's output to floating-point
|
|
17
|
+
accuracy — not just statistically.
|
|
18
|
+
- Fits can run [off the main thread](#web-workers) (`pca-web/client` + `pca-web/worker`), report
|
|
19
|
+
[live progress with intermediate models](#progress-non-blocking-fits-and-cancellation), be
|
|
20
|
+
aborted mid-fit, and be [serialized](#model-serialization) for IndexedDB/`postMessage`/JSON —
|
|
21
|
+
see the [live demo](https://jeyabbalas.github.io/pca-web/) (source in `examples/demo/`).
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { PCA } from 'pca-web';
|
|
25
|
+
|
|
26
|
+
const pca = new PCA({ nComponents: 2, svdSolver: 'randomized', randomState: 42 });
|
|
27
|
+
const embedding = pca.fitTransform(X); // X: number[][] or a Matrix
|
|
28
|
+
|
|
29
|
+
pca.components; // principal axes (Matrix, k×p)
|
|
30
|
+
pca.explainedVarianceRatio; // Float64Array
|
|
31
|
+
pca.singularValues;
|
|
32
|
+
pca.transform(Xnew);
|
|
33
|
+
pca.inverseTransform(embedding);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npm install pca-web
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { PCA, IncrementalPCA, Matrix } from 'pca-web'; // CPU core
|
|
44
|
+
import { WebGPUPCA, isWebGPUSupported } from 'pca-web/webgpu'; // GPU frontend
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Inputs, outputs and dtypes
|
|
48
|
+
|
|
49
|
+
`fit`/`transform` accept either `number[][]` or a `Matrix` — a thin row-major wrapper around a
|
|
50
|
+
typed array:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { Matrix } from 'pca-web';
|
|
54
|
+
const X = new Matrix(new Float64Array(n * p), n, p); // zero-copy
|
|
55
|
+
const X32 = new Matrix(new Float32Array(n * p), n, p);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The result dtype follows the input, like sklearn: `Float32Array` in → float32 attributes and
|
|
59
|
+
transforms out; `Float64Array` or `number[][]` in → float64 out. One deliberate divergence:
|
|
60
|
+
float32 inputs are *computed* in float64 internally and rounded to float32 on output, whereas
|
|
61
|
+
sklearn runs a native-f32 LAPACK pipeline — this port is therefore slightly **more** accurate
|
|
62
|
+
for f32 data, and agrees with sklearn within the documented f32 tolerance class (~2e-3)
|
|
63
|
+
rather than bit-for-bit. Transform results are `Matrix` (use `.toArray()` for `number[][]`,
|
|
64
|
+
or `.data`/`.rows`/`.cols` directly).
|
|
65
|
+
|
|
66
|
+
Like sklearn, `copy: false` lets `fit` overwrite the training data: the `full` solver centers
|
|
67
|
+
it in place, and the truncated solvers (`arpack`, `randomized`) center **and square** it in
|
|
68
|
+
place — the input is destroyed. The default `copy: true` never mutates inputs.
|
|
69
|
+
|
|
70
|
+
Fitting requires **at least 2 samples** (`n − 1` is the variance denominator). This is a
|
|
71
|
+
deliberate, documented deviation from sklearn, which accepts a single sample and returns an
|
|
72
|
+
all-NaN model alongside a Python `RuntimeWarning` — a channel that does not exist in JS. The
|
|
73
|
+
same applies to `IncrementalPCA`'s first batch; later `partialFit` batches may be any size,
|
|
74
|
+
including single rows.
|
|
75
|
+
|
|
76
|
+
## API ↔ scikit-learn mapping
|
|
77
|
+
|
|
78
|
+
Names are idiomatic TypeScript camelCase; each maps 1:1 to its sklearn counterpart.
|
|
79
|
+
|
|
80
|
+
| pca-web | scikit-learn |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `new PCA({ nComponents, copy, whiten, svdSolver, tol, iteratedPower, nOversamples, powerIterationNormalizer, randomState })` | `PCA(n_components, copy, whiten, svd_solver, tol, iterated_power, n_oversamples, power_iteration_normalizer, random_state)` |
|
|
83
|
+
| `fit(X)` / `fitTransform(X)` / `transform(X)` / `inverseTransform(Y)` | `fit` / `fit_transform` / `transform` / `inverse_transform` |
|
|
84
|
+
| `getCovariance()` / `getPrecision()` | `get_covariance()` / `get_precision()` |
|
|
85
|
+
| `scoreSamples(X)` / `score(X)` | `score_samples` / `score` |
|
|
86
|
+
| `getFeatureNamesOut()` | `get_feature_names_out()` |
|
|
87
|
+
| `fitAsync(X, opts?)` / `fitTransformAsync(X, opts?)` | — (non-blocking fit, [see below](#progress-non-blocking-fits-and-cancellation)) |
|
|
88
|
+
| `toModel()` / `PCA.fromModel(m)` / `modelToJSON` / `modelFromJSON` | — (serialization, [see below](#model-serialization)) |
|
|
89
|
+
| `components` | `components_` |
|
|
90
|
+
| `explainedVariance` / `explainedVarianceRatio` | `explained_variance_` / `explained_variance_ratio_` |
|
|
91
|
+
| `singularValues` / `mean` / `noiseVariance` | `singular_values_` / `mean_` / `noise_variance_` |
|
|
92
|
+
| `nComponents` / `nSamples` / `nFeaturesIn` | `n_components_` / `n_samples_` / `n_features_in_` |
|
|
93
|
+
| `resolvedSvdSolver` | `_fit_svd_solver` (the solver `'auto'` dispatched to) |
|
|
94
|
+
| `new IncrementalPCA({ nComponents, whiten, copy, batchSize })` | `IncrementalPCA(n_components, whiten, copy, batch_size)` |
|
|
95
|
+
| `partialFit(X)` / `batchSize` / `nSamplesSeen` / `variance` | `partial_fit` / `batch_size_` / `n_samples_seen_` / `var_` |
|
|
96
|
+
|
|
97
|
+
`nComponents` semantics match sklearn exactly: an integer ≥ 0; a fraction in (0, 1) selecting
|
|
98
|
+
the smallest k whose cumulative explained-variance ratio exceeds it (full/covariance_eigh); the
|
|
99
|
+
string `'mle'` for Minka's MLE (full/covariance_eigh, requires n ≥ p); or `null` (default) for
|
|
100
|
+
`min(nSamples, nFeatures)` (arpack: min − 1). Validation errors mirror sklearn's messages and
|
|
101
|
+
check order.
|
|
102
|
+
|
|
103
|
+
`tol` is accepted for API parity; the built-in Lanczos solver always converges the requested
|
|
104
|
+
triplets to machine precision (equivalent to sklearn's default `tol=0`).
|
|
105
|
+
|
|
106
|
+
## Solver selection
|
|
107
|
+
|
|
108
|
+
| solver | use when | notes |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| `'auto'` (default) | in doubt | sklearn 1.9's exact heuristic: `covariance_eigh` when p ≤ 1000 and n ≥ 10·p; else `full` when max(n, p) ≤ 500 or `nComponents` is `'mle'`; else `randomized` when 1 ≤ k < 0.8·min(n, p); else `full` |
|
|
111
|
+
| `'full'` | small/medium data, all components | Golub–Reinsch SVD of the centered data |
|
|
112
|
+
| `'covariance_eigh'` | many samples, few features (n ≫ p) | eigendecomposition of the p×p covariance; fastest for tall data, GPU-accelerated |
|
|
113
|
+
| `'randomized'` | large data, few components | Halko et al. randomized SVD, seed-compatible with sklearn; GPU-accelerated |
|
|
114
|
+
| `'arpack'` | k strictly < min(n, p), sparse-ish spectra | Golub–Kahan–Lanczos with full reorthogonalization (scipy `svds` equivalent) |
|
|
115
|
+
|
|
116
|
+
`RandomState` (exported) is the numpy-compatible RNG; pass an integer seed (< 2³²) or an
|
|
117
|
+
instance as `randomState`.
|
|
118
|
+
|
|
119
|
+
## IncrementalPCA
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { IncrementalPCA } from 'pca-web';
|
|
123
|
+
|
|
124
|
+
const ipca = new IncrementalPCA({ nComponents: 8, batchSize: 200 });
|
|
125
|
+
ipca.fit(X); // internally batched over gen_batches(n, batchSize)
|
|
126
|
+
// or stream without holding the dataset in memory:
|
|
127
|
+
for (const chunk of chunks) {
|
|
128
|
+
ipca.partialFit(chunk);
|
|
129
|
+
}
|
|
130
|
+
ipca.transform(Xnew);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Semantics ported from sklearn: `batchSize` defaults to `5 * nFeatures` in `fit`; small tail
|
|
134
|
+
batches are merged like `gen_batches`; the incremental mean/variance uses float64 accumulators
|
|
135
|
+
(so `mean`/`variance` are always float64, while float32 components stay float32 after the
|
|
136
|
+
first `partialFit` and become float64 once batches are stacked — numpy's `vstack` promotion,
|
|
137
|
+
faithfully replicated); `noiseVariance` follows sklearn's `k ∉ {batch n, p}` rule. The parity
|
|
138
|
+
suite verifies the complete fitted state after **every** `partial_fit` step against sklearn.
|
|
139
|
+
|
|
140
|
+
## WebGPU acceleration
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import { WebGPUPCA, isWebGPUSupported } from 'pca-web/webgpu';
|
|
144
|
+
|
|
145
|
+
const pca = new WebGPUPCA({ nComponents: 16, svdSolver: 'randomized', randomState: 42 });
|
|
146
|
+
await pca.fit(X); // async: GPU when available and worthwhile, CPU otherwise
|
|
147
|
+
pca.backend; // 'webgpu' | 'cpu' — what the last fit actually used
|
|
148
|
+
pca.gpuAdapterInfo; // e.g. "apple / metal-3"
|
|
149
|
+
await pca.transform(Xnew); // GPU projection for large inputs
|
|
150
|
+
pca.dispose(); // release GPU resources
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- **What runs on the GPU:** the large-GEMM hotspots — the Gram product of `covariance_eigh`,
|
|
154
|
+
the power-iteration panel products of `randomized` (the data matrix stays resident on the
|
|
155
|
+
device across iterations), and large `transform` projections. Everything else (eigh, panel
|
|
156
|
+
QR/LU, small SVDs, sign flips, validation, bookkeeping) runs through the same code as the
|
|
157
|
+
CPU class, so semantics cannot drift.
|
|
158
|
+
- **Fallback:** without WebGPU (or for `full`/`arpack`, or inputs below `minGpuElements`,
|
|
159
|
+
default 2¹⁸ elements), `WebGPUPCA` delegates to the CPU `PCA` — results are **bit-identical**
|
|
160
|
+
to `new PCA(...)` by construction (verified in the Node test suite).
|
|
161
|
+
- **Device injection:** pass your own `device: GPUDevice` (it is never destroyed by
|
|
162
|
+
`dispose()`); `powerPreference` is forwarded to `requestAdapter`.
|
|
163
|
+
- **TypeScript:** the `pca-web/webgpu` declarations use the standard WebGPU globals
|
|
164
|
+
(`GPUDevice`, …). With `lib: ["dom"]` (TypeScript ≥ 5.9 ships WebGPU in the DOM lib) they
|
|
165
|
+
resolve with no extra packages. Node-only lib targets instead add the types-only package
|
|
166
|
+
`@webgpu/types` (declared here as an optional peer dependency) with
|
|
167
|
+
`"types": ["@webgpu/types"]` and `skipLibCheck` in tsconfig. The runtime has zero
|
|
168
|
+
dependencies either way, and the core `pca-web` entry needs nothing.
|
|
169
|
+
- **Precision:** WGSL has no f64, and (measured on Chrome/Metal) the shader compiler's
|
|
170
|
+
fast-math destroys classic compensated-arithmetic tricks. The GEMM kernels therefore use
|
|
171
|
+
fma-exact products of double-single operands with **exact integer-binned accumulation**
|
|
172
|
+
(5×13-bit i32 bins per element; integer adds cannot be fast-mathed), recombined in float64.
|
|
173
|
+
The engine self-checks precision on creation and **refuses the GPU** (falling back to CPU)
|
|
174
|
+
if the driver cannot reach near-f64 accuracy — measured 5.3e-13 worst-case GEMM relative
|
|
175
|
+
error on Apple Metal, vs ~1e-5 for a plain f32 kernel.
|
|
176
|
+
- **Measured CPU↔GPU equivalence** (browser harness, Apple M-series, `apple / metal-3`):
|
|
177
|
+
components/singular values/variances agree to ~1e-10 absolute / ~1e-8 relative worst-case
|
|
178
|
+
on float64 fits (dominated by eigenvector gap sensitivity, not GEMM error); float32 fits
|
|
179
|
+
round to **identical** f32 outputs.
|
|
180
|
+
|
|
181
|
+
Benchmarks from this machine (`npm run bench:browser`, median of 3 fits, GPU time includes
|
|
182
|
+
upload + readback):
|
|
183
|
+
|
|
184
|
+
| case | size | solver | CPU (ms) | GPU (ms) | speedup |
|
|
185
|
+
| --- | --- | --- | ---: | ---: | ---: |
|
|
186
|
+
| cov_50000x200 | 50000×200 | covariance_eigh | 730 | 237 | 3.1× |
|
|
187
|
+
| cov_100000x100 | 100000×100 | covariance_eigh | 396 | 149 | 2.7× |
|
|
188
|
+
| rand_20000x500_nc16 | 20000×500 | randomized | 3332 | 637 | 5.2× |
|
|
189
|
+
| rand_5000x2000_nc32 | 5000×2000 | randomized | 4843 | 631 | 7.7× |
|
|
190
|
+
| cov_f32_50000x200 (f32) | 50000×200 | covariance_eigh | 1250 | 235 | 5.3× |
|
|
191
|
+
|
|
192
|
+
## Progress, non-blocking fits and cancellation
|
|
193
|
+
|
|
194
|
+
Every estimator has four fit entry points with identical numerics — `fit` and `fitTransform`
|
|
195
|
+
run the solver to completion synchronously; `fitAsync` and `fitTransformAsync` run the **same
|
|
196
|
+
solver steps** time-sliced through the event loop (default budget 12 ms per slice), so the UI
|
|
197
|
+
keeps painting. Observer-on vs observer-off, sync vs async: the fitted models are
|
|
198
|
+
**bit-identical** (enforced by tests, including that snapshots draw nothing from the RNG
|
|
199
|
+
stream).
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const pca = new PCA({ nComponents: 16, svdSolver: 'randomized', randomState: 42 });
|
|
203
|
+
const controller = new AbortController();
|
|
204
|
+
|
|
205
|
+
await pca.fitAsync(X, {
|
|
206
|
+
budgetMs: 12, // solver time per event-loop slice
|
|
207
|
+
signal: controller.signal, // abort mid-fit → rejects with name === 'AbortError'
|
|
208
|
+
snapshot: { scores: true, every: 1 },
|
|
209
|
+
onProgress(e) {
|
|
210
|
+
render(e.fraction); // [0,1] monotone, or null while indeterminate
|
|
211
|
+
if (e.snapshot) plot(e.snapshot.scores); // evolving embedding of the training rows
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`onProgress` fires synchronously from inside the fit with `{estimator, solver, phase, step,
|
|
217
|
+
totalSteps, fraction, snapshot?, detail?}`. Cadence and snapshot availability per solver:
|
|
218
|
+
|
|
219
|
+
| solver | phases | one event per… | `fraction` | intermediate snapshots |
|
|
220
|
+
| --- | --- | --- | --- | --- |
|
|
221
|
+
| `full` | `decompose` → `finalize` | decomposition (indeterminate) | null → 1 | finalize only |
|
|
222
|
+
| `covariance_eigh` | `gram` → `decompose` → `finalize` | Gram chunk (~2²² elements) | 0–0.85 → pinned → 1 | finalize only |
|
|
223
|
+
| `randomized` | `power-iteration` → `finalize` | power iteration | 0–0.9 → 1 | every iteration (opt-in) |
|
|
224
|
+
| `arpack` | `lanczos-step` → `finalize` | convergence check; `detail: {basisSize, jmax, maxResidual}` | null → 1 | at checkpoints |
|
|
225
|
+
| IncrementalPCA | `batch` → `finalize` | `partial_fit` batch | linear → 1 | every batch |
|
|
226
|
+
|
|
227
|
+
Snapshots are opt-in (`snapshot: { components: true }` or `{ scores: true }`): fresh float64
|
|
228
|
+
copies in sklearn's sign convention, safe to keep or transfer. On the randomized solver a
|
|
229
|
+
snapshot costs roughly one extra solver step per iteration — `every: 2` halves that.
|
|
230
|
+
|
|
231
|
+
Semantics: an `onProgress` exception propagates and fails the fit. An aborted or failed `PCA`
|
|
232
|
+
fit leaves the estimator **unfitted** (an abort before the first step leaves a previous model
|
|
233
|
+
intact); `IncrementalPCA` keeps the completed-batch model, matching `partial_fit` semantics.
|
|
234
|
+
Abort accepts any `{aborted, reason?}` object — a real `AbortSignal` works, none is required.
|
|
235
|
+
Concurrent fits on one instance throw. `WebGPUPCA.fit(X, options)` takes the same options;
|
|
236
|
+
abort during a GPU fit releases device buffers and does **not** fall back to a CPU refit.
|
|
237
|
+
|
|
238
|
+
## Web Workers
|
|
239
|
+
|
|
240
|
+
`pca-web/client` exports `WorkerPCA` and `WorkerIncrementalPCA` — async proxies that run the
|
|
241
|
+
estimator in a worker and stream progress back. The client bundle contains **no solver code**
|
|
242
|
+
(enforced by an import-graph test): apps that fit only in the worker ship the numerics once,
|
|
243
|
+
inside the worker bundle.
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { WorkerPCA } from 'pca-web/client';
|
|
247
|
+
|
|
248
|
+
const pca = new WorkerPCA({
|
|
249
|
+
nComponents: 16,
|
|
250
|
+
svdSolver: 'randomized',
|
|
251
|
+
randomState: 42, // numeric seeds only — a live RandomState can't cross threads
|
|
252
|
+
backend: 'webgpu', // optional: WebGPU inside the worker, CPU fallback
|
|
253
|
+
});
|
|
254
|
+
await pca.fit(X, {
|
|
255
|
+
signal: controller.signal, // aborts land mid-fit (the worker fit is time-sliced)
|
|
256
|
+
transfer: true, // optional: move X's buffer instead of copying it
|
|
257
|
+
onProgress: (e) => render(e.fraction, e.snapshot),
|
|
258
|
+
progress: { minIntervalMs: 33, snapshot: { scores: true } }, // worker-side throttle
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
pca.components; // sync — every fit piggybacks the model back to a client mirror
|
|
262
|
+
pca.explainedVarianceRatio; // sync
|
|
263
|
+
await pca.transform(Xnew); // compute methods stay in the worker
|
|
264
|
+
const model = pca.exportModel(); // sync snapshot of the mirror (see Model serialization)
|
|
265
|
+
await pca.dispose(); // frees the estimator; terminates the worker it owns
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Progress events are throttled worker-side (`minIntervalMs`, default 33 ms, latest-wins within
|
|
269
|
+
a phase; phase boundaries and `finalize` always delivered, `seq` monotone). `transfer: true`
|
|
270
|
+
detaches the input's buffer from the caller; views are tight-sliced first so a subarray's
|
|
271
|
+
parent buffer is never touched. With `backend: 'webgpu'`, `await pca.info()` reports which
|
|
272
|
+
backend actually executed plus the adapter (`{backend, gpuAdapterInfo, webgpuAvailable}`).
|
|
273
|
+
|
|
274
|
+
**Where the worker comes from.** By default `WorkerPCA` spawns the packaged entry via
|
|
275
|
+
`new Worker(new URL('./worker.js', import.meta.url), { type: 'module' })` — webpack 5, Vite
|
|
276
|
+
build, and native ESM all resolve that statically. If your toolchain can't, pass `{ worker }`:
|
|
277
|
+
|
|
278
|
+
| toolchain | recipe |
|
|
279
|
+
| --- | --- |
|
|
280
|
+
| webpack 5, native ESM | default works — or `{ worker: new Worker(new URL('pca-web/worker', import.meta.url), { type: 'module' }) }` |
|
|
281
|
+
| Vite | make a one-line entry `worker-entry.ts` containing `import 'pca-web/worker'`, pass `{ worker: () => new Worker(new URL('./worker-entry.ts', import.meta.url), { type: 'module' }) }`, and set `worker: { format: 'es' }` in `vite.config.ts` (what `examples/demo` does); or `import PcaWorker from 'pca-web/worker?worker'` → `{ worker: () => new PcaWorker() }`. In dev also add `'pca-web'` to `optimizeDeps.exclude`. |
|
|
282
|
+
| esbuild | bundle `pca-web/worker` as its own entry point and pass its URL: `{ worker: () => new Worker('/assets/pca-worker.js', { type: 'module' }) }` |
|
|
283
|
+
| Node ≥ 20 | `worker_threads` — bridge a `MessagePort` (below) |
|
|
284
|
+
|
|
285
|
+
Node (`worker_threads` `Worker` is an EventEmitter, not an EventTarget, so hand the client a
|
|
286
|
+
`MessagePort`, which is one):
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
// pca.worker.mjs — runs in the thread
|
|
290
|
+
import { workerData } from 'node:worker_threads';
|
|
291
|
+
import { attachPCAWorker } from 'pca-web/worker';
|
|
292
|
+
attachPCAWorker(workerData.port);
|
|
293
|
+
|
|
294
|
+
// main thread
|
|
295
|
+
import { MessageChannel, Worker } from 'node:worker_threads';
|
|
296
|
+
import { WorkerPCA } from 'pca-web/client';
|
|
297
|
+
const { port1, port2 } = new MessageChannel();
|
|
298
|
+
new Worker(new URL('./pca.worker.mjs', import.meta.url), {
|
|
299
|
+
workerData: { port: port2 },
|
|
300
|
+
transferList: [port2],
|
|
301
|
+
});
|
|
302
|
+
const pca = new WorkerPCA({ nComponents: 3, worker: port1 });
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`attachPCAWorker(port)` (from `pca-web/worker`) attaches the request handler to any
|
|
306
|
+
port-shaped endpoint; the packaged worker entry just calls it on the worker's global scope.
|
|
307
|
+
Requests on one worker execute strictly in order; aborts are handled out-of-band. Errors are
|
|
308
|
+
marshalled back `instanceof`-correct where it matters (`NotFittedError`, `AbortError`), and
|
|
309
|
+
`terminate()` rejects in-flight calls with `WorkerTerminatedError`.
|
|
310
|
+
|
|
311
|
+
## Model serialization
|
|
312
|
+
|
|
313
|
+
`toModel()` captures the complete fitted state as a plain object of typed arrays — cheap,
|
|
314
|
+
structured-clone-friendly (IndexedDB, `postMessage`, `structuredClone`), and validated on
|
|
315
|
+
rehydration. `PCA.fromModel(model)` / `IncrementalPCA.fromModel(model)` restore an estimator
|
|
316
|
+
whose every method output is **bit-identical** to the original's; a rehydrated
|
|
317
|
+
`IncrementalPCA` continues `partialFit` streams exactly as if never interrupted.
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
// IndexedDB (structured clone — no JSON, typed arrays stored directly)
|
|
321
|
+
store.put(pca.toModel(), 'my-model');
|
|
322
|
+
const pca2 = PCA.fromModel(await get(store, 'my-model'));
|
|
323
|
+
|
|
324
|
+
// JSON when you need text (bit-exact round-trip for f64 and f32)
|
|
325
|
+
localStorage.setItem('m', modelToJSON(pca.toModel()));
|
|
326
|
+
const pca3 = PCA.fromModel(modelFromJSON(localStorage.getItem('m')!) as PCAModel);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
The worker proxies speak the same format: `exportModel()` (synchronous, from the client-side
|
|
330
|
+
mirror) and `WorkerPCA.fromModel(model, options)`, which hydrates the worker without a refit.
|
|
331
|
+
Models carry a `formatVersion`; `assertValidModel` rejects malformed or future-versioned input.
|
|
332
|
+
|
|
333
|
+
## Numerical parity and tolerances
|
|
334
|
+
|
|
335
|
+
Parity is defined by tests, not intent. `python/generate_fixtures.py` runs real
|
|
336
|
+
scikit-learn 1.9.0 (numpy 2.5.1) over 96 PCA/IncrementalPCA cases — every solver, every
|
|
337
|
+
`nComponents` mode, whiten on/off, float32/float64, rank-deficient and badly scaled data,
|
|
338
|
+
per-step `partial_fit` state — plus RNG-stream and LAPACK-kernel fixtures, and commits the
|
|
339
|
+
outputs (`fixtures/`, ~11 MB). The TypeScript suite fits the same inputs and compares **every**
|
|
340
|
+
fitted attribute and method output elementwise — including signs; comparisons are never
|
|
341
|
+
up-to-sign.
|
|
342
|
+
|
|
343
|
+
Assertion bounds by class (all cases pass; see `tests/pca-parity.test.ts` for the full table):
|
|
344
|
+
|
|
345
|
+
| class | components | explained variance (rel) | transforms | notes |
|
|
346
|
+
| --- | --- | --- | --- | --- |
|
|
347
|
+
| float64 `full`/`arpack` | 1e-9 abs / 1e-7 rel | 1e-9 | 1e-8 abs / 1e-7 rel | observed component diffs ≤ ~1e-12 on unit-scale data |
|
|
348
|
+
| float64 `covariance_eigh` | 1e-9 abs / 1e-7 rel | 1e-9 | 1e-7 abs / 1e-6 rel | Gram route squares the condition number; tail singular values carry a ~√eps noise floor |
|
|
349
|
+
| float64 `randomized` (LU/QR/auto) | 1e-7 abs / 1e-5 rel | 1e-7 | 1e-6 abs / 1e-5 rel | same-seed comparison; observed ≤ ~1e-10 |
|
|
350
|
+
| float32 (all solvers) | 2e-3 | 2e-3 | 5e-3 | f32 rounding dominates |
|
|
351
|
+
| IncrementalPCA float64 | 1e-8 abs / 1e-6 rel | 1e-8 | 1e-7 | observed ≤ ~6e-13; mean/var bit-exact |
|
|
352
|
+
|
|
353
|
+
Documented caveats, discovered and verified during porting:
|
|
354
|
+
|
|
355
|
+
- **`noiseVariance`** is the cancellation `(totalVar − Σev)/(min − k)`; its relative error is
|
|
356
|
+
the explained-variance error amplified by ~totalVar/noiseVar. Bounds: 1e-11 abs (roundoff-
|
|
357
|
+
scale zeros) / 1e-9 rel deterministic, 1e-4 rel randomized.
|
|
358
|
+
- **`powerIterationNormalizer: 'none'`** (explicitly requested, ≥3 iterations) is documented
|
|
359
|
+
by sklearn itself as numerically unstable: trailing directions carry (σᵢ/σ₁)^(2·nIter+1)
|
|
360
|
+
≈ 1e-13 relative weight, and sklearn's own components move ~2e-4 under a **1-ulp** input
|
|
361
|
+
perturbation (measured). The suite compares that configuration in a dedicated loose class
|
|
362
|
+
(components 5e-3 abs); no implementation can do better than the algorithm's conditioning.
|
|
363
|
+
- **Rank deficiency:** components beyond the numerical rank span an arbitrary orthonormal
|
|
364
|
+
basis of the null space — LAPACK's choice and this library's legitimately differ. The suite
|
|
365
|
+
masks comparisons beyond the fixture rank (their variances are still compared, near zero),
|
|
366
|
+
and skips `inverseTransform` comparisons of out-of-span test data when rank < k.
|
|
367
|
+
- **Score/log-likelihood** values are large negatives; they agree to ~1e-10 relative
|
|
368
|
+
(deterministic f64).
|
|
369
|
+
- **Deliberate deviations:** fits with fewer than 2 samples throw a clear error (sklearn
|
|
370
|
+
returns an all-NaN model with a `RuntimeWarning`). All-constant (zero-variance) data keeps
|
|
371
|
+
sklearn's exact behavior: a well-formed model whose `explainedVarianceRatio` is NaN (0/0).
|
|
372
|
+
|
|
373
|
+
## Demo app
|
|
374
|
+
|
|
375
|
+
`examples/demo/` is a Vite app exercising the whole feature surface — worker vs main-thread
|
|
376
|
+
execution, live progress with snapshot embeddings, abort, WebGPU-in-worker with adapter
|
|
377
|
+
readout, IncrementalPCA `partialFit` streaming, eigen-digit/reconstruction/outlier panels, and
|
|
378
|
+
model persistence (JSON + IndexedDB restore-without-refit). It consumes the library strictly
|
|
379
|
+
through its public package exports, doubling as a packaging test.
|
|
380
|
+
|
|
381
|
+
```sh
|
|
382
|
+
npm run demo:dev # build the library, then vite dev
|
|
383
|
+
npm run demo:build # static build (examples/demo/dist)
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
The deploy workflow (`.github/workflows/deploy-pages.yml`) publishes it to GitHub Pages on
|
|
387
|
+
every push to `main`; one-time repo setup: **Settings → Pages → Source: "GitHub Actions"**.
|
|
388
|
+
The bundled digits dataset is regenerated by `npm run demo:data` (same venv as fixtures).
|
|
389
|
+
|
|
390
|
+
## Testing and development
|
|
391
|
+
|
|
392
|
+
```sh
|
|
393
|
+
npm test # Node suite: parity vs sklearn fixtures + API behavior,
|
|
394
|
+
# async/observer bit-equivalence, abort, model round-trips,
|
|
395
|
+
# worker protocol/parity over MessageChannel (275 tests)
|
|
396
|
+
npm run test:coverage # the same suite with a V8 coverage report (report-only)
|
|
397
|
+
npm run test:browser # real-GPU harness (Playwright Chromium); reports exactly which
|
|
398
|
+
# cases executed on which adapter, and fails if the GPU didn't run
|
|
399
|
+
npm run test:browser:worker # real Worker in Chromium: bit-equal results, progress on the
|
|
400
|
+
# main thread, mid-fit abort, transfer semantics, WebGPU-in-worker
|
|
401
|
+
npm run typecheck && npm run lint && npm run build
|
|
402
|
+
npm run bench # CPU timings (Node)
|
|
403
|
+
npm run bench:browser # CPU vs GPU timings (browser)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Regenerating fixtures requires Python with the pinned reference stack (scikit-learn 1.9.0,
|
|
407
|
+
numpy 2.5.1, scipy 1.18.0 — `python/requirements.txt`):
|
|
408
|
+
|
|
409
|
+
```sh
|
|
410
|
+
python -m venv .venv && .venv/bin/pip install -r python/requirements.txt
|
|
411
|
+
npm run fixtures # rewrites fixtures/ deterministically
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
The numerics core (`src/numeric/`) — Golub–Reinsch SVD, symmetric tridiagonal eigensolver,
|
|
415
|
+
Householder QR with LAPACK `dlarfg` semantics, partial-pivot LU (`scipy permute_l`
|
|
416
|
+
compatible), Golub–Kahan–Lanczos, Minka MLE, and the MT19937 `RandomState` replica — is
|
|
417
|
+
dependency-free TypeScript, unit-tested against numpy/scipy fixtures.
|
|
418
|
+
|
|
419
|
+
## License
|
|
420
|
+
|
|
421
|
+
MIT
|
package/dist/base.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared machinery for PCA and IncrementalPCA — the counterpart of sklearn's
|
|
3
|
+
* `_BasePCA`: transform / inverseTransform / getCovariance / getPrecision /
|
|
4
|
+
* getFeatureNamesOut operating on the fitted state.
|
|
5
|
+
*/
|
|
6
|
+
import { Matrix, type MatrixInput } from './matrix.js';
|
|
7
|
+
import { type PCAModelBaseFields } from './model.js';
|
|
8
|
+
import { type Dtype, type FloatArray } from './types.js';
|
|
9
|
+
export declare function castTo(a: Float64Array, dtype: Dtype): FloatArray;
|
|
10
|
+
export declare function promoteDtype(a: Dtype, b: Dtype): Dtype;
|
|
11
|
+
export declare abstract class BasePCA {
|
|
12
|
+
protected readonly estimatorName: string;
|
|
13
|
+
protected fitted: boolean;
|
|
14
|
+
/** True while a fit generator is live — re-entrant fits on one instance throw. */
|
|
15
|
+
protected fitting: boolean;
|
|
16
|
+
protected dtype: Dtype;
|
|
17
|
+
protected whitenOpt: boolean;
|
|
18
|
+
protected components_: Matrix | null;
|
|
19
|
+
protected explainedVariance_: FloatArray | null;
|
|
20
|
+
protected explainedVarianceRatio_: FloatArray | null;
|
|
21
|
+
protected singularValues_: FloatArray | null;
|
|
22
|
+
protected mean_: FloatArray | null;
|
|
23
|
+
protected nComponents_: number;
|
|
24
|
+
protected nFeaturesIn_: number;
|
|
25
|
+
protected noiseVariance_: number;
|
|
26
|
+
protected assertFitted(): void;
|
|
27
|
+
/** Principal axes (nComponents × nFeatures) — sklearn's `components_`. */
|
|
28
|
+
get components(): Matrix;
|
|
29
|
+
/** sklearn's `explained_variance_`. */
|
|
30
|
+
get explainedVariance(): FloatArray;
|
|
31
|
+
/**
|
|
32
|
+
* sklearn's `explained_variance_ratio_`. For zero-variance (all-constant)
|
|
33
|
+
* training data the total variance is 0, so every entry is NaN — the same
|
|
34
|
+
* 0/0 sklearn produces (alongside a RuntimeWarning) in that case.
|
|
35
|
+
*/
|
|
36
|
+
get explainedVarianceRatio(): FloatArray;
|
|
37
|
+
/** sklearn's `singular_values_`. */
|
|
38
|
+
get singularValues(): FloatArray;
|
|
39
|
+
/** Per-feature training mean — sklearn's `mean_`. */
|
|
40
|
+
get mean(): FloatArray;
|
|
41
|
+
/** Estimated number of components — sklearn's `n_components_`. */
|
|
42
|
+
get nComponents(): number;
|
|
43
|
+
/** sklearn's `n_features_in_`. */
|
|
44
|
+
get nFeaturesIn(): number;
|
|
45
|
+
/** Tipping–Bishop noise variance — sklearn's `noise_variance_`. */
|
|
46
|
+
get noiseVariance(): number;
|
|
47
|
+
/** Whether whitening is enabled (constructor option). */
|
|
48
|
+
get whiten(): boolean;
|
|
49
|
+
/** Project X onto the principal components — sklearn's `transform`. */
|
|
50
|
+
transform(X: MatrixInput): Matrix;
|
|
51
|
+
protected transformCore(X: Matrix, xIsCentered: boolean): Matrix;
|
|
52
|
+
/**
|
|
53
|
+
* @internal Everything in transform downstream of the X·componentsᵀ
|
|
54
|
+
* product (mean-projection removal, whitening, dtype cast). The WebGPU
|
|
55
|
+
* frontend calls this with a device-computed projection; `xt` is consumed.
|
|
56
|
+
*/
|
|
57
|
+
_transformFromProjection(xt: Float64Array, n: number, xDtype: Dtype, xIsCentered: boolean): Matrix;
|
|
58
|
+
/** Map component-space data back to feature space — sklearn's `inverse_transform`. */
|
|
59
|
+
inverseTransform(X: MatrixInput): Matrix;
|
|
60
|
+
/** Model covariance `componentsᵀ diag(ev − nv) components + nv·I` — sklearn's `get_covariance`. */
|
|
61
|
+
getCovariance(): Matrix;
|
|
62
|
+
protected covarianceF64(): Float64Array;
|
|
63
|
+
/** Model precision via the matrix inversion lemma — sklearn's `get_precision`. */
|
|
64
|
+
getPrecision(): Matrix;
|
|
65
|
+
protected precisionF64(): Float64Array;
|
|
66
|
+
/** Output feature names, e.g. ['pca0', 'pca1', …] — sklearn's `get_feature_names_out`. */
|
|
67
|
+
getFeatureNamesOut(): string[];
|
|
68
|
+
/**
|
|
69
|
+
* The shared fitted attributes as tight copies — a snapshot fully
|
|
70
|
+
* decoupled from live estimator state, safe to transfer or persist.
|
|
71
|
+
*/
|
|
72
|
+
protected exportBaseModel(): PCAModelBaseFields;
|
|
73
|
+
/**
|
|
74
|
+
* Adopts a (validated) model's fields zero-copy and marks the estimator
|
|
75
|
+
* fitted. Callers own any defensive copying.
|
|
76
|
+
*/
|
|
77
|
+
protected importBaseModel(m: PCAModelBaseFields): void;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=base.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../src/base.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAY,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAG/E,OAAO,EAAE,KAAK,KAAK,EAAmB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAG1E,wBAAgB,MAAM,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,KAAK,GAAG,UAAU,CAEhE;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,GAAG,KAAK,CAEtD;AAED,8BAAsB,OAAO;IAC3B,SAAS,CAAC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAS;IAEjD,SAAS,CAAC,MAAM,UAAS;IACzB,kFAAkF;IAClF,SAAS,CAAC,OAAO,UAAS;IAC1B,SAAS,CAAC,KAAK,EAAE,KAAK,CAAa;IACnC,SAAS,CAAC,SAAS,UAAS;IAE5B,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;IAC5C,SAAS,CAAC,kBAAkB,EAAE,UAAU,GAAG,IAAI,CAAQ;IACvD,SAAS,CAAC,uBAAuB,EAAE,UAAU,GAAG,IAAI,CAAQ;IAC5D,SAAS,CAAC,eAAe,EAAE,UAAU,GAAG,IAAI,CAAQ;IACpD,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAQ;IAC1C,SAAS,CAAC,YAAY,SAAK;IAC3B,SAAS,CAAC,YAAY,SAAK;IAC3B,SAAS,CAAC,cAAc,SAAK;IAE7B,SAAS,CAAC,YAAY,IAAI,IAAI;IAU9B,0EAA0E;IAC1E,IAAI,UAAU,IAAI,MAAM,CAGvB;IAED,uCAAuC;IACvC,IAAI,iBAAiB,IAAI,UAAU,CAGlC;IAED;;;;OAIG;IACH,IAAI,sBAAsB,IAAI,UAAU,CAGvC;IAED,oCAAoC;IACpC,IAAI,cAAc,IAAI,UAAU,CAG/B;IAED,qDAAqD;IACrD,IAAI,IAAI,IAAI,UAAU,CAGrB;IAED,kEAAkE;IAClE,IAAI,WAAW,IAAI,MAAM,CAGxB;IAED,kCAAkC;IAClC,IAAI,WAAW,IAAI,MAAM,CAGxB;IAED,mEAAmE;IACnE,IAAI,aAAa,IAAI,MAAM,CAG1B;IAED,yDAAyD;IACzD,IAAI,MAAM,IAAI,OAAO,CAEpB;IAMD,uEAAuE;IACvE,SAAS,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM;IAQjC,SAAS,CAAC,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,GAAG,MAAM;IAQhE;;;;OAIG;IACH,wBAAwB,CACtB,EAAE,EAAE,YAAY,EAChB,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,KAAK,EACb,WAAW,EAAE,OAAO,GACnB,MAAM;IAuCT,sFAAsF;IACtF,gBAAgB,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM;IA4CxC,mGAAmG;IACnG,aAAa,IAAI,MAAM;IAMvB,SAAS,CAAC,aAAa,IAAI,YAAY;IAuCvC,kFAAkF;IAClF,YAAY,IAAI,MAAM;IAMtB,SAAS,CAAC,YAAY,IAAI,YAAY;IAmEtC,0FAA0F;IAC1F,kBAAkB,IAAI,MAAM,EAAE;IAc9B;;;OAGG;IACH,SAAS,CAAC,eAAe,IAAI,kBAAkB;IAiB/C;;;OAGG;IACH,SAAS,CAAC,eAAe,CAAC,CAAC,EAAE,kBAAkB,GAAG,IAAI;CAavD"}
|