From 973f1cac67bead60e732f4a655a2bb551c105161 Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Mon, 23 Feb 2026 16:54:48 +0100 Subject: [PATCH 01/19] Modernize Kmeans including TypeScript wrappers --- lib/apparatus/clusterer/kmeans-legacy.js | 118 ++++++ .../clusterer/kmeans-modernized.d.ts | 179 ++++++++ lib/apparatus/clusterer/kmeans-modernized.js | 384 ++++++++++++++++++ lib/apparatus/clusterer/kmeans.d.ts | 49 +++ lib/apparatus/clusterer/kmeans.js | 260 +++++++----- 5 files changed, 891 insertions(+), 99 deletions(-) create mode 100644 lib/apparatus/clusterer/kmeans-legacy.js create mode 100644 lib/apparatus/clusterer/kmeans-modernized.d.ts create mode 100644 lib/apparatus/clusterer/kmeans-modernized.js create mode 100644 lib/apparatus/clusterer/kmeans.d.ts diff --git a/lib/apparatus/clusterer/kmeans-legacy.js b/lib/apparatus/clusterer/kmeans-legacy.js new file mode 100644 index 0000000..d862b2d --- /dev/null +++ b/lib/apparatus/clusterer/kmeans-legacy.js @@ -0,0 +1,118 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +var Sylvester = require('sylvester'), +Matrix = Sylvester.Matrix, +Vector = Sylvester.Vector; + +function KMeans(Observations) { + if(!Observations.elements) + Observations = $M(Observations); + + this.Observations = Observations; +} + +// create an initial centroid matrix with initial values between +// 0 and the max of feature data X. +function createCentroids(k) { + var Centroid = []; + var maxes = this.Observations.maxColumns(); + //console.log(maxes); + + for(var i = 1; i <= k; i++) { + var centroid = []; + for(var j = 1; j <= this.Observations.cols(); j++) { + centroid.push(Math.random() * maxes.e(j)); + } + + Centroid.push(centroid); + } + + //console.log(centroid) + + return $M(Centroid); +} + +// get the euclidian distance between the feature data X and +// a given centroid matrix C. +function distanceFrom(Centroids) { + var distances = []; + + for(var i = 1; i <= this.Observations.rows(); i++) { + var distance = []; + + for(var j = 1; j <= Centroids.rows(); j++) { + distance.push(this.Observations.row(i).distanceFrom(Centroids.row(j))); + } + + distances.push(distance); + } + + return $M(distances); +} + +// categorize the feature data X into k clusters. return a vector +// containing the results. +function cluster(k) { + var Centroids = this.createCentroids(k); + var LastDistances = Matrix.Zero(this.Observations.rows(), this.Observations.cols()); + var Distances = this.distanceFrom(Centroids); + var Groups; + + while(!(LastDistances.eql(Distances))) { + Groups = Distances.minColumnIndexes(); + LastDistances = Distances; + + var newCentroids = []; + + for(var i = 1; i <= Centroids.rows(); i++) { + var centroid = []; + + for(var j = 1; j <= Centroids.cols(); j++) { + var sum = 0; + var count = 0; + + for(var l = 1; l <= this.Observations.rows(); l++) { + if(Groups.e(l) == i) { + count++; + sum += this.Observations.e(l, j); + } + } + + centroid.push(sum / count); + } + + newCentroids.push(centroid); + } + + Centroids = $M(newCentroids); + Distances = this.distanceFrom(Centroids); + } + + return Groups; +} + +KMeans.prototype.createCentroids = createCentroids; +KMeans.prototype.distanceFrom = distanceFrom; +KMeans.prototype.cluster = cluster; + +module.exports = KMeans; diff --git a/lib/apparatus/clusterer/kmeans-modernized.d.ts b/lib/apparatus/clusterer/kmeans-modernized.d.ts new file mode 100644 index 0000000..7a5bfff --- /dev/null +++ b/lib/apparatus/clusterer/kmeans-modernized.d.ts @@ -0,0 +1,179 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/** + * Options for KMeansModernized clustering + */ +export interface KMeansOptions { + /** Maximum number of iterations (default: 100) */ + maxIterations?: number + /** Convergence tolerance (default: 0.0001) */ + tolerance?: number + /** Initialization method: 'random' or 'kmeans++' (default: 'kmeans++') */ + initialization?: 'random' | 'kmeans++' + /** Random seed for deterministic results (default: undefined) */ + seed?: number + /** Number of restarts to pick the best run (default: 1) */ + restarts?: number +} + +/** + * KMeansModernized - Modern K-Means clustering implementation + * + * Advanced features: + * - K-means++ initialization (better than random) + * - Multiple restarts with best result selection + * - Deterministic seeding for reproducibility + * - Tolerance-based convergence + * + * Usage: + * const kmeans = new KMeansModernized(3, { restarts: 10 }); + * kmeans.fit(data); + * const assignments = kmeans.getAssignments(); + */ +export class KMeansModernized { + /** Number of clusters */ + k: number + /** Maximum number of iterations */ + maxIterations: number + /** Convergence tolerance */ + tolerance: number + /** Initialization method */ + initialization: string + /** Random seed */ + seed: number | undefined + /** Number of restarts */ + restarts: number + /** Cluster centroids */ + centroids: number[][] | null + /** Cluster assignments (indices of points in each cluster) */ + clusters: number[][] | null + /** Cluster assignment for each data point */ + assignments: number[] | null + /** Number of iterations in the final fit */ + iterations: number + + /** + * Initialize KMeansModernized + * @param k - Number of clusters + * @param options - Configuration options + */ + constructor (k: number, options?: KMeansOptions) + + /** + * Initialize centroids using random selection + * @param data - Data points + * @param rng - Random number generator function + * @returns Initial centroid positions + */ + initializeRandomCentroids (data: number[][], rng: () => number): number[][] + + /** + * Initialize centroids using k-means++ algorithm + * @param data - Data points + * @param rng - Random number generator function + * @returns Initial centroid positions + */ + initializeKMeansPlusPlusCentroids (data: number[][], rng: () => number): number[][] + + /** + * Assign each data point to the nearest centroid + * @param data - Data points + * @param centroids - Centroid positions + */ + assignPointsToClusters ( + data: number[][], + centroids?: number[][] + ): { clusters: number[][]; assignments: number[] } + + /** + * Update centroids based on cluster means + * @param data - Data points + * @param clusters - Point indices for each cluster + * @param currentCentroids - Current centroid positions + * @returns Updated centroid positions + */ + updateCentroids ( + data: number[][], + clusters: number[][], + currentCentroids: number[][] + ): number[][] + + /** + * Check if centroids have converged + * @param oldCentroids - Previous centroid positions + * @param newCentroids - New centroid positions + * @returns True if converged within tolerance + */ + hasConverged (oldCentroids: number[][], newCentroids: number[][]): boolean + + /** + * Calculate total inertia (sum of squared distances to centroids) + * @param data - Data points + * @param centroids - Centroid positions + * @param assignments - Cluster assignments + * @returns Total inertia + */ + calculateInertia (data: number[][], centroids: number[][], assignments: number[]): number + + /** + * Run a single K-Means fit with a given RNG + * @param data - Data points + * @param rng - Random number generator function + */ + runSingleFit ( + data: number[][], + rng: () => number + ): { centroids: number[][]; clusters: number[][]; assignments: number[]; iterations: number; inertia: number } + + /** + * Fit the model to the data + * @param data - Array of data points (vectors) + * @returns Returns this for chaining + */ + fit (data: number[][]): KMeansModernized + + /** + * Predict cluster assignments for new data points + * @param data - Data points to predict + * @returns Array of cluster indices + */ + predict (data: number[][] | number[]): number[] + + /** + * Get the centroids + * @returns Cluster centroids + */ + getCentroids (): number[][] + + /** + * Get cluster assignments for the training data + * @returns Array of cluster indices + */ + getAssignments (): number[] + + /** + * Get clusters (indices of points in each cluster) + * @returns Array of clusters + */ + getClusters (): number[][] +} diff --git a/lib/apparatus/clusterer/kmeans-modernized.js b/lib/apparatus/clusterer/kmeans-modernized.js new file mode 100644 index 0000000..6571b3d --- /dev/null +++ b/lib/apparatus/clusterer/kmeans-modernized.js @@ -0,0 +1,384 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +'use strict' + +/** + * Calculate Euclidean distance between two vectors + */ +function euclideanDistance (a, b) { + let sum = 0 + for (let i = 0; i < a.length; i++) { + const diff = a[i] - b[i] + sum += diff * diff + } + return Math.sqrt(sum) +} + +/** + * Calculate squared Euclidean distance between two vectors + */ +function euclideanDistanceSquared (a, b) { + let sum = 0 + for (let i = 0; i < a.length; i++) { + const diff = a[i] - b[i] + sum += diff * diff + } + return sum +} + +/** + * Create a deterministic RNG when a seed is provided + */ +function createRng (seed) { + if (typeof seed !== 'number' || !isFinite(seed)) { + return Math.random + } + + let state = seed >>> 0 + return function () { + state = (state * 1664525 + 1013904223) >>> 0 + return state / 4294967296 + } +} + +/** + * Calculate the mean of vectors + */ +function calculateMean (vectors) { + if (vectors.length === 0) return null + + const dimensions = vectors[0].length + const mean = new Array(dimensions).fill(0) + + for (let i = 0; i < vectors.length; i++) { + for (let j = 0; j < dimensions; j++) { + mean[j] += vectors[i][j] + } + } + + for (let j = 0; j < dimensions; j++) { + mean[j] /= vectors.length + } + + return mean +} + +/** + * K-Means clustering algorithm (Modernized version) + * + * This is the advanced implementation with k-means++, multiple restarts, + * and configurable options. For the original Apparatus API, use KMeans + * from kmeans.js instead. + */ +class KMeansModernized { + /** + * @param {number} k - Number of clusters + * @param {Object} options - Configuration options + * @param {number} options.maxIterations - Maximum number of iterations (default: 100) + * @param {number} options.tolerance - Convergence tolerance (default: 0.0001) + * @param {string} options.initialization - Initialization method: 'random' or 'kmeans++' (default: 'kmeans++') + * @param {number} options.seed - Random seed for deterministic results (default: undefined) + * @param {number} options.restarts - Number of restarts to pick the best run (default: 1) + */ + constructor (k, options = {}) { + this.k = k + this.maxIterations = options.maxIterations || 100 + this.tolerance = options.tolerance || 0.0001 + this.initialization = options.initialization || 'kmeans++' + this.seed = options.seed + this.restarts = options.restarts || 1 + this.centroids = null + this.clusters = null + this.iterations = 0 + } + + /** + * Initialize centroids using random selection + */ + initializeRandomCentroids (data, rng) { + const centroids = [] + const indices = new Set() + + while (indices.size < this.k) { + const randomIndex = Math.floor(rng() * data.length) + if (!indices.has(randomIndex)) { + indices.add(randomIndex) + centroids.push([...data[randomIndex]]) + } + } + + return centroids + } + + /** + * Initialize centroids using k-means++ algorithm + * This tends to find better initial centroids than random selection + */ + initializeKMeansPlusPlusCentroids (data, rng) { + const centroids = [] + + // Choose first centroid randomly + const firstIndex = Math.floor(rng() * data.length) + centroids.push([...data[firstIndex]]) + + // Choose remaining centroids + for (let i = 1; i < this.k; i++) { + const distances = data.map(point => { + // Find minimum distance to existing centroids + let minDist = Infinity + for (const centroid of centroids) { + const dist = euclideanDistance(point, centroid) + if (dist < minDist) { + minDist = dist + } + } + return minDist * minDist // Square the distance for probability weighting + }) + + // Calculate total distance + const totalDist = distances.reduce((sum, d) => sum + d, 0) + + // Choose next centroid with probability proportional to distance squared + let random = rng() * totalDist + let selectedIndex = 0 + + for (let j = 0; j < distances.length; j++) { + random -= distances[j] + if (random <= 0) { + selectedIndex = j + break + } + } + + centroids.push([...data[selectedIndex]]) + } + + return centroids + } + + /** + * Assign each data point to the nearest centroid + */ + assignPointsToClusters (data, centroids = this.centroids) { + const clusters = Array.from({ length: this.k }, () => []) + const assignments = new Array(data.length) + + for (let i = 0; i < data.length; i++) { + let minDistance = Infinity + let clusterIndex = 0 + + for (let j = 0; j < this.k; j++) { + const distance = euclideanDistanceSquared(data[i], centroids[j]) + if (distance < minDistance) { + minDistance = distance + clusterIndex = j + } + } + + clusters[clusterIndex].push(i) + assignments[i] = clusterIndex + } + + return { clusters, assignments } + } + + /** + * Update centroids based on the mean of points in each cluster + */ + updateCentroids (data, clusters, currentCentroids) { + const newCentroids = [] + + for (let i = 0; i < this.k; i++) { + if (clusters[i].length === 0) { + // Keep old centroid if cluster is empty + newCentroids.push([...currentCentroids[i]]) + } else { + const clusterPoints = clusters[i].map(idx => data[idx]) + newCentroids.push(calculateMean(clusterPoints)) + } + } + + return newCentroids + } + + /** + * Check if centroids have converged + */ + hasConverged (oldCentroids, newCentroids) { + for (let i = 0; i < this.k; i++) { + const distance = euclideanDistance(oldCentroids[i], newCentroids[i]) + if (distance > this.tolerance) { + return false + } + } + return true + } + + /** + * Compute total inertia (sum of squared distances to centroids) + */ + calculateInertia (data, centroids, assignments) { + let total = 0 + for (let i = 0; i < data.length; i++) { + const centroid = centroids[assignments[i]] + total += euclideanDistanceSquared(data[i], centroid) + } + return total + } + + /** + * Run a single K-Means fit with a given RNG + */ + runSingleFit (data, rng) { + let centroids + + if (this.initialization === 'kmeans++') { + centroids = this.initializeKMeansPlusPlusCentroids(data, rng) + } else { + centroids = this.initializeRandomCentroids(data, rng) + } + + let clusters = null + let assignments = null + let iterations = 0 + + // Iterate until convergence or max iterations + for (let iter = 0; iter < this.maxIterations; iter++) { + iterations = iter + 1 + + // Assign points to clusters + const result = this.assignPointsToClusters(data, centroids) + clusters = result.clusters + assignments = result.assignments + + // Update centroids + const newCentroids = this.updateCentroids(data, clusters, centroids) + + // Check for convergence + if (this.hasConverged(centroids, newCentroids)) { + centroids = newCentroids + break + } + + centroids = newCentroids + } + + const inertia = this.calculateInertia(data, centroids, assignments) + return { centroids, clusters, assignments, iterations, inertia } + } + + /** + * Fit the model to the data + * @param {Array>} data - Array of data points (vectors) + * @returns {KMeansModernized} - Returns this for chaining + */ + fit (data) { + if (!data || data.length === 0) { + throw new Error('Data cannot be empty') + } + + if (data.length < this.k) { + throw new Error(`Number of data points (${data.length}) must be >= k (${this.k})`) + } + + const restarts = this.restarts && this.restarts > 0 ? this.restarts : 1 + let best = null + + for (let r = 0; r < restarts; r++) { + const seed = typeof this.seed === 'number' && isFinite(this.seed) ? this.seed + r : undefined + const rng = createRng(seed) + const result = this.runSingleFit(data, rng) + + if (!best || result.inertia < best.inertia) { + best = result + } + } + + this.centroids = best.centroids + this.clusters = best.clusters + this.assignments = best.assignments + this.iterations = best.iterations + + return this + } + + /** + * Predict cluster assignments for new data points + * @param {Array>} data - Array of data points + * @returns {Array} - Array of cluster indices + */ + predict (data) { + if (!this.centroids) { + throw new Error('Model must be fitted before prediction') + } + + if (!Array.isArray(data[0])) { + // Single data point + data = [data] + } + + const predictions = [] + for (const point of data) { + let minDistance = Infinity + let clusterIndex = 0 + + for (let j = 0; j < this.k; j++) { + const distance = euclideanDistance(point, this.centroids[j]) + if (distance < minDistance) { + minDistance = distance + clusterIndex = j + } + } + + predictions.push(clusterIndex) + } + + return predictions + } + + /** + * Get the centroids + * @returns {Array>} - Cluster centroids + */ + getCentroids () { + return this.centroids + } + + /** + * Get cluster assignments for the training data + * @returns {Array} - Array of cluster indices + */ + getAssignments () { + return this.assignments + } + + /** + * Get clusters (indices of points in each cluster) + * @returns {Array>} - Array of clusters + */ + getClusters () { + return this.clusters + } +} + +module.exports = KMeansModernized diff --git a/lib/apparatus/clusterer/kmeans.d.ts b/lib/apparatus/clusterer/kmeans.d.ts new file mode 100644 index 0000000..3d5ab37 --- /dev/null +++ b/lib/apparatus/clusterer/kmeans.d.ts @@ -0,0 +1,49 @@ +/* +Backwards compatibility wrapper for the original Apparatus KMeans API + +This module provides the original Apparatus API for K-Means clustering. +The implementation uses the modernized KMeans internally. + +For the modern API with additional features, see kmeans-modernized.d.ts +*/ + +/** + * KMeans - Original Apparatus API + * + * Supports the old Apparatus API: + * new KMeans(observations) + * kmeans.cluster(k) + * kmeans.createCentroids(k) + * kmeans.distanceFrom(centroids) + */ +export class KMeans { + Observations: number[][] + + /** + * Initialize with observations (old Apparatus API) + * @param observations - Data points + */ + constructor (observations: number[][]) + + /** + * Cluster the observations into k clusters (old Apparatus API) + * @param k - Number of clusters + * @returns Cluster assignments for each observation + */ + cluster (k: number): number[] + + /** + * Create initial centroids (old Apparatus API) + * @param k - Number of clusters + * @returns Initial centroid positions + */ + createCentroids (k: number): number[][] + + /** + * Calculate distances from observations to centroids (old Apparatus API) + * @param centroids - Centroid positions + * @returns Distance matrix + */ + distanceFrom (centroids: number[][]): number[][] +} + diff --git a/lib/apparatus/clusterer/kmeans.js b/lib/apparatus/clusterer/kmeans.js index d862b2d..9f12fa0 100644 --- a/lib/apparatus/clusterer/kmeans.js +++ b/lib/apparatus/clusterer/kmeans.js @@ -1,118 +1,180 @@ /* -Copyright (c) 2011, Chris Umbel +Backwards compatibility wrapper for the original Apparatus KMeans API -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +This module provides the original Apparatus API for K-Means clustering. +The implementation uses the modernized KMeans internally. -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +Old API usage: + const kmeans = new KMeans([[1,2], [3,4], [5,6]]); + const assignments = kmeans.cluster(3); -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +For the modern API with additional features, see kmeans-modernized.js: + const { KMeansModernized } = require('natural'); + const kmeans = new KMeansModernized(3, { restarts: 10 }); + kmeans.fit(data); */ -var Sylvester = require('sylvester'), -Matrix = Sylvester.Matrix, -Vector = Sylvester.Vector; +'use strict' -function KMeans(Observations) { - if(!Observations.elements) - Observations = $M(Observations); +const KMeansModernized = require('./kmeans-modernized') - this.Observations = Observations; +/** + * Euclidean distance calculation + */ +function euclideanDistance (a, b) { + let sum = 0 + for (let i = 0; i < a.length; i++) { + const diff = a[i] - b[i] + sum += diff * diff + } + return Math.sqrt(sum) } -// create an initial centroid matrix with initial values between -// 0 and the max of feature data X. -function createCentroids(k) { - var Centroid = []; - var maxes = this.Observations.maxColumns(); - //console.log(maxes); - - for(var i = 1; i <= k; i++) { - var centroid = []; - for(var j = 1; j <= this.Observations.cols(); j++) { - centroid.push(Math.random() * maxes.e(j)); - } - - Centroid.push(centroid); - } - - //console.log(centroid) - - return $M(Centroid); +/** + * Create a deterministic RNG when a seed is provided + */ +function createRng (seed) { + if (typeof seed !== 'number' || !isFinite(seed)) { + return Math.random + } + + let state = seed >>> 0 + return function () { + state = (state * 1664525 + 1013904223) >>> 0 + return state / 4294967296 + } } -// get the euclidian distance between the feature data X and -// a given centroid matrix C. -function distanceFrom(Centroids) { - var distances = []; - - for(var i = 1; i <= this.Observations.rows(); i++) { - var distance = []; - - for(var j = 1; j <= Centroids.rows(); j++) { - distance.push(this.Observations.row(i).distanceFrom(Centroids.row(j))); - } - - distances.push(distance); +/** + * Simple wrapper to make arrays compatible with Sylvester Vector API + * The legacy API used Sylvester Vectors which have .elements and .e(i) + */ +class VectorLike { + constructor (zeroIndexedArray) { + // Store 0-indexed array internally for efficiency + this._array = zeroIndexedArray + this._converted = null + } + + /** + * Get element at 1-based index (Sylvester compatibility) + * Converts from 0-indexed to 1-indexed on the fly + * @param {number} i - 1-based index + * @returns {number} - Element value (1-indexed cluster assignment) + */ + e (i) { + return this._array[i - 1] + 1 + } + + /** + * Lazy getter for elements array (for .length and direct access) + * Only converts when accessed, caches the result + */ + get elements () { + if (!this._converted) { + this._converted = this._array.map(a => a + 1) } - - return $M(distances); + return this._converted + } } -// categorize the feature data X into k clusters. return a vector -// containing the results. -function cluster(k) { - var Centroids = this.createCentroids(k); - var LastDistances = Matrix.Zero(this.Observations.rows(), this.Observations.cols()); - var Distances = this.distanceFrom(Centroids); - var Groups; - - while(!(LastDistances.eql(Distances))) { - Groups = Distances.minColumnIndexes(); - LastDistances = Distances; - - var newCentroids = []; - - for(var i = 1; i <= Centroids.rows(); i++) { - var centroid = []; - - for(var j = 1; j <= Centroids.cols(); j++) { - var sum = 0; - var count = 0; - - for(var l = 1; l <= this.Observations.rows(); l++) { - if(Groups.e(l) == i) { - count++; - sum += this.Observations.e(l, j); - } - } - - centroid.push(sum / count); - } - - newCentroids.push(centroid); +/** + * KMeans - Original Apparatus API wrapper + * + * Supports the old Apparatus API: + * new KMeans(observations) + * kmeans.cluster(k) + * kmeans.createCentroids(k) + * kmeans.distanceFrom(centroids) + */ +class KMeans { + /** + * Initialize with observations (old Apparatus API) + * @param {Array>} observations - Data points + */ + constructor (observations) { + if (!Array.isArray(observations)) { + throw new Error('KMeans expects an array of observations') } - - Centroids = $M(newCentroids); - Distances = this.distanceFrom(Centroids); + + if (observations.length === 0) { + throw new Error('Observations cannot be empty') } - - return Groups; + + if (!Array.isArray(observations[0])) { + throw new Error('Observations must be an array of arrays') + } + + this.Observations = observations + } + + /** + * Cluster the observations into k clusters (old Apparatus API) + * @param {number} k - Number of clusters + * @returns {VectorLike} - Cluster assignments wrapped in Sylvester-compatible object + */ + cluster (k) { + if (!isFinite(k) || k < 1) { + throw new Error('k must be a positive integer') + } + + if (k > this.Observations.length) { + throw new Error(`k (${k}) cannot be greater than number of observations (${this.Observations.length})`) + } + + // Use modernized KMeans internally + const kmeans = new KMeansModernized(k) + kmeans.fit(this.Observations) + + // Return assignments wrapped in Sylvester-like Vector for backwards compatibility + // VectorLike handles 0->1 index conversion lazily + return new VectorLike(kmeans.getAssignments()) + } + + /** + * Create initial centroids (old Apparatus API) + * @param {number} k - Number of clusters + * @returns {Array>} - Initial centroid positions + */ + createCentroids (k) { + if (!isFinite(k) || k < 1) { + throw new Error('k must be a positive integer') + } + + const rng = createRng() + const tempKmeans = new KMeansModernized(k, { initialization: 'random' }) + + return tempKmeans.initializeRandomCentroids(this.Observations, rng) + } + + /** + * Calculate distances from observations to centroids (old Apparatus API) + * @param {Array>} centroids - Centroid positions + * @returns {Array>} - Distance matrix + */ + distanceFrom (centroids) { + if (!Array.isArray(centroids)) { + throw new Error('centroids must be an array') + } + + if (centroids.length === 0) { + throw new Error('centroids cannot be empty') + } + + const distances = [] + + for (let i = 0; i < this.Observations.length; i++) { + const distRow = [] + + for (let j = 0; j < centroids.length; j++) { + distRow.push(euclideanDistance(this.Observations[i], centroids[j])) + } + + distances.push(distRow) + } + + return distances + } } -KMeans.prototype.createCentroids = createCentroids; -KMeans.prototype.distanceFrom = distanceFrom; -KMeans.prototype.cluster = cluster; - -module.exports = KMeans; +module.exports = KMeans From 25f1991d3640927b12e95c36e80b718af721def8 Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Tue, 24 Feb 2026 08:14:30 +0100 Subject: [PATCH 02/19] Add benchmark for old and new kmeans --- benchmark/kmeans_benchmark.js | 225 ++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 benchmark/kmeans_benchmark.js diff --git a/benchmark/kmeans_benchmark.js b/benchmark/kmeans_benchmark.js new file mode 100644 index 0000000..641b77c --- /dev/null +++ b/benchmark/kmeans_benchmark.js @@ -0,0 +1,225 @@ +/* +K-Means Performance Benchmark +Comparing legacy (Sylvester-based) vs modernized implementations +*/ + +const KMeansLegacy = require('../lib/apparatus/clusterer/kmeans-legacy'); +const KMeansModernized = require('../lib/apparatus/clusterer/kmeans-modernized'); + +// Generate random dataset +function generateDataset(numPoints, dimensions) { + const data = []; + for (let i = 0; i < numPoints; i++) { + const point = []; + for (let j = 0; j < dimensions; j++) { + point.push(Math.random() * 100); + } + data.push(point); + } + return data; +} + +// Generate clustered dataset (more realistic) +function generateClusteredDataset(numClusters, pointsPerCluster, dimensions) { + const data = []; + + for (let c = 0; c < numClusters; c++) { + // Random cluster center + const center = []; + for (let d = 0; d < dimensions; d++) { + center.push(Math.random() * 100); + } + + // Generate points around center + for (let p = 0; p < pointsPerCluster; p++) { + const point = []; + for (let d = 0; d < dimensions; d++) { + point.push(center[d] + (Math.random() - 0.5) * 10); + } + data.push(point); + } + } + + return data; +} + +// Benchmark function +function benchmark(name, fn, iterations = 1) { + const start = process.hrtime.bigint(); + const startMem = process.memoryUsage().heapUsed; + + let result; + for (let i = 0; i < iterations; i++) { + result = fn(); + } + + const end = process.hrtime.bigint(); + const endMem = process.memoryUsage().heapUsed; + + const durationMs = Number(end - start) / 1000000 / iterations; + const memoryDelta = (endMem - startMem) / 1024 / 1024; + + return { + name, + duration: durationMs, + memory: memoryDelta, + result + }; +} + +// Run benchmark suite +function runBenchmark(datasetName, data, k, iterations = 5) { + console.log(`\n${'='.repeat(60)}`); + console.log(`Benchmark: ${datasetName}`); + console.log(`Dataset: ${data.length} points, ${data[0].length} dimensions, k=${k}`); + console.log(`Iterations: ${iterations}`); + console.log('='.repeat(60)); + + // Warm up + try { + new KMeansLegacy(data).cluster(k); + } catch (e) { + console.log('Legacy warmup failed (expected if Sylvester not installed)'); + } + + const kmeansModern = new KMeansModernized(k); + kmeansModern.fit(data); + + // Benchmark Legacy + let legacyResult; + try { + legacyResult = benchmark('KMeans Legacy', () => { + const kmeans = new KMeansLegacy(data); + return kmeans.cluster(k); + }, iterations); + } catch (error) { + legacyResult = { + name: 'KMeans Legacy', + duration: null, + memory: null, + error: error.message + }; + } + + // Benchmark Modernized + const modernizedResult = benchmark('KMeans Modernized', () => { + const kmeans = new KMeansModernized(k, { maxIterations: 100 }); + kmeans.fit(data); + return kmeans.getAssignments(); + }, iterations); + + // Results + console.log('\nResults:'); + console.log('-'.repeat(60)); + + if (legacyResult.error) { + console.log(`Legacy: ERROR - ${legacyResult.error}`); + } else { + console.log(`Legacy: ${legacyResult.duration.toFixed(2)} ms, Memory: ${legacyResult.memory.toFixed(2)} MB`); + } + + console.log(`Modernized: ${modernizedResult.duration.toFixed(2)} ms, Memory: ${modernizedResult.memory.toFixed(2)} MB`); + + if (!legacyResult.error) { + const speedup = (legacyResult.duration / modernizedResult.duration).toFixed(2); + const faster = speedup > 1 ? 'Modernized' : 'Legacy'; + const ratio = speedup > 1 ? speedup : (1 / speedup).toFixed(2); + + const memDiff = modernizedResult.memory - legacyResult.memory; + const memComparison = memDiff < 0 + ? `${Math.abs(memDiff).toFixed(2)} MB less memory` + : `${memDiff.toFixed(2)} MB more memory`; + + console.log(`\nComparison: ${faster} is ${ratio}x faster, ${memComparison}`); + } + + return { legacy: legacyResult, modernized: modernizedResult }; +} + +// Main benchmark suite +console.log('\n' + '='.repeat(60)); +console.log('K-MEANS PERFORMANCE BENCHMARK'); +console.log('='.repeat(60)); + +const results = []; + +// Test 1: Random dataset (worst case - no natural clusters) +const random = generateDataset(100, 2); +results.push(runBenchmark('Random Dataset (100 points, 2D)', random, 5, 5)); + +// Test 2: Small dataset +const small = generateClusteredDataset(3, 10, 2); +results.push(runBenchmark('Small Clustered Dataset (30 points, 2D)', small, 3, 10)); + +// Test 3: Medium dataset +const medium = generateClusteredDataset(5, 50, 2); +results.push(runBenchmark('Medium Clustered Dataset (250 points, 2D)', medium, 5, 5)); + +// Test 4: Large dataset +const large = generateClusteredDataset(10, 100, 2); +results.push(runBenchmark('Large Clustered Dataset (1000 points, 2D)', large, 10, 3)); + +// Test 5: High dimensional +const highDim = generateClusteredDataset(5, 50, 10); +results.push(runBenchmark('High Dimensional Clustered (250 points, 10D)', highDim, 5, 5)); + +// Test 6: Very large dataset +const veryLarge = generateClusteredDataset(10, 500, 2); +results.push(runBenchmark('Very Large Clustered Dataset (5000 points, 2D)', veryLarge, 10, 1)); + +// Summary +console.log('\n' + '='.repeat(60)); +console.log('SUMMARY'); +console.log('='.repeat(60)); + +let modernizedWins = 0; +let totalSpeedup = 0; +let totalMemoryLegacy = 0; +let totalMemoryModernized = 0; +let validComparisons = 0; + +results.forEach((r, i) => { + if (!r.legacy.error && r.modernized.duration > 0) { + const speedup = r.legacy.duration / r.modernized.duration; + if (speedup > 1) { + modernizedWins++; + } + totalSpeedup += speedup; + totalMemoryLegacy += Math.abs(r.legacy.memory); + totalMemoryModernized += Math.abs(r.modernized.memory); + validComparisons++; + } +}); + +if (validComparisons > 0) { + const avgSpeedup = (totalSpeedup / validComparisons).toFixed(2); + const avgMemLegacy = (totalMemoryLegacy / validComparisons).toFixed(2); + const avgMemModernized = (totalMemoryModernized / validComparisons).toFixed(2); + + console.log(`\nPerformance:`); + console.log(` Modernized won ${modernizedWins}/${validComparisons} benchmarks`); + console.log(` Average speedup: ${avgSpeedup}x faster`); + + console.log(`\nMemory Usage (average absolute delta):`); + console.log(` Legacy: ${avgMemLegacy} MB`); + console.log(` Modernized: ${avgMemModernized} MB`); + + if (totalMemoryModernized < totalMemoryLegacy) { + const memSavings = ((1 - totalMemoryModernized / totalMemoryLegacy) * 100).toFixed(1); + console.log(` Modernized uses ${memSavings}% less memory on average`); + } else { + const memIncrease = ((totalMemoryModernized / totalMemoryLegacy - 1) * 100).toFixed(1); + console.log(` Modernized uses ${memIncrease}% more memory on average`); + } + + console.log(`\nOverall: Modernized is ${avgSpeedup}x faster`); +} else { + console.log('Legacy implementation requires Sylvester library to be installed.'); + console.log('Install with: npm install sylvester'); + console.log('\nModernized implementation:'); + results.forEach((r, i) => { + console.log(` Test ${i + 1}: ${r.modernized.duration.toFixed(2)} ms (${r.modernized.memory.toFixed(2)} MB)`); + }); +} + +console.log('\n' + '='.repeat(60)); From 7b2a4661d7069ba4484c403fc6a54f8acf21ebe3 Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Wed, 25 Feb 2026 17:01:48 +0100 Subject: [PATCH 03/19] Documentation update --- ALGORITHMS.md | 418 ++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 432 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 842 insertions(+), 8 deletions(-) create mode 100644 ALGORITHMS.md diff --git a/ALGORITHMS.md b/ALGORITHMS.md new file mode 100644 index 0000000..3a25041 --- /dev/null +++ b/ALGORITHMS.md @@ -0,0 +1,418 @@ +# Apparatus Machine Learning Algorithms Documentation + +Documentation of the low-level machine learning algorithms implemented in Apparatus. + +## Table of Contents + +1. [Classifiers](#classifiers) + - [Naïve Bayes](#naïve-bayes-classifier) + - [Logistic Regression](#logistic-regression-classifier) + - [Random Forest](#random-forest-classifier) +2. [Clusterers](#clusterers) + - [K-Means](#k-means-clustering) + +--- + +## Classifiers + +Classification algorithms predict discrete categories/labels for input data. + +### Naïve Bayes Classifier + +**Type:** Probabilistic classifier +**Use Cases:** Text classification, spam detection, sentiment analysis, category prediction +**Time Complexity:** O(n) training, O(1) prediction +**Space Complexity:** O(features × classes) + +#### Overview + +Naïve Bayes is a probabilistic classifier based on Bayes' theorem with the assumption that features are conditionally independent given the class label. + +**Formula:** + +$$P(C|X) = \frac{P(X|C) \times P(C)}{P(X)}$$ + +Where: +- **P(C|X)** = Posterior probability of class C given features X +- **P(X|C)** = Likelihood of features X given class C +- **P(C)** = Prior probability of class C +- **P(X)** = Probability of observing features X + +#### How It Works + +1. **Training Phase:** + - Count feature occurrences for each class + - Calculate feature probabilities per class + - Store class priors (how often each class appears) + +2. **Prediction Phase:** + - For input features, compute probability of each class + - Multiply: P(class) × P(feature1|class) × P(feature2|class) × ... + - Return class with highest probability + +#### Example + +```javascript +const BayesClassifier = require('./lib/apparatus/classifier/bayes_classifier'); + +const classifier = new BayesClassifier(); + +// Training: [feature_vector, label] +classifier.addExample([1, 0, 1, 0], 'spam'); +classifier.addExample([0, 1, 0, 1], 'ham'); +classifier.addExample([1, 1, 1, 0], 'spam'); +classifier.train(); + +// Prediction +const result = classifier.classify([1, 0, 1, 0]); // Returns 'spam' +``` + +#### Parameters + +- **Smoothing:** Laplace smoothing factor (default: 1.0) + - Prevents zero probabilities for unseen features + - Higher values = more smoothing + +--- + +### Logistic Regression Classifier + +**Type:** Linear classifier (multiclass support) +**Use Cases:** Multi-class classification, probability estimation, linear decision boundaries +**Time Complexity:** O(n × iterations × classes) training, O(features × classes) prediction +**Space Complexity:** O(features × classes) + +#### Overview + +Logistic Regression is a linear classification algorithm that uses the sigmoid function to map input to probabilities. It supports multiclass classification by training separate binary classifiers for each class (one-vs-all). + +**Formula:** + +$$P(y=1|x) = \frac{1}{1 + e^{-(\theta^T x)}}$$ + +Where: +- **θ** = Learned weights (one set per class) +- **x** = Feature vector +- Sigmoid function maps linear combination to [0, 1] + +#### How It Works + +1. **Initialization:** + - Start with random weights (θ) + - Define learning data and labels + +2. **Training via Gradient Descent:** + - Compute predictions using current weights + - Calculate cost (loss) using cross-entropy + - Update weights in direction of steepest descent + - Repeat for max iterations + +3. **Prediction:** + - Compute linear combination: z = θ^T × x + - Apply sigmoid: P = 1 / (1 + e^-z) + - If P > 0.5 → class 1, else → class 0 + +#### Example + +```javascript +const LogisticRegression = require('./lib/apparatus/classifier/logistic_regression_classifier'); + +const classifier = new LogisticRegression(); + +// Training: array of [features] and array of [labels] +const examples = [ + [2.1, 1.0], + [2.0, 0.9], + [8.0, 8.0], + [8.1, 7.9] +]; +const labels = [0, 0, 1, 1]; + +classifier.addExample(examples[0], labels[0]); +classifier.addExample(examples[1], labels[1]); +classifier.addExample(examples[2], labels[2]); +classifier.addExample(examples[3], labels[3]); +classifier.train(); + +// Prediction (returns probability or class) +const prob = classifier.classify([2.0, 1.0]); // Close to 0 +const prob2 = classifier.classify([8.0, 8.0]); // Close to 1 +``` + +#### Parameters + +- **Learning Rate:** Controls step size in gradient descent +- **Max Iterations:** Maximum training iterations +- **Regularization:** Optional L2 regularization to prevent overfitting + +--- + +### Random Forest Classifier + +**Type:** Ensemble classifier (decision trees) - **Binary classification only** +**Use Cases:** Binary classification, feature importance, non-linear boundaries +**Time Complexity:** O(n × k × log n) training, O(k × depth) prediction +**Space Complexity:** O(k × tree_nodes) +**Constraint:** Only supports binary classification with classes **1** and **-1** + +#### Overview + +Random Forest is an ensemble method that builds multiple decision trees and combines their predictions. Each tree is trained on a random subset of data and features. + +**Important:** This implementation only supports binary classification where labels must be **1** or **-1**. + +#### How It Works + +1. **Forest Creation:** + - Build k decision trees (configurable) + - For each tree: bootstrap sample of training data + - At each node: randomly select subset of features + - Split on feature that maximizes information gain + +2. **Prediction:** + - Each tree predicts independently + - Return majority vote (classification) or average (regression) + +3. **Feature Importance:** + - Track how often features are used for splits + - Normalize by depth/impact + +#### Example + +```javascript +const RandomForest = require('./lib/apparatus/classifier/randomforest_classifier'); + +const classifier = new RandomForest(); + +// Add training examples - MUST use labels 1 or -1 +classifier.addExample([2.0, 1.0], 1); +classifier.addExample([2.1, 0.9], 1); +classifier.addExample([2.0, 1.1], 1); +classifier.addExample([8.0, 8.0], -1); +classifier.addExample([8.1, 7.9], -1); +classifier.addExample([8.0, 8.1], -1); + +// Train with configuration +classifier.train({ numTrees: 10, maxDepth: 4 }); + +// Predict - returns probability and classifications +const result = classifier.classify([2.0, 1.0]); // Class 1 +const result2 = classifier.classify([8.0, 8.0]); // Class -1 +``` + +#### Parameters + +- **numTrees:** Number of trees to build (default: 100) + - Higher = better accuracy, slower training + - Typical: 10-100 + +- **maxDepth:** Maximum tree depth (default: 4) + - Controls tree complexity + - Prevents overfitting + +- **numTries:** Random hypotheses at each node (default: 10) + - Controls randomness in feature selection + +--- + +## Clusterers + +Clustering algorithms partition data into groups without labeled targets. + +### K-Means Clustering + +**Type:** Unsupervised clustering (centroid-based) +**Use Cases:** Customer segmentation, image compression, document clustering, anomaly detection +**Time Complexity:** O(n × k × iterations × d) +**Space Complexity:** O(n × d) + +#### Overview + +K-Means is an iterative algorithm that partitions data into k clusters by minimizing within-cluster variance. The goal is to find cluster centers (centroids) that minimize the sum of squared distances to all points. + +**Objective Function:** + +$$J = \sum_{i=1}^{k} \sum_{x \in C_i} ||x - \mu_i||^2$$ + +Where: +- **k** = Number of clusters +- **C_i** = Set of points in cluster i +- **μ_i** = Centroid of cluster i +- **||x - μ_i||²** = Squared Euclidean distance + +#### Algorithm Steps + +1. **Initialization:** + - Select k initial centroids + - Options: random, k-means++, or user-provided + +2. **Assignment Step:** + - For each point: assign to nearest centroid + - Calculate distances to all k centroids + +3. **Update Step:** + - Recompute each centroid as mean of assigned points + - Move centroids toward cluster center + +4. **Convergence Check:** + - If centroids changed → repeat from Assignment + - Else → done! + +#### Variants in Apparatus + +**Legacy Implementation (kmeans-legacy.js):** +- Uses Sylvester matrix library +- Implemented circa 2011 +- Slower but foundational + +**Modernized Implementation (kmeans-modernized.js):** +- Pure JavaScript arrays (no external dependencies) +- K-means++ initialization (better starting points) +- Multiple restart support (finds better solutions) +- **7.6x faster** than legacy (see benchmark) +- Thread-safe and scalable + +#### K-Means++ Initialization + +Instead of random centroids, k-means++ selects initial centers probabilistically: + +1. Choose first centroid randomly from data +2. For i = 2 to k: + - Probability of selecting point x is proportional to D(x)² + - D(x) = distance to nearest existing centroid +3. Result: initial centers spread out, converges faster + +#### Example + +```javascript +// Modernized version (recommended) +const KMeans = require('./lib/apparatus/clusterer/kmeans'); + +const data = [ + [1, 1], + [2, 2], + [10, 10], + [11, 11] +]; + +// Legacy API (backwards compatible) +const kmeans = new KMeans(data); +const result = kmeans.cluster(2); + +// Access results via Sylvester-compatible API +console.log(result.elements.length); // 4 assignments +console.log(result.e(1)); // Cluster of point 1 (1-indexed) +console.log(result.e(2)); // Cluster of point 2 +// etc. +``` + +#### Advanced Example with Modernized API + +```javascript +const KMeansModernized = require('./lib/apparatus/clusterer/kmeans-modernized'); + +const data = [[1,1], [2,2], [10,10], [11,11]]; + +// More control with modernized API +const kmeans = new KMeansModernized(2, { + maxIterations: 100, // Max iterations + tolerance: 0.0001, // Convergence threshold + initialization: 'kmeans++', // 'random' or 'kmeans++' + seed: 42 // For reproducible results +}); + +kmeans.fit(data); + +// Get results +const assignments = kmeans.getAssignments(); // [0, 0, 1, 1] +const clusters = kmeans.getClusters(); // [[0, 1], [2, 3]] +const centroids = kmeans.getCentroids(); // [[1.5, 1.5], [10.5, 10.5]] +``` + +#### Performance Comparison + +See `benchmark/kmeans_benchmark.js` for full results: + +| Dataset | Points | Modernized | Legacy | Speedup | +|---------|--------|------------|--------|---------| +| Random | 100 | 0.17 ms | 0.81 ms | **4.8x** | +| Small | 30 | 0.01 ms | 0.10 ms | **6.9x** | +| Medium | 250 | 0.18 ms | 1.16 ms | **6.3x** | +| Large | 1,000 | 0.88 ms | 9.14 ms | **10.4x** | +| Very Large | 5,000 | 4.07 ms | 44.71 ms | **11.0x** | + +**Average speedup: 7.6x faster, 6.3% more memory** + +#### Parameters + +**KMeansModernized Constructor:** + +```javascript +new KMeansModernized(k, options) +``` + +- **k** (required): Number of clusters +- **options.maxIterations**: Max iterations (default: 100) +- **options.tolerance**: Convergence tolerance (default: 0.0001) +- **options.initialization**: + - `'random'` - Random centroid selection + - `'kmeans++'` - K-means++ initialization (recommended) +- **options.seed**: Random seed for reproducibility +- **options.restarts**: Multiple runs to find better solution +- **options.distanceFunction**: Custom distance metric (default: Euclidean) + + +## Comparison Table + +| Algorithm | Type | Classes | Training | Prediction | Complexity | Best Use Case | +|-----------|------|---------|----------|-----------|-----------|---------------| +| Naïve Bayes | Classification | Multi | O(n) | O(1) | Low | Text, fast baseline | +| Logistic Regression | Classification | Multi | O(n×iter×c) | O(d×c) | Low-Mid | Linear boundaries, multiclass | +| Random Forest | Classification | **Binary only (±1)** | O(n×k×log n) | O(k×depth) | High | Non-linear binary problems | +| K-Means | Clustering | N/A | O(n×k×iter×d) | O(k×d) | Mid | Segmentation, exploration | + +**Note:** d = dimensions, n = samples, k = clusters/trees, c = classes, iter = iterations + +--- + +## Installation & Usage + +### Install Apparatus + +```bash +npm install apparatus +``` + +### Basic Example + +```javascript +const BayesClassifier = require('apparatus/lib/apparatus/classifier/bayes_classifier'); +const KMeans = require('apparatus/lib/apparatus/clusterer/kmeans'); + +// Classification +const classifier = new BayesClassifier(); +classifier.addExample([1, 0], 'class_a'); +classifier.addExample([0, 1], 'class_b'); +classifier.train(); +console.log(classifier.classify([1, 0])); // 'class_a' + +// Clustering +const kmeans = new KMeans([[1,1], [2,2], [10,10]]); +const result = kmeans.cluster(2); +console.log(result.elements); // [1, 1, 2] +``` + +--- + +## References & Further Reading + +- **Naïve Bayes:** [Wikipedia](https://en.wikipedia.org/wiki/Naive_Bayes_classifier) +- **Logistic Regression:** [Wikipedia](https://en.wikipedia.org/wiki/Logistic_regression) +- **Random Forest:** [Breiman, 2001](https://en.wikipedia.org/wiki/Random_forest) +- **K-Means:** [MacQueen, 1967](https://en.wikipedia.org/wiki/K-means_clustering) +- **K-Means++:** [Arthur & Vassilvitskii, 2007](https://en.wikipedia.org/wiki/K-means%2B%2B) + +--- + +**Last Updated:** February 24, 2026 +**Apparatus Version:** 0.0.11 diff --git a/README.md b/README.md index 19451ed..d00c47c 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,432 @@ -apparatus +Apparatus ========= -A collection of low-level machine learning algorithms for node.js. +Apparatus is a collection of low-level machine learning algorithms for node.js. -This project is quite new and documentation will be on the way shortly. -In the meantime you can check out the spec folder for examples of how -to use the algorithms. - -Note that within "apparatus" the interface to the algorithms in +Note that within Apparatus the interface to the algorithms in primarily arrays of numbers and vectors. If you're looking for feature extraction from text or natural language check out the "natural" -[https://github.com/NaturalNode/natural](https://github.com/NaturalNode/natural) node package. "natural" uses +[https://github.com/NaturalNode/natural](https://github.com/NaturalNode/natural) node package. Natural uses many of these algorithms but adds a layer of natural language/text feature extraction. + +# Apparatus Machine Learning Algorithms Documentation + +Documentation of the low-level machine learning algorithms implemented in Apparatus. + +## Table of Contents + +1. [Classifiers](#classifiers) + - [Naïve Bayes](#naïve-bayes-classifier) + - [Logistic Regression](#logistic-regression-classifier) + - [Random Forest](#random-forest-classifier) +2. [Clusterers](#clusterers) + - [K-Means](#k-means-clustering) + +--- + +## Classifiers + +Classification algorithms predict discrete categories/labels for input data. + +### Naïve Bayes Classifier + +**Type:** Probabilistic classifier +**Use Cases:** Text classification, spam detection, sentiment analysis, category prediction +**Time Complexity:** O(n) training, O(1) prediction +**Space Complexity:** O(features × classes) + +#### Overview + +Naïve Bayes is a probabilistic classifier based on Bayes' theorem with the assumption that features are conditionally independent given the class label. + +**Formula:** + +$$P(C|X) = \frac{P(X|C) \times P(C)}{P(X)}$$ + +Where: +- **P(C|X)** = Posterior probability of class C given features X +- **P(X|C)** = Likelihood of features X given class C +- **P(C)** = Prior probability of class C +- **P(X)** = Probability of observing features X + +#### How It Works + +1. **Training Phase:** + - Count feature occurrences for each class + - Calculate feature probabilities per class + - Store class priors (how often each class appears) + +2. **Prediction Phase:** + - For input features, compute probability of each class + - Multiply: P(class) × P(feature1|class) × P(feature2|class) × ... + - Return class with highest probability + +#### Example + +```javascript +const BayesClassifier = require('./lib/apparatus/classifier/bayes_classifier'); + +const classifier = new BayesClassifier(); + +// Training: [feature_vector, label] +classifier.addExample([1, 0, 1, 0], 'spam'); +classifier.addExample([0, 1, 0, 1], 'ham'); +classifier.addExample([1, 1, 1, 0], 'spam'); +classifier.train(); + +// Prediction +const result = classifier.classify([1, 0, 1, 0]); // Returns 'spam' +``` + +#### Parameters + +- **Smoothing:** Laplace smoothing factor (default: 1.0) + - Prevents zero probabilities for unseen features + - Higher values = more smoothing + +--- + +### Logistic Regression Classifier + +**Type:** Linear classifier (multiclass support) +**Use Cases:** Multi-class classification, probability estimation, linear decision boundaries +**Time Complexity:** O(n × iterations × classes) training, O(features × classes) prediction +**Space Complexity:** O(features × classes) + +#### Overview + +Logistic Regression is a linear classification algorithm that uses the sigmoid function to map input to probabilities. It supports multiclass classification by training separate binary classifiers for each class (one-vs-all). + +**Formula:** + +$$P(y=1|x) = \frac{1}{1 + e^{-(\theta^T x)}}$$ + +Where: +- **θ** = Learned weights (one set per class) +- **x** = Feature vector +- Sigmoid function maps linear combination to [0, 1] + +#### How It Works + +1. **Initialization:** + - Start with random weights (θ) + - Define learning data and labels + +2. **Training via Gradient Descent:** + - Compute predictions using current weights + - Calculate cost (loss) using cross-entropy + - Update weights in direction of steepest descent + - Repeat for max iterations + +3. **Prediction:** + - Compute linear combination: z = θ^T × x + - Apply sigmoid: P = 1 / (1 + e^-z) + - If P > 0.5 → class 1, else → class 0 + +#### Example + +```javascript +const LogisticRegression = require('./lib/apparatus/classifier/logistic_regression_classifier'); + +const classifier = new LogisticRegression(); + +// Training: array of [features] and array of [labels] +const examples = [ + [2.1, 1.0], + [2.0, 0.9], + [8.0, 8.0], + [8.1, 7.9] +]; +const labels = [0, 0, 1, 1]; + +classifier.addExample(examples[0], labels[0]); +classifier.addExample(examples[1], labels[1]); +classifier.addExample(examples[2], labels[2]); +classifier.addExample(examples[3], labels[3]); +classifier.train(); + +// Prediction (returns probability or class) +const prob = classifier.classify([2.0, 1.0]); // Close to 0 +const prob2 = classifier.classify([8.0, 8.0]); // Close to 1 +``` + +#### Parameters + +- **Learning Rate:** Controls step size in gradient descent +- **Max Iterations:** Maximum training iterations +- **Regularization:** Optional L2 regularization to prevent overfitting + +--- + +### Random Forest Classifier + +**Type:** Ensemble classifier (decision trees) - **Binary classification only** +**Use Cases:** Binary classification, feature importance, non-linear boundaries +**Time Complexity:** O(n × k × log n) training, O(k × depth) prediction +**Space Complexity:** O(k × tree_nodes) +**Constraint:** Only supports binary classification with classes **1** and **-1** + +#### Overview + +Random Forest is an ensemble method that builds multiple decision trees and combines their predictions. Each tree is trained on a random subset of data and features. + +**Important:** This implementation only supports binary classification where labels must be **1** or **-1**. + +#### How It Works + +1. **Forest Creation:** + - Build k decision trees (configurable) + - For each tree: bootstrap sample of training data + - At each node: randomly select subset of features + - Split on feature that maximizes information gain + +2. **Prediction:** + - Each tree predicts independently + - Return majority vote (classification) or average (regression) + +3. **Feature Importance:** + - Track how often features are used for splits + - Normalize by depth/impact + +#### Example + +```javascript +const RandomForest = require('./lib/apparatus/classifier/randomforest_classifier'); + +const classifier = new RandomForest(); + +// Add training examples - MUST use labels 1 or -1 +classifier.addExample([2.0, 1.0], 1); +classifier.addExample([2.1, 0.9], 1); +classifier.addExample([2.0, 1.1], 1); +classifier.addExample([8.0, 8.0], -1); +classifier.addExample([8.1, 7.9], -1); +classifier.addExample([8.0, 8.1], -1); + +// Train with configuration +classifier.train({ numTrees: 10, maxDepth: 4 }); + +// Predict - returns probability and classifications +const result = classifier.classify([2.0, 1.0]); // Class 1 +const result2 = classifier.classify([8.0, 8.0]); // Class -1 +``` + +#### Parameters + +- **numTrees:** Number of trees to build (default: 100) + - Higher = better accuracy, slower training + - Typical: 10-100 + +- **maxDepth:** Maximum tree depth (default: 4) + - Controls tree complexity + - Prevents overfitting + +- **numTries:** Random hypotheses at each node (default: 10) + - Controls randomness in feature selection + +--- + +## Clusterers + +Clustering algorithms partition data into groups without labeled targets. + +### K-Means Clustering + +**Type:** Unsupervised clustering (centroid-based) +**Use Cases:** Customer segmentation, image compression, document clustering, anomaly detection +**Time Complexity:** O(n × k × iterations × d) +**Space Complexity:** O(n × d) + +#### Overview + +K-Means is an iterative algorithm that partitions data into k clusters by minimizing within-cluster variance. The goal is to find cluster centers (centroids) that minimize the sum of squared distances to all points. + +**Objective Function:** + +$$J = \sum_{i=1}^{k} \sum_{x \in C_i} ||x - \mu_i||^2$$ + +Where: +- **k** = Number of clusters +- **C_i** = Set of points in cluster i +- **μ_i** = Centroid of cluster i +- **||x - μ_i||²** = Squared Euclidean distance + +#### Algorithm Steps + +1. **Initialization:** + - Select k initial centroids + - Options: random, k-means++, or user-provided + +2. **Assignment Step:** + - For each point: assign to nearest centroid + - Calculate distances to all k centroids + +3. **Update Step:** + - Recompute each centroid as mean of assigned points + - Move centroids toward cluster center + +4. **Convergence Check:** + - If centroids changed → repeat from Assignment + - Else → done! + +#### Variants in Apparatus + +**Legacy Implementation (kmeans-legacy.js):** +- Uses Sylvester matrix library +- Implemented circa 2011 +- Slower but foundational + +**Modernized Implementation (kmeans-modernized.js):** +- Pure JavaScript arrays (no external dependencies) +- K-means++ initialization (better starting points) +- Multiple restart support (finds better solutions) +- **7.6x faster** than legacy (see benchmark) +- Thread-safe and scalable + +#### K-Means++ Initialization + +Instead of random centroids, k-means++ selects initial centers probabilistically: + +1. Choose first centroid randomly from data +2. For i = 2 to k: + - Probability of selecting point x is proportional to D(x)² + - D(x) = distance to nearest existing centroid +3. Result: initial centers spread out, converges faster + +#### Example + +```javascript +// Modernized version (recommended) +const KMeans = require('./lib/apparatus/clusterer/kmeans'); + +const data = [ + [1, 1], + [2, 2], + [10, 10], + [11, 11] +]; + +// Legacy API (backwards compatible) +const kmeans = new KMeans(data); +const result = kmeans.cluster(2); + +// Access results via Sylvester-compatible API +console.log(result.elements.length); // 4 assignments +console.log(result.e(1)); // Cluster of point 1 (1-indexed) +console.log(result.e(2)); // Cluster of point 2 +// etc. +``` + +#### Advanced Example with Modernized API + +```javascript +const KMeansModernized = require('./lib/apparatus/clusterer/kmeans-modernized'); + +const data = [[1,1], [2,2], [10,10], [11,11]]; + +// More control with modernized API +const kmeans = new KMeansModernized(2, { + maxIterations: 100, // Max iterations + tolerance: 0.0001, // Convergence threshold + initialization: 'kmeans++', // 'random' or 'kmeans++' + seed: 42 // For reproducible results +}); + +kmeans.fit(data); + +// Get results +const assignments = kmeans.getAssignments(); // [0, 0, 1, 1] +const clusters = kmeans.getClusters(); // [[0, 1], [2, 3]] +const centroids = kmeans.getCentroids(); // [[1.5, 1.5], [10.5, 10.5]] +``` + +#### Performance Comparison + +See `benchmark/kmeans_benchmark.js` for full results: + +| Dataset | Points | Modernized | Legacy | Speedup | +|---------|--------|------------|--------|---------| +| Random | 100 | 0.17 ms | 0.81 ms | **4.8x** | +| Small | 30 | 0.01 ms | 0.10 ms | **6.9x** | +| Medium | 250 | 0.18 ms | 1.16 ms | **6.3x** | +| Large | 1,000 | 0.88 ms | 9.14 ms | **10.4x** | +| Very Large | 5,000 | 4.07 ms | 44.71 ms | **11.0x** | + +**Average speedup: 7.6x faster, 6.3% more memory** + +#### Parameters + +**KMeansModernized Constructor:** + +```javascript +new KMeansModernized(k, options) +``` + +- **k** (required): Number of clusters +- **options.maxIterations**: Max iterations (default: 100) +- **options.tolerance**: Convergence tolerance (default: 0.0001) +- **options.initialization**: + - `'random'` - Random centroid selection + - `'kmeans++'` - K-means++ initialization (recommended) +- **options.seed**: Random seed for reproducibility +- **options.restarts**: Multiple runs to find better solution +- **options.distanceFunction**: Custom distance metric (default: Euclidean) + + +## Comparison Table + +| Algorithm | Type | Classes | Training | Prediction | Complexity | Best Use Case | +|-----------|------|---------|----------|-----------|-----------|---------------| +| Naïve Bayes | Classification | Multi | O(n) | O(1) | Low | Text, fast baseline | +| Logistic Regression | Classification | Multi | O(n×iter×c) | O(d×c) | Low-Mid | Linear boundaries, multiclass | +| Random Forest | Classification | **Binary only (±1)** | O(n×k×log n) | O(k×depth) | High | Non-linear binary problems | +| K-Means | Clustering | N/A | O(n×k×iter×d) | O(k×d) | Mid | Segmentation, exploration | + +**Note:** d = dimensions, n = samples, k = clusters/trees, c = classes, iter = iterations + +--- + +## Installation & Usage + +### Install Apparatus + +```bash +npm install apparatus +``` + +### Basic Example + +```javascript +const BayesClassifier = require('apparatus/lib/apparatus/classifier/bayes_classifier'); +const KMeans = require('apparatus/lib/apparatus/clusterer/kmeans'); + +// Classification +const classifier = new BayesClassifier(); +classifier.addExample([1, 0], 'class_a'); +classifier.addExample([0, 1], 'class_b'); +classifier.train(); +console.log(classifier.classify([1, 0])); // 'class_a' + +// Clustering +const kmeans = new KMeans([[1,1], [2,2], [10,10]]); +const result = kmeans.cluster(2); +console.log(result.elements); // [1, 1, 2] +``` + +--- + +## References & Further Reading + +- **Naïve Bayes:** [Wikipedia](https://en.wikipedia.org/wiki/Naive_Bayes_classifier) +- **Logistic Regression:** [Wikipedia](https://en.wikipedia.org/wiki/Logistic_regression) +- **Random Forest:** [Breiman, 2001](https://en.wikipedia.org/wiki/Random_forest) +- **K-Means:** [MacQueen, 1967](https://en.wikipedia.org/wiki/K-means_clustering) +- **K-Means++:** [Arthur & Vassilvitskii, 2007](https://en.wikipedia.org/wiki/K-means%2B%2B) + +--- + +**Last Updated:** February 24, 2026 +**Apparatus Version:** 0.0.11 + From 1ab337d291296ab3c9f9b28990338d0776aff187 Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Thu, 26 Feb 2026 11:23:32 +0100 Subject: [PATCH 04/19] Documentation anbd IP preambles --- ALGORITHMS.md | 418 ---------------------------- lib/apparatus/clusterer/kmeans.d.ts | 22 ++ lib/apparatus/clusterer/kmeans.js | 22 ++ 3 files changed, 44 insertions(+), 418 deletions(-) delete mode 100644 ALGORITHMS.md diff --git a/ALGORITHMS.md b/ALGORITHMS.md deleted file mode 100644 index 3a25041..0000000 --- a/ALGORITHMS.md +++ /dev/null @@ -1,418 +0,0 @@ -# Apparatus Machine Learning Algorithms Documentation - -Documentation of the low-level machine learning algorithms implemented in Apparatus. - -## Table of Contents - -1. [Classifiers](#classifiers) - - [Naïve Bayes](#naïve-bayes-classifier) - - [Logistic Regression](#logistic-regression-classifier) - - [Random Forest](#random-forest-classifier) -2. [Clusterers](#clusterers) - - [K-Means](#k-means-clustering) - ---- - -## Classifiers - -Classification algorithms predict discrete categories/labels for input data. - -### Naïve Bayes Classifier - -**Type:** Probabilistic classifier -**Use Cases:** Text classification, spam detection, sentiment analysis, category prediction -**Time Complexity:** O(n) training, O(1) prediction -**Space Complexity:** O(features × classes) - -#### Overview - -Naïve Bayes is a probabilistic classifier based on Bayes' theorem with the assumption that features are conditionally independent given the class label. - -**Formula:** - -$$P(C|X) = \frac{P(X|C) \times P(C)}{P(X)}$$ - -Where: -- **P(C|X)** = Posterior probability of class C given features X -- **P(X|C)** = Likelihood of features X given class C -- **P(C)** = Prior probability of class C -- **P(X)** = Probability of observing features X - -#### How It Works - -1. **Training Phase:** - - Count feature occurrences for each class - - Calculate feature probabilities per class - - Store class priors (how often each class appears) - -2. **Prediction Phase:** - - For input features, compute probability of each class - - Multiply: P(class) × P(feature1|class) × P(feature2|class) × ... - - Return class with highest probability - -#### Example - -```javascript -const BayesClassifier = require('./lib/apparatus/classifier/bayes_classifier'); - -const classifier = new BayesClassifier(); - -// Training: [feature_vector, label] -classifier.addExample([1, 0, 1, 0], 'spam'); -classifier.addExample([0, 1, 0, 1], 'ham'); -classifier.addExample([1, 1, 1, 0], 'spam'); -classifier.train(); - -// Prediction -const result = classifier.classify([1, 0, 1, 0]); // Returns 'spam' -``` - -#### Parameters - -- **Smoothing:** Laplace smoothing factor (default: 1.0) - - Prevents zero probabilities for unseen features - - Higher values = more smoothing - ---- - -### Logistic Regression Classifier - -**Type:** Linear classifier (multiclass support) -**Use Cases:** Multi-class classification, probability estimation, linear decision boundaries -**Time Complexity:** O(n × iterations × classes) training, O(features × classes) prediction -**Space Complexity:** O(features × classes) - -#### Overview - -Logistic Regression is a linear classification algorithm that uses the sigmoid function to map input to probabilities. It supports multiclass classification by training separate binary classifiers for each class (one-vs-all). - -**Formula:** - -$$P(y=1|x) = \frac{1}{1 + e^{-(\theta^T x)}}$$ - -Where: -- **θ** = Learned weights (one set per class) -- **x** = Feature vector -- Sigmoid function maps linear combination to [0, 1] - -#### How It Works - -1. **Initialization:** - - Start with random weights (θ) - - Define learning data and labels - -2. **Training via Gradient Descent:** - - Compute predictions using current weights - - Calculate cost (loss) using cross-entropy - - Update weights in direction of steepest descent - - Repeat for max iterations - -3. **Prediction:** - - Compute linear combination: z = θ^T × x - - Apply sigmoid: P = 1 / (1 + e^-z) - - If P > 0.5 → class 1, else → class 0 - -#### Example - -```javascript -const LogisticRegression = require('./lib/apparatus/classifier/logistic_regression_classifier'); - -const classifier = new LogisticRegression(); - -// Training: array of [features] and array of [labels] -const examples = [ - [2.1, 1.0], - [2.0, 0.9], - [8.0, 8.0], - [8.1, 7.9] -]; -const labels = [0, 0, 1, 1]; - -classifier.addExample(examples[0], labels[0]); -classifier.addExample(examples[1], labels[1]); -classifier.addExample(examples[2], labels[2]); -classifier.addExample(examples[3], labels[3]); -classifier.train(); - -// Prediction (returns probability or class) -const prob = classifier.classify([2.0, 1.0]); // Close to 0 -const prob2 = classifier.classify([8.0, 8.0]); // Close to 1 -``` - -#### Parameters - -- **Learning Rate:** Controls step size in gradient descent -- **Max Iterations:** Maximum training iterations -- **Regularization:** Optional L2 regularization to prevent overfitting - ---- - -### Random Forest Classifier - -**Type:** Ensemble classifier (decision trees) - **Binary classification only** -**Use Cases:** Binary classification, feature importance, non-linear boundaries -**Time Complexity:** O(n × k × log n) training, O(k × depth) prediction -**Space Complexity:** O(k × tree_nodes) -**Constraint:** Only supports binary classification with classes **1** and **-1** - -#### Overview - -Random Forest is an ensemble method that builds multiple decision trees and combines their predictions. Each tree is trained on a random subset of data and features. - -**Important:** This implementation only supports binary classification where labels must be **1** or **-1**. - -#### How It Works - -1. **Forest Creation:** - - Build k decision trees (configurable) - - For each tree: bootstrap sample of training data - - At each node: randomly select subset of features - - Split on feature that maximizes information gain - -2. **Prediction:** - - Each tree predicts independently - - Return majority vote (classification) or average (regression) - -3. **Feature Importance:** - - Track how often features are used for splits - - Normalize by depth/impact - -#### Example - -```javascript -const RandomForest = require('./lib/apparatus/classifier/randomforest_classifier'); - -const classifier = new RandomForest(); - -// Add training examples - MUST use labels 1 or -1 -classifier.addExample([2.0, 1.0], 1); -classifier.addExample([2.1, 0.9], 1); -classifier.addExample([2.0, 1.1], 1); -classifier.addExample([8.0, 8.0], -1); -classifier.addExample([8.1, 7.9], -1); -classifier.addExample([8.0, 8.1], -1); - -// Train with configuration -classifier.train({ numTrees: 10, maxDepth: 4 }); - -// Predict - returns probability and classifications -const result = classifier.classify([2.0, 1.0]); // Class 1 -const result2 = classifier.classify([8.0, 8.0]); // Class -1 -``` - -#### Parameters - -- **numTrees:** Number of trees to build (default: 100) - - Higher = better accuracy, slower training - - Typical: 10-100 - -- **maxDepth:** Maximum tree depth (default: 4) - - Controls tree complexity - - Prevents overfitting - -- **numTries:** Random hypotheses at each node (default: 10) - - Controls randomness in feature selection - ---- - -## Clusterers - -Clustering algorithms partition data into groups without labeled targets. - -### K-Means Clustering - -**Type:** Unsupervised clustering (centroid-based) -**Use Cases:** Customer segmentation, image compression, document clustering, anomaly detection -**Time Complexity:** O(n × k × iterations × d) -**Space Complexity:** O(n × d) - -#### Overview - -K-Means is an iterative algorithm that partitions data into k clusters by minimizing within-cluster variance. The goal is to find cluster centers (centroids) that minimize the sum of squared distances to all points. - -**Objective Function:** - -$$J = \sum_{i=1}^{k} \sum_{x \in C_i} ||x - \mu_i||^2$$ - -Where: -- **k** = Number of clusters -- **C_i** = Set of points in cluster i -- **μ_i** = Centroid of cluster i -- **||x - μ_i||²** = Squared Euclidean distance - -#### Algorithm Steps - -1. **Initialization:** - - Select k initial centroids - - Options: random, k-means++, or user-provided - -2. **Assignment Step:** - - For each point: assign to nearest centroid - - Calculate distances to all k centroids - -3. **Update Step:** - - Recompute each centroid as mean of assigned points - - Move centroids toward cluster center - -4. **Convergence Check:** - - If centroids changed → repeat from Assignment - - Else → done! - -#### Variants in Apparatus - -**Legacy Implementation (kmeans-legacy.js):** -- Uses Sylvester matrix library -- Implemented circa 2011 -- Slower but foundational - -**Modernized Implementation (kmeans-modernized.js):** -- Pure JavaScript arrays (no external dependencies) -- K-means++ initialization (better starting points) -- Multiple restart support (finds better solutions) -- **7.6x faster** than legacy (see benchmark) -- Thread-safe and scalable - -#### K-Means++ Initialization - -Instead of random centroids, k-means++ selects initial centers probabilistically: - -1. Choose first centroid randomly from data -2. For i = 2 to k: - - Probability of selecting point x is proportional to D(x)² - - D(x) = distance to nearest existing centroid -3. Result: initial centers spread out, converges faster - -#### Example - -```javascript -// Modernized version (recommended) -const KMeans = require('./lib/apparatus/clusterer/kmeans'); - -const data = [ - [1, 1], - [2, 2], - [10, 10], - [11, 11] -]; - -// Legacy API (backwards compatible) -const kmeans = new KMeans(data); -const result = kmeans.cluster(2); - -// Access results via Sylvester-compatible API -console.log(result.elements.length); // 4 assignments -console.log(result.e(1)); // Cluster of point 1 (1-indexed) -console.log(result.e(2)); // Cluster of point 2 -// etc. -``` - -#### Advanced Example with Modernized API - -```javascript -const KMeansModernized = require('./lib/apparatus/clusterer/kmeans-modernized'); - -const data = [[1,1], [2,2], [10,10], [11,11]]; - -// More control with modernized API -const kmeans = new KMeansModernized(2, { - maxIterations: 100, // Max iterations - tolerance: 0.0001, // Convergence threshold - initialization: 'kmeans++', // 'random' or 'kmeans++' - seed: 42 // For reproducible results -}); - -kmeans.fit(data); - -// Get results -const assignments = kmeans.getAssignments(); // [0, 0, 1, 1] -const clusters = kmeans.getClusters(); // [[0, 1], [2, 3]] -const centroids = kmeans.getCentroids(); // [[1.5, 1.5], [10.5, 10.5]] -``` - -#### Performance Comparison - -See `benchmark/kmeans_benchmark.js` for full results: - -| Dataset | Points | Modernized | Legacy | Speedup | -|---------|--------|------------|--------|---------| -| Random | 100 | 0.17 ms | 0.81 ms | **4.8x** | -| Small | 30 | 0.01 ms | 0.10 ms | **6.9x** | -| Medium | 250 | 0.18 ms | 1.16 ms | **6.3x** | -| Large | 1,000 | 0.88 ms | 9.14 ms | **10.4x** | -| Very Large | 5,000 | 4.07 ms | 44.71 ms | **11.0x** | - -**Average speedup: 7.6x faster, 6.3% more memory** - -#### Parameters - -**KMeansModernized Constructor:** - -```javascript -new KMeansModernized(k, options) -``` - -- **k** (required): Number of clusters -- **options.maxIterations**: Max iterations (default: 100) -- **options.tolerance**: Convergence tolerance (default: 0.0001) -- **options.initialization**: - - `'random'` - Random centroid selection - - `'kmeans++'` - K-means++ initialization (recommended) -- **options.seed**: Random seed for reproducibility -- **options.restarts**: Multiple runs to find better solution -- **options.distanceFunction**: Custom distance metric (default: Euclidean) - - -## Comparison Table - -| Algorithm | Type | Classes | Training | Prediction | Complexity | Best Use Case | -|-----------|------|---------|----------|-----------|-----------|---------------| -| Naïve Bayes | Classification | Multi | O(n) | O(1) | Low | Text, fast baseline | -| Logistic Regression | Classification | Multi | O(n×iter×c) | O(d×c) | Low-Mid | Linear boundaries, multiclass | -| Random Forest | Classification | **Binary only (±1)** | O(n×k×log n) | O(k×depth) | High | Non-linear binary problems | -| K-Means | Clustering | N/A | O(n×k×iter×d) | O(k×d) | Mid | Segmentation, exploration | - -**Note:** d = dimensions, n = samples, k = clusters/trees, c = classes, iter = iterations - ---- - -## Installation & Usage - -### Install Apparatus - -```bash -npm install apparatus -``` - -### Basic Example - -```javascript -const BayesClassifier = require('apparatus/lib/apparatus/classifier/bayes_classifier'); -const KMeans = require('apparatus/lib/apparatus/clusterer/kmeans'); - -// Classification -const classifier = new BayesClassifier(); -classifier.addExample([1, 0], 'class_a'); -classifier.addExample([0, 1], 'class_b'); -classifier.train(); -console.log(classifier.classify([1, 0])); // 'class_a' - -// Clustering -const kmeans = new KMeans([[1,1], [2,2], [10,10]]); -const result = kmeans.cluster(2); -console.log(result.elements); // [1, 1, 2] -``` - ---- - -## References & Further Reading - -- **Naïve Bayes:** [Wikipedia](https://en.wikipedia.org/wiki/Naive_Bayes_classifier) -- **Logistic Regression:** [Wikipedia](https://en.wikipedia.org/wiki/Logistic_regression) -- **Random Forest:** [Breiman, 2001](https://en.wikipedia.org/wiki/Random_forest) -- **K-Means:** [MacQueen, 1967](https://en.wikipedia.org/wiki/K-means_clustering) -- **K-Means++:** [Arthur & Vassilvitskii, 2007](https://en.wikipedia.org/wiki/K-means%2B%2B) - ---- - -**Last Updated:** February 24, 2026 -**Apparatus Version:** 0.0.11 diff --git a/lib/apparatus/clusterer/kmeans.d.ts b/lib/apparatus/clusterer/kmeans.d.ts index 3d5ab37..62b0fdb 100644 --- a/lib/apparatus/clusterer/kmeans.d.ts +++ b/lib/apparatus/clusterer/kmeans.d.ts @@ -1,3 +1,25 @@ +/* +Copyright (c) 2026, Hugo W.L. ter Doest + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + /* Backwards compatibility wrapper for the original Apparatus KMeans API diff --git a/lib/apparatus/clusterer/kmeans.js b/lib/apparatus/clusterer/kmeans.js index 9f12fa0..dacf00b 100644 --- a/lib/apparatus/clusterer/kmeans.js +++ b/lib/apparatus/clusterer/kmeans.js @@ -1,3 +1,25 @@ +/* +Copyright (c) 2026, Hugo W.L. ter Doest + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + /* Backwards compatibility wrapper for the original Apparatus KMeans API From 4e4e1a0f9609a29d723f585d5aee190ff229d801 Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Thu, 26 Feb 2026 11:27:48 +0100 Subject: [PATCH 05/19] Corrected IP --- lib/apparatus/clusterer/kmeans-modernized.d.ts | 2 +- lib/apparatus/clusterer/kmeans-modernized.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/apparatus/clusterer/kmeans-modernized.d.ts b/lib/apparatus/clusterer/kmeans-modernized.d.ts index 7a5bfff..9ecdd7c 100644 --- a/lib/apparatus/clusterer/kmeans-modernized.d.ts +++ b/lib/apparatus/clusterer/kmeans-modernized.d.ts @@ -1,5 +1,5 @@ /* -Copyright (c) 2011, Chris Umbel +Copyright (c) 2026 Hugo W.L. ter Doest Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/lib/apparatus/clusterer/kmeans-modernized.js b/lib/apparatus/clusterer/kmeans-modernized.js index 6571b3d..85386b1 100644 --- a/lib/apparatus/clusterer/kmeans-modernized.js +++ b/lib/apparatus/clusterer/kmeans-modernized.js @@ -1,5 +1,5 @@ /* -Copyright (c) 2011, Chris Umbel +Copyright (c) 2026, Hugo W.L. ter Doest Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 508f621f23a77a45fa2756061e2767db4eaac23b Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Mon, 2 Mar 2026 14:01:24 +0100 Subject: [PATCH 06/19] Modernize logistic regression classifier --- benchmark/logistic_regression_benchmark.js | 190 ++++++++ .../classifier/logistic-regression-legacy.js | 192 ++++++++ .../logistic-regression-modernized.js | 419 ++++++++++++++++++ .../logistic_regression_classifier.js | 172 +------ 4 files changed, 803 insertions(+), 170 deletions(-) create mode 100644 benchmark/logistic_regression_benchmark.js create mode 100644 lib/apparatus/classifier/logistic-regression-legacy.js create mode 100644 lib/apparatus/classifier/logistic-regression-modernized.js diff --git a/benchmark/logistic_regression_benchmark.js b/benchmark/logistic_regression_benchmark.js new file mode 100644 index 0000000..410428f --- /dev/null +++ b/benchmark/logistic_regression_benchmark.js @@ -0,0 +1,190 @@ +/* +Benchmark comparing Logistic Regression Classifier implementations +Original (Sylvester-based) vs Modernized (direct array manipulation) +*/ + +'use strict'; + +const LogisticRegressionLegacy = require('../lib/apparatus/classifier/logistic-regression-legacy'); +const LogisticRegressionModernized = require('../lib/apparatus/classifier/logistic-regression-modernized'); + +/** + * Generate random training data + */ +function generateTrainingData(numSamples, numFeatures, numClasses) { + const data = []; + + for (let classIdx = 0; classIdx < numClasses; classIdx++) { + const samplesPerClass = numSamples / numClasses; + for (let i = 0; i < samplesPerClass; i++) { + const example = new Array(numFeatures); + for (let j = 0; j < numFeatures; j++) { + // Generate features biased by class + example[j] = Math.random() + (classIdx * 0.5); + } + data.push({ + features: example, + label: `class_${classIdx}` + }); + } + } + + return data; +} + +/** + * Measure memory usage in MB + */ +function getMemoryUsage() { + if (global.gc) { + global.gc(); + } + const used = process.memoryUsage(); + return { + heapUsed: Math.round(used.heapUsed / 1024 / 1024 * 100) / 100, + heapTotal: Math.round(used.heapTotal / 1024 / 1024 * 100) / 100, + external: Math.round(used.external / 1024 / 1024 * 100) / 100 + }; +} + +/** + * Benchmark training phase + */ +function benchmarkTraining(ClassifierClass, data, label) { + const classifier = new ClassifierClass(); + + // Measure memory before training + const memBefore = getMemoryUsage(); + const startTime = process.hrtime.bigint(); + + // Add examples + for (let i = 0; i < data.length; i++) { + classifier.addExample(data[i].features, data[i].label); + } + + // Train + classifier.train(); + + const endTime = process.hrtime.bigint(); + const memAfter = getMemoryUsage(); + + const timeMs = Number(endTime - startTime) / 1000000; // Convert to milliseconds + const heapUsedDiff = memAfter.heapUsed - memBefore.heapUsed; + + return { + label, + trainingTime: Math.round(timeMs * 100) / 100, + memoryUsed: Math.round(heapUsedDiff * 100) / 100, + classifier + }; +} + +/** + * Benchmark classification phase + */ +function benchmarkClassification(classifier, testData, label) { + const memBefore = getMemoryUsage(); + const startTime = process.hrtime.bigint(); + + // Classify all test samples + let correctCount = 0; + for (let i = 0; i < testData.length; i++) { + const result = classifier.getClassifications(testData[i].features); + // Check if top prediction matches actual label + if (result[0].label === testData[i].label) { + correctCount++; + } + } + + const endTime = process.hrtime.bigint(); + const memAfter = getMemoryUsage(); + + const timeMs = Number(endTime - startTime) / 1000000; + const heapUsedDiff = memAfter.heapUsed - memBefore.heapUsed; + const accuracy = Math.round((correctCount / testData.length) * 10000) / 100; + + return { + label, + classificationTime: Math.round(timeMs * 100) / 100, + memoryUsed: Math.round(heapUsedDiff * 100) / 100, + accuracy, + sampleCount: testData.length + }; +} + +/** + * Main benchmark + */ +function runBenchmark() { + console.log('='.repeat(80)); + console.log('Logistic Regression Classifier - Performance Benchmark'); + console.log('='.repeat(80)); + console.log(); + + const testConfigs = [ + { samples: 100, features: 10, classes: 2, name: 'Small (100 samples)' }, + { samples: 500, features: 20, classes: 3, name: 'Medium (500 samples)' }, + { samples: 2000, features: 50, classes: 5, name: 'Large (2000 samples)' } + ]; + + for (const config of testConfigs) { + console.log(`\n${'='.repeat(80)}`); + console.log(`Dataset: ${config.name}`); + console.log(`Features: ${config.features}, Classes: ${config.classes}`); + console.log('-'.repeat(80)); + + // Generate data split + const allData = generateTrainingData(config.samples, config.features, config.classes); + const trainSize = Math.floor(allData.length * 0.8); + const trainData = allData.slice(0, trainSize); + const testData = allData.slice(trainSize); + + console.log(`Training samples: ${trainData.length}, Test samples: ${testData.length}`); + console.log(); + + // Benchmark legacy (Sylvester) + console.log('Legacy Classifier (Sylvester):'); + const legacyTrain = benchmarkTraining(LogisticRegressionLegacy, trainData, 'Legacy'); + console.log(` Training time: ${legacyTrain.trainingTime} ms`); + console.log(` Memory used: ${legacyTrain.memoryUsed} MB`); + + const legacyClass = benchmarkClassification(legacyTrain.classifier, testData, 'Legacy'); + console.log(` Classification time: ${legacyClass.classificationTime} ms (${testData.length} samples)`); + console.log(` Memory used: ${legacyClass.memoryUsed} MB`); + console.log(` Accuracy: ${legacyClass.accuracy}%`); + console.log(); + + // Benchmark modernized + console.log('Modernized Classifier (Direct arrays):'); + const modernTrain = benchmarkTraining(LogisticRegressionModernized, trainData, 'Modernized'); + console.log(` Training time: ${modernTrain.trainingTime} ms`); + console.log(` Memory used: ${modernTrain.memoryUsed} MB`); + + const modernClass = benchmarkClassification(modernTrain.classifier, testData, 'Modernized'); + console.log(` Classification time: ${modernClass.classificationTime} ms (${testData.length} samples)`); + console.log(` Memory used: ${modernClass.memoryUsed} MB`); + console.log(` Accuracy: ${modernClass.accuracy}%`); + console.log(); + + // Calculate improvements + const speedupTrain = Math.round((legacyTrain.trainingTime / modernTrain.trainingTime) * 100) / 100; + const speedupClass = Math.round((legacyClass.classificationTime / modernClass.classificationTime) * 100) / 100; + const memSavingsTrain = Math.round((legacyTrain.memoryUsed - modernTrain.memoryUsed) * 100) / 100; + const memSavingsClass = Math.round((legacyClass.memoryUsed - modernClass.memoryUsed) * 100) / 100; + + console.log('IMPROVEMENTS (Modernized vs Legacy):'); + console.log(` Training speedup: ${speedupTrain}x faster`); + console.log(` Classification speedup: ${speedupClass}x faster`); + console.log(` Training memory saving: ${memSavingsTrain} MB (${Math.round((memSavingsTrain / Math.abs(legacyTrain.memoryUsed)) * 100)}%)`); + console.log(` Classification memory saving: ${memSavingsClass} MB (${Math.round((memSavingsClass / Math.abs(legacyClass.memoryUsed)) * 100)}%)`); + } + + console.log(); + console.log('='.repeat(80)); + console.log('Benchmark Complete'); + console.log('='.repeat(80)); +} + +// Run benchmark +console.log('\nNote: For accurate memory measurements, run with: node --expose-gc benchmark/logistic_regression_benchmark.js\n'); +runBenchmark(); diff --git a/lib/apparatus/classifier/logistic-regression-legacy.js b/lib/apparatus/classifier/logistic-regression-legacy.js new file mode 100644 index 0000000..043e898 --- /dev/null +++ b/lib/apparatus/classifier/logistic-regression-legacy.js @@ -0,0 +1,192 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +var util = require('util'), + Classifier = require('./classifier'); + +var sylvester = require('sylvester'), +Matrix = sylvester.Matrix, +Vector = sylvester.Vector; + +function sigmoid(z) { + return 1 / (1 + Math.exp(0 - z)); +} + +function hypothesis(theta, Observations) { + return Observations.x(theta).map(sigmoid); +} + +function cost(theta, Examples, classifications) { + var hypothesisResult = hypothesis(theta, Examples); + + var ones = Vector.One(Examples.rows()); + var cost_1 = Vector.Zero(Examples.rows()).subtract(classifications).elementMultiply(hypothesisResult.log()); + var cost_0 = ones.subtract(classifications).elementMultiply(ones.subtract(hypothesisResult).log()); + + return (1 / Examples.rows()) * cost_1.subtract(cost_0).sum(); +} + +function descendGradient(theta, Examples, classifications) { + var maxIt = 500 * Examples.rows(); + var last; + var current; + var learningRate = 3; + var learningRateFound = false; + + Examples = Matrix.One(Examples.rows(), 1).augment(Examples); + theta = theta.augment([0]); + + while(!learningRateFound && learningRate !== 0) { + var i = 0; + last = null; + + while(true) { + var hypothesisResult = hypothesis(theta, Examples); + theta = theta.subtract(Examples.transpose().x( + hypothesisResult.subtract(classifications)).x(1 / Examples.rows()).x(learningRate)); + current = cost(theta, Examples, classifications); + + i++; + + if(last) { + if(current < last) + learningRateFound = true; + else + break; + + if(last - current < 0.0001) + break; + } + + if(i >= maxIt) { + throw 'unable to find minimum'; + } + + last = current; + } + + learningRate /= 3; + } + + return theta.chomp(1); +} + +var LogisticRegressionClassifier = function() { + Classifier.call(this); + this.examples = {}; + this.features = []; + this.featurePositions = {}; + this.maxFeaturePosition = 0; + this.classifications = []; + this.exampleCount = 0; +}; + +util.inherits(LogisticRegressionClassifier, Classifier); + +function createClassifications() { + var classifications = []; + + for(var i = 0; i < this.exampleCount; i++) { + var classification = []; + + for(var _ in this.examples) { + classification.push(0); + } + + classifications.push(classification); + } + + return classifications; +} + +function computeThetas(Examples, Classifications) { + this.theta = []; + + // each class will have it's own theta. + var zero = function() { return 0; }; + for(var i = 1; i <= this.classifications.length; i++) { + var theta = Examples.row(1).map(zero); + this.theta.push(descendGradient(theta, Examples, Classifications.column(i))); + } +} + +function train() { + var examples = []; + var classifications = this.createClassifications(); + var d = 0, c = 0; + + for(var classification in this.examples) { + for(var i = 0; i < this.examples[classification].length; i++) { + var doc = this.examples[classification][i]; + var example = doc; + + examples.push(example); + classifications[d][c] = 1; + d++; + } + + c++; + } + + this.computeThetas($M(examples), $M(classifications)); +} + +function addExample(data, classification) { + if(!this.examples[classification]) { + this.examples[classification] = []; + this.classifications.push(classification); + } + + this.examples[classification].push(data); + this.exampleCount++; +} + +function getClassifications(observation) { + observation = $V(observation); + var classifications = []; + + for(var i = 0; i < this.theta.length; i++) { + classifications.push({label: this.classifications[i], value: sigmoid(observation.dot(this.theta[i])) }); + } + + return classifications.sort(function(x, y) { + return y.value - x.value; + }); +} + +function restore(classifier) { + classifier = Classifier.restore(classifier); + classifier.__proto__ = LogisticRegressionClassifier.prototype; + + return classifier; +} + +LogisticRegressionClassifier.prototype.addExample = addExample; +LogisticRegressionClassifier.prototype.restore = restore; +LogisticRegressionClassifier.prototype.train = train; +LogisticRegressionClassifier.prototype.createClassifications = createClassifications; +LogisticRegressionClassifier.prototype.computeThetas = computeThetas; +LogisticRegressionClassifier.prototype.getClassifications = getClassifications; + +LogisticRegressionClassifier.restore = restore; + +module.exports = LogisticRegressionClassifier; diff --git a/lib/apparatus/classifier/logistic-regression-modernized.js b/lib/apparatus/classifier/logistic-regression-modernized.js new file mode 100644 index 0000000..e9f8e6a --- /dev/null +++ b/lib/apparatus/classifier/logistic-regression-modernized.js @@ -0,0 +1,419 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +var util = require('util'), + Classifier = require('./classifier'); + +/** + * Sigmoid function + */ +function sigmoid(z) { + return 1 / (1 + Math.exp(-z)); +} + +/** + * Dot product of two vectors (arrays) + */ +function dotProduct(a, b) { + let sum = 0; + for (let i = 0; i < a.length; i++) { + sum += a[i] * b[i]; + } + return sum; +} + +/** + * Matrix-vector multiplication + * matrix: array of arrays (rows x cols) + * vector: array (cols) + * returns: array (rows) + */ +function matrixVectorMultiply(matrix, vector) { + const result = new Array(matrix.length); + for (let i = 0; i < matrix.length; i++) { + result[i] = dotProduct(matrix[i], vector); + } + return result; +} + +/** + * Matrix transpose + * matrix: array of arrays (rows x cols) + * returns: array of arrays (cols x rows) + */ +function matrixTranspose(matrix) { + if (matrix.length === 0) return []; + + const rows = matrix.length; + const cols = matrix[0].length; + const result = Array(cols); + + for (let j = 0; j < cols; j++) { + result[j] = new Array(rows); + for (let i = 0; i < rows; i++) { + result[j][i] = matrix[i][j]; + } + } + return result; +} + +/** + * Matrix multiplication + * a: array of arrays (m x n) + * b: array of arrays (n x p) + * returns: array of arrays (m x p) + */ +function matrixMultiply(a, b) { + const m = a.length; + const n = a[0].length; + const p = b[0].length; + const result = Array(m); + + for (let i = 0; i < m; i++) { + result[i] = new Array(p); + for (let j = 0; j < p; j++) { + let sum = 0; + for (let k = 0; k < n; k++) { + sum += a[i][k] * b[k][j]; + } + result[i][j] = sum; + } + } + return result; +} + +/** + * Element-wise operations + */ +function elementWiseSubtract(a, b) { + const result = new Array(a.length); + for (let i = 0; i < a.length; i++) { + result[i] = a[i] - b[i]; + } + return result; +} + +function elementWiseMultiply(a, b) { + const result = new Array(a.length); + for (let i = 0; i < a.length; i++) { + result[i] = a[i] * b[i]; + } + return result; +} + +function elementWiseLog(a) { + const result = new Array(a.length); + for (let i = 0; i < a.length; i++) { + result[i] = Math.log(a[i]); + } + return result; +} + +function elementWiseApply(a, fn) { + const result = new Array(a.length); + for (let i = 0; i < a.length; i++) { + result[i] = fn(a[i]); + } + return result; +} + +/** + * Sum of array elements + */ +function sum(arr) { + let total = 0; + for (let i = 0; i < arr.length; i++) { + total += arr[i]; + } + return total; +} + +/** + * Scalar multiply + */ +function scalarMultiply(arr, scalar) { + const result = new Array(arr.length); + for (let i = 0; i < arr.length; i++) { + result[i] = arr[i] * scalar; + } + return result; +} + +/** + * Matrix scalar multiply + */ +function matrixScalarMultiply(matrix, scalar) { + const result = new Array(matrix.length); + for (let i = 0; i < matrix.length; i++) { + result[i] = scalarMultiply(matrix[i], scalar); + } + return result; +} + +/** + * Augment matrix with ones column + */ +function augmentWithOnes(matrix) { + const result = new Array(matrix.length); + for (let i = 0; i < matrix.length; i++) { + result[i] = [1, ...matrix[i]]; + } + return result; +} + +/** + * Remove first element from each column (inverse of augment) + */ +function removeAugmentation(matrix) { + const result = new Array(matrix.length); + for (let i = 0; i < matrix.length; i++) { + result[i] = matrix[i].slice(1); + } + return result; +} + +/** + * Cost function + */ +function cost(theta, Examples, classifications) { + const hypothesisResult = matrixVectorMultiply(Examples, theta); + const sigmoidResult = elementWiseApply(hypothesisResult, sigmoid); + + const numExamples = Examples.length; + const ones = Array(numExamples).fill(1); + + // cost_1 = (-classifications) .* log(sigmoidResult) + const negClassifications = elementWiseMultiply(classifications, Array(numExamples).fill(-1)); + const cost_1 = elementWiseMultiply( + negClassifications, + elementWiseLog(sigmoidResult) + ); + + // cost_0 = (1 - classifications) .* log(1 - sigmoidResult) + const oneMinusHypothesis = elementWiseSubtract(ones, sigmoidResult); + const oneMinusClassifications = elementWiseSubtract(ones, classifications); + const cost_0 = elementWiseMultiply( + oneMinusClassifications, + elementWiseLog(oneMinusHypothesis) + ); + + const totalCost = sum(elementWiseSubtract(cost_1, cost_0)); + return totalCost / numExamples; +} + +/** + * Descending gradient function - trains the model + * Optimized for speed with pre-computed matrix transpose + */ +function descendGradient(theta, Examples, classifications) { + const maxIterPerRate = 100; + let learningRate = 1.0; + let currentTheta = theta.slice(); + let bestTheta = theta.slice(); + let bestCost = cost(currentTheta, Examples, classifications); + let learningRateFound = false; + const m = Examples.length; + + // Pre-compute transpose once - this is the key optimization! + const ExamplesTransposed = matrixTranspose(Examples); + + while (!learningRateFound && learningRate > 0.0001) { + let iterationCount = 0; + let lastCost = bestCost; + let improvementCount = 0; + + while (iterationCount < maxIterPerRate) { + // Compute hypothesis and error + const hypothesisResult = matrixVectorMultiply(Examples, currentTheta); + const sigmoidResult = elementWiseApply(hypothesisResult, sigmoid); + + // Gradient = X^T * (h(X) - y) / m + const error = elementWiseSubtract(sigmoidResult, classifications); + const gradient = matrixVectorMultiply(ExamplesTransposed, error); + const scaledGradient = scalarMultiply(gradient, learningRate / m); + + // Update theta + currentTheta = elementWiseSubtract(currentTheta, scaledGradient); + + // Evaluate cost + const currentCost = cost(currentTheta, Examples, classifications); + + if (currentCost < bestCost) { + bestCost = currentCost; + bestTheta = currentTheta.slice(); + improvementCount++; + } + + // Check for convergence with improved logic + if (lastCost - currentCost < 0.00001) { + learningRateFound = true; + break; + } + + lastCost = currentCost; + iterationCount++; + } + + // If we made progress, accept this learning rate + if (improvementCount > maxIterPerRate * 0.1) { + learningRateFound = true; + } else { + // Try smaller learning rate + learningRate *= 0.5; + currentTheta = bestTheta.slice(); // Reset to best known theta + } + } + + return bestTheta.slice(1); // Remove augmented 0 at the beginning +} + +/** + * Logistic Regression Classifier - Modernized version + */ +var LogisticRegressionModernized = function () { + Classifier.call(this); + this.examples = {}; + this.features = []; + this.featurePositions = {}; + this.maxFeaturePosition = 0; + this.classifications = []; + this.exampleCount = 0; + this.theta = []; +}; + +util.inherits(LogisticRegressionModernized, Classifier); + +/** + * Create classifications matrix + */ +function createClassifications() { + const classifications = []; + + for (let i = 0; i < this.exampleCount; i++) { + const classification = []; + + for (let _ in this.examples) { + classification.push(0); + } + + classifications.push(classification); + } + + return classifications; +} + +/** + * Compute theta parameters for each class + */ +function computeThetas(Examples, Classifications) { + this.theta = []; + + // each class will have its own theta + const zeroVector = new Array(Examples[0].length).fill(0); + + for (let i = 0; i < this.classifications.length; i++) { + // Extract column i from Classifications + const classColumn = new Array(Classifications.length); + for (let j = 0; j < Classifications.length; j++) { + classColumn[j] = Classifications[j][i]; + } + + this.theta.push(descendGradient(zeroVector, Examples, classColumn)); + } +} + +/** + * Train the classifier + */ +function train() { + const examples = []; + const classifications = this.createClassifications(); + let d = 0; + let c = 0; + + for (let classification in this.examples) { + for (let i = 0; i < this.examples[classification].length; i++) { + const doc = this.examples[classification][i]; + examples.push(doc); + classifications[d][c] = 1; + d++; + } + + c++; + } + + const augmentedExamples = augmentWithOnes(examples); + this.computeThetas(augmentedExamples, classifications); +} + +/** + * Add example to training set + */ +function addExample(data, classification) { + if (!this.examples[classification]) { + this.examples[classification] = []; + this.classifications.push(classification); + } + + this.examples[classification].push(data); + this.exampleCount++; +} + +/** + * Get classifications for an observation + */ +function getClassifications(observation) { + const classifications = []; + + for (let i = 0; i < this.theta.length; i++) { + const score = dotProduct(observation, this.theta[i]); + classifications.push({ + label: this.classifications[i], + value: sigmoid(score) + }); + } + + return classifications.sort(function (x, y) { + return y.value - x.value; + }); +} + +/** + * Restore classifier from JSON + */ +function restore(classifier) { + classifier = Classifier.restore(classifier); + classifier.__proto__ = LogisticRegressionModernized.prototype; + + return classifier; +} + +LogisticRegressionModernized.prototype.addExample = addExample; +LogisticRegressionModernized.prototype.restore = restore; +LogisticRegressionModernized.prototype.train = train; +LogisticRegressionModernized.prototype.createClassifications = createClassifications; +LogisticRegressionModernized.prototype.computeThetas = computeThetas; +LogisticRegressionModernized.prototype.getClassifications = getClassifications; + +LogisticRegressionModernized.restore = restore; + +module.exports = LogisticRegressionModernized; diff --git a/lib/apparatus/classifier/logistic_regression_classifier.js b/lib/apparatus/classifier/logistic_regression_classifier.js index 043e898..65a4f9c 100644 --- a/lib/apparatus/classifier/logistic_regression_classifier.js +++ b/lib/apparatus/classifier/logistic_regression_classifier.js @@ -20,173 +20,5 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var util = require('util'), - Classifier = require('./classifier'); - -var sylvester = require('sylvester'), -Matrix = sylvester.Matrix, -Vector = sylvester.Vector; - -function sigmoid(z) { - return 1 / (1 + Math.exp(0 - z)); -} - -function hypothesis(theta, Observations) { - return Observations.x(theta).map(sigmoid); -} - -function cost(theta, Examples, classifications) { - var hypothesisResult = hypothesis(theta, Examples); - - var ones = Vector.One(Examples.rows()); - var cost_1 = Vector.Zero(Examples.rows()).subtract(classifications).elementMultiply(hypothesisResult.log()); - var cost_0 = ones.subtract(classifications).elementMultiply(ones.subtract(hypothesisResult).log()); - - return (1 / Examples.rows()) * cost_1.subtract(cost_0).sum(); -} - -function descendGradient(theta, Examples, classifications) { - var maxIt = 500 * Examples.rows(); - var last; - var current; - var learningRate = 3; - var learningRateFound = false; - - Examples = Matrix.One(Examples.rows(), 1).augment(Examples); - theta = theta.augment([0]); - - while(!learningRateFound && learningRate !== 0) { - var i = 0; - last = null; - - while(true) { - var hypothesisResult = hypothesis(theta, Examples); - theta = theta.subtract(Examples.transpose().x( - hypothesisResult.subtract(classifications)).x(1 / Examples.rows()).x(learningRate)); - current = cost(theta, Examples, classifications); - - i++; - - if(last) { - if(current < last) - learningRateFound = true; - else - break; - - if(last - current < 0.0001) - break; - } - - if(i >= maxIt) { - throw 'unable to find minimum'; - } - - last = current; - } - - learningRate /= 3; - } - - return theta.chomp(1); -} - -var LogisticRegressionClassifier = function() { - Classifier.call(this); - this.examples = {}; - this.features = []; - this.featurePositions = {}; - this.maxFeaturePosition = 0; - this.classifications = []; - this.exampleCount = 0; -}; - -util.inherits(LogisticRegressionClassifier, Classifier); - -function createClassifications() { - var classifications = []; - - for(var i = 0; i < this.exampleCount; i++) { - var classification = []; - - for(var _ in this.examples) { - classification.push(0); - } - - classifications.push(classification); - } - - return classifications; -} - -function computeThetas(Examples, Classifications) { - this.theta = []; - - // each class will have it's own theta. - var zero = function() { return 0; }; - for(var i = 1; i <= this.classifications.length; i++) { - var theta = Examples.row(1).map(zero); - this.theta.push(descendGradient(theta, Examples, Classifications.column(i))); - } -} - -function train() { - var examples = []; - var classifications = this.createClassifications(); - var d = 0, c = 0; - - for(var classification in this.examples) { - for(var i = 0; i < this.examples[classification].length; i++) { - var doc = this.examples[classification][i]; - var example = doc; - - examples.push(example); - classifications[d][c] = 1; - d++; - } - - c++; - } - - this.computeThetas($M(examples), $M(classifications)); -} - -function addExample(data, classification) { - if(!this.examples[classification]) { - this.examples[classification] = []; - this.classifications.push(classification); - } - - this.examples[classification].push(data); - this.exampleCount++; -} - -function getClassifications(observation) { - observation = $V(observation); - var classifications = []; - - for(var i = 0; i < this.theta.length; i++) { - classifications.push({label: this.classifications[i], value: sigmoid(observation.dot(this.theta[i])) }); - } - - return classifications.sort(function(x, y) { - return y.value - x.value; - }); -} - -function restore(classifier) { - classifier = Classifier.restore(classifier); - classifier.__proto__ = LogisticRegressionClassifier.prototype; - - return classifier; -} - -LogisticRegressionClassifier.prototype.addExample = addExample; -LogisticRegressionClassifier.prototype.restore = restore; -LogisticRegressionClassifier.prototype.train = train; -LogisticRegressionClassifier.prototype.createClassifications = createClassifications; -LogisticRegressionClassifier.prototype.computeThetas = computeThetas; -LogisticRegressionClassifier.prototype.getClassifications = getClassifications; - -LogisticRegressionClassifier.restore = restore; - -module.exports = LogisticRegressionClassifier; +// Use the modernized implementation directly +module.exports = require('./logistic-regression-modernized'); From cfb9ddf6507078468ab859db4716a5d40b5dc325 Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Mon, 2 Mar 2026 19:28:18 +0100 Subject: [PATCH 07/19] Cleaning up the modernized versions --- lib/apparatus/clusterer/kmeans-modernized.js | 6 ++ lib/apparatus/clusterer/kmeans.js | 62 +------------------- lib/apparatus/clusterer/vector-like.js | 59 +++++++++++++++++++ lib/apparatus/index.js | 3 +- 4 files changed, 70 insertions(+), 60 deletions(-) create mode 100644 lib/apparatus/clusterer/vector-like.js diff --git a/lib/apparatus/clusterer/kmeans-modernized.js b/lib/apparatus/clusterer/kmeans-modernized.js index 85386b1..c871c35 100644 --- a/lib/apparatus/clusterer/kmeans-modernized.js +++ b/lib/apparatus/clusterer/kmeans-modernized.js @@ -381,4 +381,10 @@ class KMeansModernized { } } +// Export utility functions for use by other modules +KMeansModernized.euclideanDistance = euclideanDistance +KMeansModernized.euclideanDistanceSquared = euclideanDistanceSquared +KMeansModernized.createRng = createRng +KMeansModernized.calculateMean = calculateMean + module.exports = KMeansModernized diff --git a/lib/apparatus/clusterer/kmeans.js b/lib/apparatus/clusterer/kmeans.js index dacf00b..4454eec 100644 --- a/lib/apparatus/clusterer/kmeans.js +++ b/lib/apparatus/clusterer/kmeans.js @@ -39,66 +39,10 @@ For the modern API with additional features, see kmeans-modernized.js: 'use strict' const KMeansModernized = require('./kmeans-modernized') +const VectorLike = require('./vector-like') -/** - * Euclidean distance calculation - */ -function euclideanDistance (a, b) { - let sum = 0 - for (let i = 0; i < a.length; i++) { - const diff = a[i] - b[i] - sum += diff * diff - } - return Math.sqrt(sum) -} - -/** - * Create a deterministic RNG when a seed is provided - */ -function createRng (seed) { - if (typeof seed !== 'number' || !isFinite(seed)) { - return Math.random - } - - let state = seed >>> 0 - return function () { - state = (state * 1664525 + 1013904223) >>> 0 - return state / 4294967296 - } -} - -/** - * Simple wrapper to make arrays compatible with Sylvester Vector API - * The legacy API used Sylvester Vectors which have .elements and .e(i) - */ -class VectorLike { - constructor (zeroIndexedArray) { - // Store 0-indexed array internally for efficiency - this._array = zeroIndexedArray - this._converted = null - } - - /** - * Get element at 1-based index (Sylvester compatibility) - * Converts from 0-indexed to 1-indexed on the fly - * @param {number} i - 1-based index - * @returns {number} - Element value (1-indexed cluster assignment) - */ - e (i) { - return this._array[i - 1] + 1 - } - - /** - * Lazy getter for elements array (for .length and direct access) - * Only converts when accessed, caches the result - */ - get elements () { - if (!this._converted) { - this._converted = this._array.map(a => a + 1) - } - return this._converted - } -} +// Use utility functions from KMeansModernized +const { euclideanDistance, createRng } = KMeansModernized /** * KMeans - Original Apparatus API wrapper diff --git a/lib/apparatus/clusterer/vector-like.js b/lib/apparatus/clusterer/vector-like.js new file mode 100644 index 0000000..8c3ae8d --- /dev/null +++ b/lib/apparatus/clusterer/vector-like.js @@ -0,0 +1,59 @@ +/* +Copyright (c) 2026, Hugo W.L. ter Doest + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +'use strict' + +/** + * VectorLike - Sylvester Vector API compatible wrapper + * Makes arrays compatible with Sylvester Vector API without needing Sylvester + * The legacy API used Sylvester Vectors which have .elements and .e(i) + */ +class VectorLike { + /** + * Create a VectorLike object + * @param {Array} zeroIndexedArray - 0-indexed array of cluster assignments + */ + constructor (zeroIndexedArray) { + // Store 0-indexed array internally for efficiency + this._array = zeroIndexedArray + } + + /** + * Get element at 1-based index (Sylvester compatibility) + * Converts from 0-indexed to 1-indexed on the fly + * @param {number} i - 1-based index + * @returns {number} - Element value (1-indexed cluster assignment) + */ + e (i) { + return this._array[i - 1] + 1 + } + + /** + * Getter for elements array + * Converts from 0-indexed to 1-indexed for Sylvester compatibility + */ + get elements () { + return this._array.map(a => a + 1) + } +} + +module.exports = VectorLike diff --git a/lib/apparatus/index.js b/lib/apparatus/index.js index 922b686..a301bf2 100644 --- a/lib/apparatus/index.js +++ b/lib/apparatus/index.js @@ -1,4 +1,5 @@ exports.BayesClassifier = require('./classifier/bayes_classifier'); exports.LogisticRegressionClassifier = require('./classifier/logistic_regression_classifier'); -exports.KMeans = require('./clusterer/kmeans'); +exports.RandomForestClassifier = require('./classifier/randomforest_classifier'); +exports.KMeans = require('./clusterer/kmeans'); \ No newline at end of file From d462d08fa3849e19290828aae700672be6277575 Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Mon, 2 Mar 2026 19:31:56 +0100 Subject: [PATCH 08/19] Action for CI --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e02547f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [ main, master, develop ] + pull_request: + branches: [ main, master, develop ] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [22.x, 24.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test From ca21fd3497ac663e7d7efc2d6336a7c1e69fa124 Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Mon, 2 Mar 2026 19:51:36 +0100 Subject: [PATCH 09/19] Compare legacy implementations with modernized versions --- spec/kmeans_comparison_spec.js | 138 ++++++++++++ spec/logistic_regression_comparison_spec.js | 220 ++++++++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 spec/kmeans_comparison_spec.js create mode 100644 spec/logistic_regression_comparison_spec.js diff --git a/spec/kmeans_comparison_spec.js b/spec/kmeans_comparison_spec.js new file mode 100644 index 0000000..0561608 --- /dev/null +++ b/spec/kmeans_comparison_spec.js @@ -0,0 +1,138 @@ +/* +Comparison tests between KMeans (modernized) and KMeans-Legacy (Sylvester) +Verifies both implementations produce equivalent results +*/ + +'use strict' + +const KMeans = require('../lib/apparatus/clusterer/kmeans') +const KMeansLegacy = require('../lib/apparatus/clusterer/kmeans-legacy') + +describe('KMeans vs KMeans-Legacy Comparison', () => { + const testData = [ + [1, 2], + [1.5, 1.8], + [5, 8], + [8, 8], + [1, 0.6], + [9, 11], + [8, 2], + [10, 2], + [9, 3] + ] + + it('should produce same cluster assignments with k=2', () => { + const kmeans = new KMeans(testData) + const modernResult = kmeans.cluster(2) + + const kmeansLegacy = new KMeansLegacy(testData) + const legacyResult = kmeansLegacy.cluster(2) + + // Both should return cluster assignments for all points + expect(modernResult.elements.length).toBe(testData.length) + expect(legacyResult.elements.length).toBe(testData.length) + + // All assignments should be valid cluster numbers (1 or 2) + modernResult.elements.forEach(assignment => { + expect([1, 2]).toContain(assignment) + }) + + legacyResult.elements.forEach(assignment => { + expect([1, 2]).toContain(assignment) + }) + }) + + it('should produce same cluster assignments with k=3', () => { + const kmeans = new KMeans(testData) + const modernResult = kmeans.cluster(3) + + const kmeansLegacy = new KMeansLegacy(testData) + const legacyResult = kmeansLegacy.cluster(3) + + // Both should return cluster assignments for all points + expect(modernResult.elements.length).toBe(testData.length) + expect(legacyResult.elements.length).toBe(testData.length) + + // All assignments should be valid cluster numbers (1, 2, or 3) + modernResult.elements.forEach(assignment => { + expect([1, 2, 3]).toContain(assignment) + }) + + legacyResult.elements.forEach(assignment => { + expect([1, 2, 3]).toContain(assignment) + }) + }) + + it('should have consistent clustering - same data produces same results in multiple runs', () => { + // Run modernized version twice + const kmeans1 = new KMeans(testData) + const result1 = kmeans1.cluster(2) + + const kmeans2 = new KMeans(testData) + const result2 = kmeans2.cluster(2) + + // Results should be identical (or inverse/permuted, which is expected for k-means) + // At minimum, we check the structure is the same + expect(result1.elements.length).toBe(result2.elements.length) + }) + + it('should correctly assign nearby points to same cluster', () => { + // Points [1, 2] and [1.5, 1.8] are very close + // Points [5, 8] and [8, 8] are close + const kmeans = new KMeans(testData) + const result = kmeans.cluster(2) + + const assignments = result.elements + + // Point 0: [1, 2] + const cluster0 = assignments[0] + const cluster1 = assignments[1] // [1.5, 1.8] + + // These two close points should be in the same cluster + expect(cluster0).toBe(cluster1) + }) + + it('should match legacy implementation clustering logic', () => { + const smallData = [ + [0, 0], + [1, 1], + [10, 10], + [11, 11] + ] + + const kmeans = new KMeans(smallData) + const modernResult = kmeans.cluster(2) + + const kmeansLegacy = new KMeansLegacy(smallData) + const legacyResult = kmeansLegacy.cluster(2) + + // Both should assign [0,0] and [1,1] to same cluster + const modernAssignments = modernResult.elements + const legacyAssignments = legacyResult.elements + + expect(modernAssignments[0]).toBe(modernAssignments[1]) + expect(legacyAssignments[0]).toBe(legacyAssignments[1]) + + // Both should assign [10,10] and [11,11] to same cluster + expect(modernAssignments[2]).toBe(modernAssignments[3]) + expect(legacyAssignments[2]).toBe(legacyAssignments[3]) + }) + + it('VectorLike should provide same interface as Sylvester Vector', () => { + const kmeans = new KMeans(testData) + const result = kmeans.cluster(2) + + // Check Sylvester-compatible interface + expect(typeof result.e).toBe('function') + expect(typeof result.elements).toBe('object') + expect(Array.isArray(result.elements)).toBe(true) + + // Check 1-based indexing (Sylvester style) + const firstElement = result.e(1) + expect(typeof firstElement).toBe('number') + expect([1, 2]).toContain(firstElement) + + // elements should be 1-indexed + expect(result.elements[0]).toBeGreaterThanOrEqual(1) + }) +}) diff --git a/spec/logistic_regression_comparison_spec.js b/spec/logistic_regression_comparison_spec.js new file mode 100644 index 0000000..1f3ecc9 --- /dev/null +++ b/spec/logistic_regression_comparison_spec.js @@ -0,0 +1,220 @@ +/* +Comparison tests between LogisticRegressionClassifier (modernized) and LogisticRegressionClassifier-Legacy (Sylvester) +Verifies both implementations produce equivalent results +*/ + +'use strict' + +const LogisticRegressionClassifier = require('../lib/apparatus/classifier/logistic_regression_classifier') +const LogisticRegressionClassifierLegacy = require('../lib/apparatus/classifier/logistic-regression-legacy') + +describe('LogisticRegressionClassifier vs Legacy Comparison', () => { + const trainingData = [ + { text: [1, 0, 1, 0], label: 'positive' }, + { text: [1, 1, 0, 0], label: 'positive' }, + { text: [0, 1, 1, 0], label: 'positive' }, + { text: [0, 0, 1, 1], label: 'negative' }, + { text: [0, 0, 0, 1], label: 'negative' }, + { text: [1, 0, 0, 1], label: 'negative' } + ] + + it('should both train successfully on same data', () => { + const modernClassifier = new LogisticRegressionClassifier() + trainingData.forEach(item => { + modernClassifier.addExample(item.text, item.label) + }) + modernClassifier.train() + + const legacyClassifier = new LogisticRegressionClassifierLegacy() + trainingData.forEach(item => { + legacyClassifier.addExample(item.text, item.label) + }) + legacyClassifier.train() + + // Both should have learned theta values + expect(modernClassifier.theta).toBeDefined() + expect(modernClassifier.theta.length).toBeGreaterThan(0) + + expect(legacyClassifier.theta).toBeDefined() + expect(legacyClassifier.theta.length).toBeGreaterThan(0) + }) + + it('should both classify observations with consistent class labels', () => { + const modernClassifier = new LogisticRegressionClassifier() + trainingData.forEach(item => { + modernClassifier.addExample(item.text, item.label) + }) + modernClassifier.train() + + const legacyClassifier = new LogisticRegressionClassifierLegacy() + trainingData.forEach(item => { + legacyClassifier.addExample(item.text, item.label) + }) + legacyClassifier.train() + + // Test observation + const testObservation = [1, 0, 1, 0] + + const modernResult = modernClassifier.getClassifications(testObservation) + const legacyResult = legacyClassifier.getClassifications(testObservation) + + // Both should return array of classifications + expect(Array.isArray(modernResult)).toBe(true) + expect(Array.isArray(legacyResult)).toBe(true) + + // Both should return results for same classes + expect(modernResult.length).toBe(legacyResult.length) + expect(modernResult.length).toBe(2) // positive and negative + + // Both should have label and value properties + modernResult.forEach(result => { + expect(result.label).toBeDefined() + expect(result.value).toBeDefined() + expect(typeof result.value).toBe('number') + expect(result.value).toBeGreaterThanOrEqual(0) + expect(result.value).toBeLessThanOrEqual(1) + }) + + legacyResult.forEach(result => { + expect(result.label).toBeDefined() + expect(result.value).toBeDefined() + expect(typeof result.value).toBe('number') + }) + }) + + it('should return sorted results by confidence', () => { + const modernClassifier = new LogisticRegressionClassifier() + trainingData.forEach(item => { + modernClassifier.addExample(item.text, item.label) + }) + modernClassifier.train() + + const legacyClassifier = new LogisticRegressionClassifierLegacy() + trainingData.forEach(item => { + legacyClassifier.addExample(item.text, item.label) + }) + legacyClassifier.train() + + const testObservation = [1, 0, 1, 0] + + const modernResult = modernClassifier.getClassifications(testObservation) + const legacyResult = legacyClassifier.getClassifications(testObservation) + + // Both should return results sorted by value (descending) + for (let i = 0; i < modernResult.length - 1; i++) { + expect(modernResult[i].value).toBeGreaterThanOrEqual(modernResult[i + 1].value) + } + + for (let i = 0; i < legacyResult.length - 1; i++) { + expect(legacyResult[i].value).toBeGreaterThanOrEqual(legacyResult[i + 1].value) + } + }) + + it('should have consistent class labels in both implementations', () => { + const modernClassifier = new LogisticRegressionClassifier() + trainingData.forEach(item => { + modernClassifier.addExample(item.text, item.label) + }) + modernClassifier.train() + + const legacyClassifier = new LogisticRegressionClassifierLegacy() + trainingData.forEach(item => { + legacyClassifier.addExample(item.text, item.label) + }) + legacyClassifier.train() + + const testObservation = [1, 0, 1, 0] + + const modernResult = modernClassifier.getClassifications(testObservation) + const legacyResult = legacyClassifier.getClassifications(testObservation) + + // Extract labels and sort for comparison + const modernLabels = modernResult.map(r => r.label).sort() + const legacyLabels = legacyResult.map(r => r.label).sort() + + expect(modernLabels).toEqual(legacyLabels) + }) + + it('should both classify training examples with reasonable confidence', () => { + const modernClassifier = new LogisticRegressionClassifier() + trainingData.forEach(item => { + modernClassifier.addExample(item.text, item.label) + }) + modernClassifier.train() + + const legacyClassifier = new LogisticRegressionClassifierLegacy() + trainingData.forEach(item => { + legacyClassifier.addExample(item.text, item.label) + }) + legacyClassifier.train() + + // Test on a training example + const positiveExample = trainingData[0].text + + const modernResult = modernClassifier.getClassifications(positiveExample) + const legacyResult = legacyClassifier.getClassifications(positiveExample) + + // Top prediction should be the correct class + expect(modernResult[0].label).toBe('positive') + expect(legacyResult[0].label).toBe('positive') + + // Confidence should be reasonably high + expect(modernResult[0].value).toBeGreaterThan(0.3) + expect(legacyResult[0].value).toBeGreaterThan(0.3) + }) + + it('should produce results with value between 0 and 1 (sigmoid output)', () => { + const modernClassifier = new LogisticRegressionClassifier() + trainingData.forEach(item => { + modernClassifier.addExample(item.text, item.label) + }) + modernClassifier.train() + + const testObservation = [1, 1, 0, 0] + const modernResult = modernClassifier.getClassifications(testObservation) + + modernResult.forEach(result => { + expect(result.value).toBeGreaterThanOrEqual(0) + expect(result.value).toBeLessThanOrEqual(1) + }) + }) + + it('should both handle multiple classes correctly', () => { + const multiClassData = [ + { text: [1, 0, 0], label: 'class_a' }, + { text: [1, 1, 0], label: 'class_a' }, + { text: [0, 1, 0], label: 'class_b' }, + { text: [0, 1, 1], label: 'class_b' }, + { text: [0, 0, 1], label: 'class_c' }, + { text: [1, 0, 1], label: 'class_c' } + ] + + const modernClassifier = new LogisticRegressionClassifier() + multiClassData.forEach(item => { + modernClassifier.addExample(item.text, item.label) + }) + modernClassifier.train() + + const legacyClassifier = new LogisticRegressionClassifierLegacy() + multiClassData.forEach(item => { + legacyClassifier.addExample(item.text, item.label) + }) + legacyClassifier.train() + + const testObservation = [1, 0, 0] + + const modernResult = modernClassifier.getClassifications(testObservation) + const legacyResult = legacyClassifier.getClassifications(testObservation) + + // Both should return 3 class predictions + expect(modernResult.length).toBe(3) + expect(legacyResult.length).toBe(3) + + // Both should contain all three classes + const modernLabels = modernResult.map(r => r.label).sort() + const legacyLabels = legacyResult.map(r => r.label).sort() + + expect(modernLabels).toEqual(['class_a', 'class_b', 'class_c']) + expect(legacyLabels).toEqual(['class_a', 'class_b', 'class_c']) + }) +}) From 2f0924460d4263c050aa88f84a9ffc8abc41a691 Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Mon, 2 Mar 2026 20:05:23 +0100 Subject: [PATCH 10/19] Simpler wrapper for kmeans --- .../logistic-regression-modernized.js | 189 +++++++++--------- lib/apparatus/clusterer/kmeans.js | 22 +- 2 files changed, 102 insertions(+), 109 deletions(-) diff --git a/lib/apparatus/classifier/logistic-regression-modernized.js b/lib/apparatus/classifier/logistic-regression-modernized.js index e9f8e6a..46d8a29 100644 --- a/lib/apparatus/classifier/logistic-regression-modernized.js +++ b/lib/apparatus/classifier/logistic-regression-modernized.js @@ -289,131 +289,124 @@ function descendGradient(theta, Examples, classifications) { /** * Logistic Regression Classifier - Modernized version */ -var LogisticRegressionModernized = function () { - Classifier.call(this); - this.examples = {}; - this.features = []; - this.featurePositions = {}; - this.maxFeaturePosition = 0; - this.classifications = []; - this.exampleCount = 0; - this.theta = []; -}; - -util.inherits(LogisticRegressionModernized, Classifier); - -/** - * Create classifications matrix - */ -function createClassifications() { - const classifications = []; +class LogisticRegressionModernized extends Classifier { + constructor () { + super() + this.examples = {} + this.features = [] + this.featurePositions = {} + this.maxFeaturePosition = 0 + this.classifications = [] + this.exampleCount = 0 + this.theta = [] + } + + /** + * Create classifications matrix + */ + createClassifications () { + const classifications = [] for (let i = 0; i < this.exampleCount; i++) { - const classification = []; + const classification = [] - for (let _ in this.examples) { - classification.push(0); - } + for (let _ in this.examples) { + classification.push(0) + } - classifications.push(classification); + classifications.push(classification) } - return classifications; -} + return classifications + } -/** - * Compute theta parameters for each class - */ -function computeThetas(Examples, Classifications) { - this.theta = []; + /** + * Compute theta parameters for each class + */ + computeThetas (Examples, Classifications) { + this.theta = [] // each class will have its own theta - const zeroVector = new Array(Examples[0].length).fill(0); + const zeroVector = new Array(Examples[0].length).fill(0) for (let i = 0; i < this.classifications.length; i++) { - // Extract column i from Classifications - const classColumn = new Array(Classifications.length); - for (let j = 0; j < Classifications.length; j++) { - classColumn[j] = Classifications[j][i]; - } - - this.theta.push(descendGradient(zeroVector, Examples, classColumn)); + // Extract column i from Classifications + const classColumn = new Array(Classifications.length) + for (let j = 0; j < Classifications.length; j++) { + classColumn[j] = Classifications[j][i] + } + + this.theta.push(descendGradient(zeroVector, Examples, classColumn)) } -} + } -/** - * Train the classifier - */ -function train() { - const examples = []; - const classifications = this.createClassifications(); - let d = 0; - let c = 0; + /** + * Train the classifier + */ + train () { + const examples = [] + const classifications = this.createClassifications() + let d = 0 + let c = 0 for (let classification in this.examples) { - for (let i = 0; i < this.examples[classification].length; i++) { - const doc = this.examples[classification][i]; - examples.push(doc); - classifications[d][c] = 1; - d++; - } - - c++; + for (let i = 0; i < this.examples[classification].length; i++) { + const doc = this.examples[classification][i] + examples.push(doc) + classifications[d][c] = 1 + d++ + } + + c++ } - const augmentedExamples = augmentWithOnes(examples); - this.computeThetas(augmentedExamples, classifications); -} + const augmentedExamples = augmentWithOnes(examples) + this.computeThetas(augmentedExamples, classifications) + } -/** - * Add example to training set - */ -function addExample(data, classification) { + /** + * Add example to training set + */ + addExample (data, classification) { if (!this.examples[classification]) { - this.examples[classification] = []; - this.classifications.push(classification); + this.examples[classification] = [] + this.classifications.push(classification) } - this.examples[classification].push(data); - this.exampleCount++; -} + this.examples[classification].push(data) + this.exampleCount++ + } -/** - * Get classifications for an observation - */ -function getClassifications(observation) { - const classifications = []; + /** + * Get classifications for an observation + */ + getClassifications (observation) { + const classifications = [] for (let i = 0; i < this.theta.length; i++) { - const score = dotProduct(observation, this.theta[i]); - classifications.push({ - label: this.classifications[i], - value: sigmoid(score) - }); + const score = dotProduct(observation, this.theta[i]) + classifications.push({ + label: this.classifications[i], + value: sigmoid(score) + }) } return classifications.sort(function (x, y) { - return y.value - x.value; - }); + return y.value - x.value + }) + } + + /** + * Restore classifier from JSON + */ + static restore (classifier) { + classifier = Classifier.restore(classifier) + classifier.__proto__ = LogisticRegressionModernized.prototype + + return classifier + } } -/** - * Restore classifier from JSON - */ -function restore(classifier) { - classifier = Classifier.restore(classifier); - classifier.__proto__ = LogisticRegressionModernized.prototype; - - return classifier; -} - -LogisticRegressionModernized.prototype.addExample = addExample; -LogisticRegressionModernized.prototype.restore = restore; -LogisticRegressionModernized.prototype.train = train; -LogisticRegressionModernized.prototype.createClassifications = createClassifications; -LogisticRegressionModernized.prototype.computeThetas = computeThetas; -LogisticRegressionModernized.prototype.getClassifications = getClassifications; - -LogisticRegressionModernized.restore = restore; +LogisticRegressionModernized.restore = LogisticRegressionModernized.restore -module.exports = LogisticRegressionModernized; +module.exports = LogisticRegressionModernized diff --git a/lib/apparatus/clusterer/kmeans.js b/lib/apparatus/clusterer/kmeans.js index 4454eec..7275f0e 100644 --- a/lib/apparatus/clusterer/kmeans.js +++ b/lib/apparatus/clusterer/kmeans.js @@ -24,14 +24,13 @@ THE SOFTWARE. Backwards compatibility wrapper for the original Apparatus KMeans API This module provides the original Apparatus API for K-Means clustering. -The implementation uses the modernized KMeans internally. +It extends KMeansModernized with a legacy-compatible interface. Old API usage: const kmeans = new KMeans([[1,2], [3,4], [5,6]]); const assignments = kmeans.cluster(3); For the modern API with additional features, see kmeans-modernized.js: - const { KMeansModernized } = require('natural'); const kmeans = new KMeansModernized(3, { restarts: 10 }); kmeans.fit(data); */ @@ -47,18 +46,21 @@ const { euclideanDistance, createRng } = KMeansModernized /** * KMeans - Original Apparatus API wrapper * - * Supports the old Apparatus API: + * Extends KMeansModernized to provide backward-compatible API: * new KMeans(observations) * kmeans.cluster(k) * kmeans.createCentroids(k) * kmeans.distanceFrom(centroids) */ -class KMeans { +class KMeans extends KMeansModernized { /** * Initialize with observations (old Apparatus API) * @param {Array>} observations - Data points */ constructor (observations) { + // Call parent with placeholder k (will be set in cluster()) + super(1) + if (!Array.isArray(observations)) { throw new Error('KMeans expects an array of observations') } @@ -88,13 +90,13 @@ class KMeans { throw new Error(`k (${k}) cannot be greater than number of observations (${this.Observations.length})`) } - // Use modernized KMeans internally - const kmeans = new KMeansModernized(k) - kmeans.fit(this.Observations) + // Set k and fit using parent's fit method + this.k = k + this.fit(this.Observations) // Return assignments wrapped in Sylvester-like Vector for backwards compatibility // VectorLike handles 0->1 index conversion lazily - return new VectorLike(kmeans.getAssignments()) + return new VectorLike(this.getAssignments()) } /** @@ -108,9 +110,7 @@ class KMeans { } const rng = createRng() - const tempKmeans = new KMeansModernized(k, { initialization: 'random' }) - - return tempKmeans.initializeRandomCentroids(this.Observations, rng) + return this.initializeRandomCentroids(this.Observations, rng) } /** From d1a3d4474e212723e94e7d273b015a7e3ab90bc9 Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Mon, 2 Mar 2026 22:57:30 +0100 Subject: [PATCH 11/19] Remove some comments --- lib/apparatus/clusterer/kmeans.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/apparatus/clusterer/kmeans.js b/lib/apparatus/clusterer/kmeans.js index 7275f0e..e597011 100644 --- a/lib/apparatus/clusterer/kmeans.js +++ b/lib/apparatus/clusterer/kmeans.js @@ -29,10 +29,6 @@ It extends KMeansModernized with a legacy-compatible interface. Old API usage: const kmeans = new KMeans([[1,2], [3,4], [5,6]]); const assignments = kmeans.cluster(3); - -For the modern API with additional features, see kmeans-modernized.js: - const kmeans = new KMeansModernized(3, { restarts: 10 }); - kmeans.fit(data); */ 'use strict' From 8b443dfe8ebeb55c702b242ffbd2b31c9202b971 Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Tue, 3 Mar 2026 07:47:28 +0100 Subject: [PATCH 12/19] Add Typescript definition files --- .../classifier/bayes_classifier.d.ts | 72 ++++++++++++ lib/apparatus/classifier/classifier.d.ts | 72 ++++++++++++ .../logistic-regression-legacy.d.ts | 64 +++++++++++ .../logistic-regression-modernized.d.ts | 63 +++++++++++ .../logistic_regression_classifier.d.ts | 31 +++++ .../classifier/randomforest_classifier.d.ts | 107 ++++++++++++++++++ lib/apparatus/clusterer/kmeans.d.ts | 6 +- 7 files changed, 413 insertions(+), 2 deletions(-) create mode 100644 lib/apparatus/classifier/bayes_classifier.d.ts create mode 100644 lib/apparatus/classifier/classifier.d.ts create mode 100644 lib/apparatus/classifier/logistic-regression-legacy.d.ts create mode 100644 lib/apparatus/classifier/logistic-regression-modernized.d.ts create mode 100644 lib/apparatus/classifier/logistic_regression_classifier.d.ts create mode 100644 lib/apparatus/classifier/randomforest_classifier.d.ts diff --git a/lib/apparatus/classifier/bayes_classifier.d.ts b/lib/apparatus/classifier/bayes_classifier.d.ts new file mode 100644 index 0000000..b493bac --- /dev/null +++ b/lib/apparatus/classifier/bayes_classifier.d.ts @@ -0,0 +1,72 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +import { Classifier, Classification } from './classifier' + +/** + * Bayes Classifier + * + * Naive Bayes classifier implementation + */ +export class BayesClassifier extends Classifier { + /** + * Initialize with optional smoothing parameter + * @param smoothing - Smoothing constant (default: 1.0) + */ + constructor (smoothing?: number) + + /** + * Add an example to the training set + * @param observation - Feature vector (array or sparse object) + * @param label - Class label + */ + addExample (observation: number[] | { [key: string]: any }, label: string): void + + /** + * Train the classifier + */ + train (): void + + /** + * Get probability of a class given an observation + * @param observation - Feature vector (array or sparse object) + * @param label - Class label + * @returns Log probability + */ + probabilityOfClass (observation: number[] | { [key: string]: any }, label: string): number + + /** + * Get classifications for an observation + * @param observation - Feature vector (array or sparse object) + * @returns Array of {label, value} pairs sorted by probability + */ + getClassifications (observation: number[] | { [key: string]: any }): Classification[] + + /** + * Restore classifier from JSON + * @param classifier - JSON representation + * @returns Restored classifier + */ + static restore (classifier: any): BayesClassifier +} + +export default BayesClassifier diff --git a/lib/apparatus/classifier/classifier.d.ts b/lib/apparatus/classifier/classifier.d.ts new file mode 100644 index 0000000..8313fad --- /dev/null +++ b/lib/apparatus/classifier/classifier.d.ts @@ -0,0 +1,72 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/** + * Classification result + */ +export interface Classification { + label: string | number + value: number +} + +/** + * Base Classifier class + * + * This is the base class for all classifiers in Apparatus. + * Subclasses must implement addExample() and train() methods. + */ +export class Classifier { + /** + * Add an example to the training set + * @param observation - The feature vector (array or object) + * @param classification - The label/class for this example + */ + addExample (observation: any, classification: string | number): void + + /** + * Train the classifier on the added examples + */ + train (): void + + /** + * Classify an observation + * @param observation - The feature vector to classify + * @returns The predicted class label + */ + classify (observation: any): string | number + + /** + * Get classifications for an observation with confidence scores + * @param observation - The feature vector to classify + * @returns Array of {label, value} pairs sorted by confidence + */ + getClassifications (observation: any): Classification[] + + /** + * Restore a classifier from JSON + * @param classifier - JSON representation of classifier or JSON string + * @returns Restored classifier instance + */ + static restore (classifier: any): Classifier +} + +export default Classifier diff --git a/lib/apparatus/classifier/logistic-regression-legacy.d.ts b/lib/apparatus/classifier/logistic-regression-legacy.d.ts new file mode 100644 index 0000000..52e195d --- /dev/null +++ b/lib/apparatus/classifier/logistic-regression-legacy.d.ts @@ -0,0 +1,64 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +import { Classifier, Classification } from './classifier' + +/** + * Logistic Regression Classifier - Legacy Version + * + * Legacy implementation of logistic regression classifier. + * For new code, use LogisticRegressionModernized instead. + */ +export class LogisticRegressionLegacy extends Classifier { + /** + * Initialize the classifier + */ + constructor () + + /** + * Add an example to the training set + * @param data - Feature vector + * @param classification - Class label + */ + addExample (data: number[], classification: string): void + + /** + * Train the classifier on added examples + */ + train (): void + + /** + * Get classifications for an observation with confidence scores + * @param observation - Feature vector to classify + * @returns Array of {label, value} pairs sorted by confidence + */ + getClassifications (observation: number[]): Classification[] + + /** + * Restore classifier from JSON + * @param classifier - JSON representation + * @returns Restored classifier + */ + static restore (classifier: any): LogisticRegressionLegacy +} + +export default LogisticRegressionLegacy diff --git a/lib/apparatus/classifier/logistic-regression-modernized.d.ts b/lib/apparatus/classifier/logistic-regression-modernized.d.ts new file mode 100644 index 0000000..705e0e8 --- /dev/null +++ b/lib/apparatus/classifier/logistic-regression-modernized.d.ts @@ -0,0 +1,63 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +import { Classifier, Classification } from './classifier' + +/** + * Logistic Regression Classifier - Modernized Version + * + * Supports multi-class logistic regression using gradient descent optimization + */ +export class LogisticRegressionModernized extends Classifier { + /** + * Initialize the classifier + */ + constructor () + + /** + * Add an example to the training set + * @param data - Feature vector + * @param classification - Class label + */ + addExample (data: number[], classification: string): void + + /** + * Train the classifier on added examples + */ + train (): void + + /** + * Get classifications for an observation with confidence scores + * @param observation - Feature vector to classify + * @returns Array of {label, value} pairs sorted by confidence (sigmoid scores) + */ + getClassifications (observation: number[]): Classification[] + + /** + * Restore classifier from JSON + * @param classifier - JSON representation + * @returns Restored classifier + */ + static restore (classifier: any): LogisticRegressionModernized +} + +export default LogisticRegressionModernized diff --git a/lib/apparatus/classifier/logistic_regression_classifier.d.ts b/lib/apparatus/classifier/logistic_regression_classifier.d.ts new file mode 100644 index 0000000..379e669 --- /dev/null +++ b/lib/apparatus/classifier/logistic_regression_classifier.d.ts @@ -0,0 +1,31 @@ +/* +Copyright (c) 2011, Chris Umbel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/** + * Logistic Regression Classifier + * + * This module exports the modernized Logistic Regression Classifier. + * For the legacy implementation, use logistic-regression-legacy. + */ +export { LogisticRegressionModernized as default } from './logistic-regression-modernized' +export { LogisticRegressionModernized } from './logistic-regression-modernized' +export type { Classification } from './classifier' diff --git a/lib/apparatus/classifier/randomforest_classifier.d.ts b/lib/apparatus/classifier/randomforest_classifier.d.ts new file mode 100644 index 0000000..bbc2010 --- /dev/null +++ b/lib/apparatus/classifier/randomforest_classifier.d.ts @@ -0,0 +1,107 @@ +/* +Copyright (c) 2012 Andrej Karpathy + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import { Classifier } from './classifier' + +/** + * Random Forest Classifier Options + */ +export interface RandomForestOptions { + /** + * Number of trees to train (default: 100) + */ + numTrees?: number + + /** + * Maximum depth of each tree in the forest (default: 4) + */ + maxDepth?: number + + /** + * Number of random hypotheses generated at each node during training (default: 10) + */ + numTries?: number + + /** + * Weak learner training function + */ + trainFun?: (data: number[][], labels: number[], ix: number[], options: RandomForestOptions) => any + + /** + * Weak learner test function + */ + testFun?: (inst: number[], model: any) => number + + /** + * Type of weak learner (0: decisionStump, 1: decision2DStump) + */ + type?: number +} + +/** + * Random Forest Classifier + * + * Ensemble classifier using multiple decision trees + */ +export class RandomForestClassifier extends Classifier { + /** + * Initialize with optional options + * @param options - Configuration options for the forest + */ + constructor (options?: RandomForestOptions) + + /** + * Add an example to the training set + * @param data - Feature vector + * @param label - Classification label (typically 1 or -1 for binary classification) + */ + addExample (data: number[], label: number): void + + /** + * Train the classifier on added examples + */ + train (): void + + /** + * Classify an observation (returns the predicted label) + * @param inst - Feature vector to classify + * @returns Predicted label (1 or -1) + */ + classify (inst: number[]): number + + /** + * Get probability prediction for a single instance + * @param inst - Feature vector to predict + * @returns Probability/prediction value in range [0, 1] + */ + predictOne (inst: number[]): number + + /** + * Get probability predictions for multiple instances + * @param data - Array of feature vectors + * @returns Array of probability values + */ + predict (data: number[][]): number[] +} + +export default RandomForestClassifier diff --git a/lib/apparatus/clusterer/kmeans.d.ts b/lib/apparatus/clusterer/kmeans.d.ts index 62b0fdb..c42e291 100644 --- a/lib/apparatus/clusterer/kmeans.d.ts +++ b/lib/apparatus/clusterer/kmeans.d.ts @@ -29,6 +29,8 @@ The implementation uses the modernized KMeans internally. For the modern API with additional features, see kmeans-modernized.d.ts */ +import { VectorLike } from './vector-like' + /** * KMeans - Original Apparatus API * @@ -50,9 +52,9 @@ export class KMeans { /** * Cluster the observations into k clusters (old Apparatus API) * @param k - Number of clusters - * @returns Cluster assignments for each observation + * @returns Cluster assignments for each observation (Sylvester-compatible vector) */ - cluster (k: number): number[] + cluster (k: number): VectorLike /** * Create initial centroids (old Apparatus API) From 17e112d1308046dc3fcf625888209656c358b2fd Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Tue, 3 Mar 2026 13:09:06 +0100 Subject: [PATCH 13/19] Add benchmarkt script --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 344a1d4..9954d33 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "jasmine": "^6.0.0" }, "scripts": { - "test": "jasmine" + "test": "jasmine", + "benchmark": "node benchmark/kmeans_benchmark.js && node benchmark/logistic_regression_benchmark.js" }, "author": "Chris Umbel ", "keywords": [ From b86fe2f841a5c23759f1196d9f4811ca87ec242c Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Tue, 3 Mar 2026 18:05:58 +0100 Subject: [PATCH 14/19] standard js --- benchmark/kmeans_benchmark.js | 270 +- benchmark/logistic_regression_benchmark.js | 310 +- index.js | 3 +- lib/apparatus/classifier/bayes_classifier.js | 174 +- lib/apparatus/classifier/classifier.js | 38 +- .../classifier/logistic-regression-legacy.js | 234 +- .../logistic-regression-modernized.js | 370 +- .../logistic_regression_classifier.js | 2 +- .../classifier/randomforest_classifier.js | 691 +-- lib/apparatus/clusterer/kmeans-legacy.js | 131 +- lib/apparatus/clusterer/kmeans-modernized.js | 2 +- lib/apparatus/clusterer/kmeans.js | 30 +- lib/apparatus/clusterer/vector-like.js | 4 +- lib/apparatus/index.js | 9 +- package-lock.json | 4266 ++++++++++++++++- package.json | 7 +- spec/bayes_classifier_spec.js | 197 +- spec/kmeans_comparison_spec.js | 2 +- spec/kmeans_spec.js | 134 +- spec/logistic_regression_classifier_spec.js | 191 +- spec/logistic_regression_comparison_spec.js | 2 +- spec/randomforest_classifier_spec.js | 64 +- 22 files changed, 5549 insertions(+), 1582 deletions(-) diff --git a/benchmark/kmeans_benchmark.js b/benchmark/kmeans_benchmark.js index 641b77c..871f68c 100644 --- a/benchmark/kmeans_benchmark.js +++ b/benchmark/kmeans_benchmark.js @@ -3,223 +3,223 @@ K-Means Performance Benchmark Comparing legacy (Sylvester-based) vs modernized implementations */ -const KMeansLegacy = require('../lib/apparatus/clusterer/kmeans-legacy'); -const KMeansModernized = require('../lib/apparatus/clusterer/kmeans-modernized'); +const KMeansLegacy = require('../lib/apparatus/clusterer/kmeans-legacy') +const KMeansModernized = require('../lib/apparatus/clusterer/kmeans-modernized') // Generate random dataset -function generateDataset(numPoints, dimensions) { - const data = []; +function generateDataset (numPoints, dimensions) { + const data = [] for (let i = 0; i < numPoints; i++) { - const point = []; + const point = [] for (let j = 0; j < dimensions; j++) { - point.push(Math.random() * 100); + point.push(Math.random() * 100) } - data.push(point); + data.push(point) } - return data; + return data } // Generate clustered dataset (more realistic) -function generateClusteredDataset(numClusters, pointsPerCluster, dimensions) { - const data = []; - +function generateClusteredDataset (numClusters, pointsPerCluster, dimensions) { + const data = [] + for (let c = 0; c < numClusters; c++) { // Random cluster center - const center = []; + const center = [] for (let d = 0; d < dimensions; d++) { - center.push(Math.random() * 100); + center.push(Math.random() * 100) } - + // Generate points around center for (let p = 0; p < pointsPerCluster; p++) { - const point = []; + const point = [] for (let d = 0; d < dimensions; d++) { - point.push(center[d] + (Math.random() - 0.5) * 10); + point.push(center[d] + (Math.random() - 0.5) * 10) } - data.push(point); + data.push(point) } } - - return data; + + return data } // Benchmark function -function benchmark(name, fn, iterations = 1) { - const start = process.hrtime.bigint(); - const startMem = process.memoryUsage().heapUsed; - - let result; +function benchmark (name, fn, iterations = 1) { + const start = process.hrtime.bigint() + const startMem = process.memoryUsage().heapUsed + + let result for (let i = 0; i < iterations; i++) { - result = fn(); + result = fn() } - - const end = process.hrtime.bigint(); - const endMem = process.memoryUsage().heapUsed; - - const durationMs = Number(end - start) / 1000000 / iterations; - const memoryDelta = (endMem - startMem) / 1024 / 1024; - + + const end = process.hrtime.bigint() + const endMem = process.memoryUsage().heapUsed + + const durationMs = Number(end - start) / 1000000 / iterations + const memoryDelta = (endMem - startMem) / 1024 / 1024 + return { name, duration: durationMs, memory: memoryDelta, result - }; + } } // Run benchmark suite -function runBenchmark(datasetName, data, k, iterations = 5) { - console.log(`\n${'='.repeat(60)}`); - console.log(`Benchmark: ${datasetName}`); - console.log(`Dataset: ${data.length} points, ${data[0].length} dimensions, k=${k}`); - console.log(`Iterations: ${iterations}`); - console.log('='.repeat(60)); - +function runBenchmark (datasetName, data, k, iterations = 5) { + console.log(`\n${'='.repeat(60)}`) + console.log(`Benchmark: ${datasetName}`) + console.log(`Dataset: ${data.length} points, ${data[0].length} dimensions, k=${k}`) + console.log(`Iterations: ${iterations}`) + console.log('='.repeat(60)) + // Warm up try { - new KMeansLegacy(data).cluster(k); + new KMeansLegacy(data).cluster(k) } catch (e) { - console.log('Legacy warmup failed (expected if Sylvester not installed)'); + console.log('Legacy warmup failed (expected if Sylvester not installed)') } - - const kmeansModern = new KMeansModernized(k); - kmeansModern.fit(data); - + + const kmeansModern = new KMeansModernized(k) + kmeansModern.fit(data) + // Benchmark Legacy - let legacyResult; + let legacyResult try { legacyResult = benchmark('KMeans Legacy', () => { - const kmeans = new KMeansLegacy(data); - return kmeans.cluster(k); - }, iterations); + const kmeans = new KMeansLegacy(data) + return kmeans.cluster(k) + }, iterations) } catch (error) { - legacyResult = { - name: 'KMeans Legacy', - duration: null, + legacyResult = { + name: 'KMeans Legacy', + duration: null, memory: null, - error: error.message - }; + error: error.message + } } - + // Benchmark Modernized const modernizedResult = benchmark('KMeans Modernized', () => { - const kmeans = new KMeansModernized(k, { maxIterations: 100 }); - kmeans.fit(data); - return kmeans.getAssignments(); - }, iterations); - + const kmeans = new KMeansModernized(k, { maxIterations: 100 }) + kmeans.fit(data) + return kmeans.getAssignments() + }, iterations) + // Results - console.log('\nResults:'); - console.log('-'.repeat(60)); - + console.log('\nResults:') + console.log('-'.repeat(60)) + if (legacyResult.error) { - console.log(`Legacy: ERROR - ${legacyResult.error}`); + console.log(`Legacy: ERROR - ${legacyResult.error}`) } else { - console.log(`Legacy: ${legacyResult.duration.toFixed(2)} ms, Memory: ${legacyResult.memory.toFixed(2)} MB`); + console.log(`Legacy: ${legacyResult.duration.toFixed(2)} ms, Memory: ${legacyResult.memory.toFixed(2)} MB`) } - - console.log(`Modernized: ${modernizedResult.duration.toFixed(2)} ms, Memory: ${modernizedResult.memory.toFixed(2)} MB`); - + + console.log(`Modernized: ${modernizedResult.duration.toFixed(2)} ms, Memory: ${modernizedResult.memory.toFixed(2)} MB`) + if (!legacyResult.error) { - const speedup = (legacyResult.duration / modernizedResult.duration).toFixed(2); - const faster = speedup > 1 ? 'Modernized' : 'Legacy'; - const ratio = speedup > 1 ? speedup : (1 / speedup).toFixed(2); - - const memDiff = modernizedResult.memory - legacyResult.memory; - const memComparison = memDiff < 0 + const speedup = (legacyResult.duration / modernizedResult.duration).toFixed(2) + const faster = speedup > 1 ? 'Modernized' : 'Legacy' + const ratio = speedup > 1 ? speedup : (1 / speedup).toFixed(2) + + const memDiff = modernizedResult.memory - legacyResult.memory + const memComparison = memDiff < 0 ? `${Math.abs(memDiff).toFixed(2)} MB less memory` - : `${memDiff.toFixed(2)} MB more memory`; - - console.log(`\nComparison: ${faster} is ${ratio}x faster, ${memComparison}`); + : `${memDiff.toFixed(2)} MB more memory` + + console.log(`\nComparison: ${faster} is ${ratio}x faster, ${memComparison}`) } - - return { legacy: legacyResult, modernized: modernizedResult }; + + return { legacy: legacyResult, modernized: modernizedResult } } // Main benchmark suite -console.log('\n' + '='.repeat(60)); -console.log('K-MEANS PERFORMANCE BENCHMARK'); -console.log('='.repeat(60)); +console.log('\n' + '='.repeat(60)) +console.log('K-MEANS PERFORMANCE BENCHMARK') +console.log('='.repeat(60)) -const results = []; +const results = [] // Test 1: Random dataset (worst case - no natural clusters) -const random = generateDataset(100, 2); -results.push(runBenchmark('Random Dataset (100 points, 2D)', random, 5, 5)); +const random = generateDataset(100, 2) +results.push(runBenchmark('Random Dataset (100 points, 2D)', random, 5, 5)) // Test 2: Small dataset -const small = generateClusteredDataset(3, 10, 2); -results.push(runBenchmark('Small Clustered Dataset (30 points, 2D)', small, 3, 10)); +const small = generateClusteredDataset(3, 10, 2) +results.push(runBenchmark('Small Clustered Dataset (30 points, 2D)', small, 3, 10)) // Test 3: Medium dataset -const medium = generateClusteredDataset(5, 50, 2); -results.push(runBenchmark('Medium Clustered Dataset (250 points, 2D)', medium, 5, 5)); +const medium = generateClusteredDataset(5, 50, 2) +results.push(runBenchmark('Medium Clustered Dataset (250 points, 2D)', medium, 5, 5)) // Test 4: Large dataset -const large = generateClusteredDataset(10, 100, 2); -results.push(runBenchmark('Large Clustered Dataset (1000 points, 2D)', large, 10, 3)); +const large = generateClusteredDataset(10, 100, 2) +results.push(runBenchmark('Large Clustered Dataset (1000 points, 2D)', large, 10, 3)) // Test 5: High dimensional -const highDim = generateClusteredDataset(5, 50, 10); -results.push(runBenchmark('High Dimensional Clustered (250 points, 10D)', highDim, 5, 5)); +const highDim = generateClusteredDataset(5, 50, 10) +results.push(runBenchmark('High Dimensional Clustered (250 points, 10D)', highDim, 5, 5)) // Test 6: Very large dataset -const veryLarge = generateClusteredDataset(10, 500, 2); -results.push(runBenchmark('Very Large Clustered Dataset (5000 points, 2D)', veryLarge, 10, 1)); +const veryLarge = generateClusteredDataset(10, 500, 2) +results.push(runBenchmark('Very Large Clustered Dataset (5000 points, 2D)', veryLarge, 10, 1)) // Summary -console.log('\n' + '='.repeat(60)); -console.log('SUMMARY'); -console.log('='.repeat(60)); +console.log('\n' + '='.repeat(60)) +console.log('SUMMARY') +console.log('='.repeat(60)) -let modernizedWins = 0; -let totalSpeedup = 0; -let totalMemoryLegacy = 0; -let totalMemoryModernized = 0; -let validComparisons = 0; +let modernizedWins = 0 +let totalSpeedup = 0 +let totalMemoryLegacy = 0 +let totalMemoryModernized = 0 +let validComparisons = 0 results.forEach((r, i) => { if (!r.legacy.error && r.modernized.duration > 0) { - const speedup = r.legacy.duration / r.modernized.duration; + const speedup = r.legacy.duration / r.modernized.duration if (speedup > 1) { - modernizedWins++; + modernizedWins++ } - totalSpeedup += speedup; - totalMemoryLegacy += Math.abs(r.legacy.memory); - totalMemoryModernized += Math.abs(r.modernized.memory); - validComparisons++; + totalSpeedup += speedup + totalMemoryLegacy += Math.abs(r.legacy.memory) + totalMemoryModernized += Math.abs(r.modernized.memory) + validComparisons++ } -}); +}) if (validComparisons > 0) { - const avgSpeedup = (totalSpeedup / validComparisons).toFixed(2); - const avgMemLegacy = (totalMemoryLegacy / validComparisons).toFixed(2); - const avgMemModernized = (totalMemoryModernized / validComparisons).toFixed(2); - - console.log(`\nPerformance:`); - console.log(` Modernized won ${modernizedWins}/${validComparisons} benchmarks`); - console.log(` Average speedup: ${avgSpeedup}x faster`); - - console.log(`\nMemory Usage (average absolute delta):`); - console.log(` Legacy: ${avgMemLegacy} MB`); - console.log(` Modernized: ${avgMemModernized} MB`); - + const avgSpeedup = (totalSpeedup / validComparisons).toFixed(2) + const avgMemLegacy = (totalMemoryLegacy / validComparisons).toFixed(2) + const avgMemModernized = (totalMemoryModernized / validComparisons).toFixed(2) + + console.log('\nPerformance:') + console.log(` Modernized won ${modernizedWins}/${validComparisons} benchmarks`) + console.log(` Average speedup: ${avgSpeedup}x faster`) + + console.log('\nMemory Usage (average absolute delta):') + console.log(` Legacy: ${avgMemLegacy} MB`) + console.log(` Modernized: ${avgMemModernized} MB`) + if (totalMemoryModernized < totalMemoryLegacy) { - const memSavings = ((1 - totalMemoryModernized / totalMemoryLegacy) * 100).toFixed(1); - console.log(` Modernized uses ${memSavings}% less memory on average`); + const memSavings = ((1 - totalMemoryModernized / totalMemoryLegacy) * 100).toFixed(1) + console.log(` Modernized uses ${memSavings}% less memory on average`) } else { - const memIncrease = ((totalMemoryModernized / totalMemoryLegacy - 1) * 100).toFixed(1); - console.log(` Modernized uses ${memIncrease}% more memory on average`); + const memIncrease = ((totalMemoryModernized / totalMemoryLegacy - 1) * 100).toFixed(1) + console.log(` Modernized uses ${memIncrease}% more memory on average`) } - - console.log(`\nOverall: Modernized is ${avgSpeedup}x faster`); + + console.log(`\nOverall: Modernized is ${avgSpeedup}x faster`) } else { - console.log('Legacy implementation requires Sylvester library to be installed.'); - console.log('Install with: npm install sylvester'); - console.log('\nModernized implementation:'); + console.log('Legacy implementation requires Sylvester library to be installed.') + console.log('Install with: npm install sylvester') + console.log('\nModernized implementation:') results.forEach((r, i) => { - console.log(` Test ${i + 1}: ${r.modernized.duration.toFixed(2)} ms (${r.modernized.memory.toFixed(2)} MB)`); - }); + console.log(` Test ${i + 1}: ${r.modernized.duration.toFixed(2)} ms (${r.modernized.memory.toFixed(2)} MB)`) + }) } -console.log('\n' + '='.repeat(60)); +console.log('\n' + '='.repeat(60)) diff --git a/benchmark/logistic_regression_benchmark.js b/benchmark/logistic_regression_benchmark.js index 410428f..628a6bc 100644 --- a/benchmark/logistic_regression_benchmark.js +++ b/benchmark/logistic_regression_benchmark.js @@ -3,188 +3,188 @@ Benchmark comparing Logistic Regression Classifier implementations Original (Sylvester-based) vs Modernized (direct array manipulation) */ -'use strict'; +'use strict' -const LogisticRegressionLegacy = require('../lib/apparatus/classifier/logistic-regression-legacy'); -const LogisticRegressionModernized = require('../lib/apparatus/classifier/logistic-regression-modernized'); +const LogisticRegressionLegacy = require('../lib/apparatus/classifier/logistic-regression-legacy') +const LogisticRegressionModernized = require('../lib/apparatus/classifier/logistic-regression-modernized') /** * Generate random training data */ -function generateTrainingData(numSamples, numFeatures, numClasses) { - const data = []; - - for (let classIdx = 0; classIdx < numClasses; classIdx++) { - const samplesPerClass = numSamples / numClasses; - for (let i = 0; i < samplesPerClass; i++) { - const example = new Array(numFeatures); - for (let j = 0; j < numFeatures; j++) { - // Generate features biased by class - example[j] = Math.random() + (classIdx * 0.5); - } - data.push({ - features: example, - label: `class_${classIdx}` - }); - } +function generateTrainingData (numSamples, numFeatures, numClasses) { + const data = [] + + for (let classIdx = 0; classIdx < numClasses; classIdx++) { + const samplesPerClass = numSamples / numClasses + for (let i = 0; i < samplesPerClass; i++) { + const example = new Array(numFeatures) + for (let j = 0; j < numFeatures; j++) { + // Generate features biased by class + example[j] = Math.random() + (classIdx * 0.5) + } + data.push({ + features: example, + label: `class_${classIdx}` + }) } - - return data; + } + + return data } /** * Measure memory usage in MB */ -function getMemoryUsage() { - if (global.gc) { - global.gc(); - } - const used = process.memoryUsage(); - return { - heapUsed: Math.round(used.heapUsed / 1024 / 1024 * 100) / 100, - heapTotal: Math.round(used.heapTotal / 1024 / 1024 * 100) / 100, - external: Math.round(used.external / 1024 / 1024 * 100) / 100 - }; +function getMemoryUsage () { + if (global.gc) { + global.gc() + } + const used = process.memoryUsage() + return { + heapUsed: Math.round(used.heapUsed / 1024 / 1024 * 100) / 100, + heapTotal: Math.round(used.heapTotal / 1024 / 1024 * 100) / 100, + external: Math.round(used.external / 1024 / 1024 * 100) / 100 + } } /** * Benchmark training phase */ -function benchmarkTraining(ClassifierClass, data, label) { - const classifier = new ClassifierClass(); - - // Measure memory before training - const memBefore = getMemoryUsage(); - const startTime = process.hrtime.bigint(); - - // Add examples - for (let i = 0; i < data.length; i++) { - classifier.addExample(data[i].features, data[i].label); - } - - // Train - classifier.train(); - - const endTime = process.hrtime.bigint(); - const memAfter = getMemoryUsage(); - - const timeMs = Number(endTime - startTime) / 1000000; // Convert to milliseconds - const heapUsedDiff = memAfter.heapUsed - memBefore.heapUsed; - - return { - label, - trainingTime: Math.round(timeMs * 100) / 100, - memoryUsed: Math.round(heapUsedDiff * 100) / 100, - classifier - }; +function benchmarkTraining (ClassifierClass, data, label) { + const classifier = new ClassifierClass() + + // Measure memory before training + const memBefore = getMemoryUsage() + const startTime = process.hrtime.bigint() + + // Add examples + for (let i = 0; i < data.length; i++) { + classifier.addExample(data[i].features, data[i].label) + } + + // Train + classifier.train() + + const endTime = process.hrtime.bigint() + const memAfter = getMemoryUsage() + + const timeMs = Number(endTime - startTime) / 1000000 // Convert to milliseconds + const heapUsedDiff = memAfter.heapUsed - memBefore.heapUsed + + return { + label, + trainingTime: Math.round(timeMs * 100) / 100, + memoryUsed: Math.round(heapUsedDiff * 100) / 100, + classifier + } } /** * Benchmark classification phase */ -function benchmarkClassification(classifier, testData, label) { - const memBefore = getMemoryUsage(); - const startTime = process.hrtime.bigint(); - - // Classify all test samples - let correctCount = 0; - for (let i = 0; i < testData.length; i++) { - const result = classifier.getClassifications(testData[i].features); - // Check if top prediction matches actual label - if (result[0].label === testData[i].label) { - correctCount++; - } +function benchmarkClassification (classifier, testData, label) { + const memBefore = getMemoryUsage() + const startTime = process.hrtime.bigint() + + // Classify all test samples + let correctCount = 0 + for (let i = 0; i < testData.length; i++) { + const result = classifier.getClassifications(testData[i].features) + // Check if top prediction matches actual label + if (result[0].label === testData[i].label) { + correctCount++ } - - const endTime = process.hrtime.bigint(); - const memAfter = getMemoryUsage(); - - const timeMs = Number(endTime - startTime) / 1000000; - const heapUsedDiff = memAfter.heapUsed - memBefore.heapUsed; - const accuracy = Math.round((correctCount / testData.length) * 10000) / 100; - - return { - label, - classificationTime: Math.round(timeMs * 100) / 100, - memoryUsed: Math.round(heapUsedDiff * 100) / 100, - accuracy, - sampleCount: testData.length - }; + } + + const endTime = process.hrtime.bigint() + const memAfter = getMemoryUsage() + + const timeMs = Number(endTime - startTime) / 1000000 + const heapUsedDiff = memAfter.heapUsed - memBefore.heapUsed + const accuracy = Math.round((correctCount / testData.length) * 10000) / 100 + + return { + label, + classificationTime: Math.round(timeMs * 100) / 100, + memoryUsed: Math.round(heapUsedDiff * 100) / 100, + accuracy, + sampleCount: testData.length + } } /** * Main benchmark */ -function runBenchmark() { - console.log('='.repeat(80)); - console.log('Logistic Regression Classifier - Performance Benchmark'); - console.log('='.repeat(80)); - console.log(); - - const testConfigs = [ - { samples: 100, features: 10, classes: 2, name: 'Small (100 samples)' }, - { samples: 500, features: 20, classes: 3, name: 'Medium (500 samples)' }, - { samples: 2000, features: 50, classes: 5, name: 'Large (2000 samples)' } - ]; - - for (const config of testConfigs) { - console.log(`\n${'='.repeat(80)}`); - console.log(`Dataset: ${config.name}`); - console.log(`Features: ${config.features}, Classes: ${config.classes}`); - console.log('-'.repeat(80)); - - // Generate data split - const allData = generateTrainingData(config.samples, config.features, config.classes); - const trainSize = Math.floor(allData.length * 0.8); - const trainData = allData.slice(0, trainSize); - const testData = allData.slice(trainSize); - - console.log(`Training samples: ${trainData.length}, Test samples: ${testData.length}`); - console.log(); - - // Benchmark legacy (Sylvester) - console.log('Legacy Classifier (Sylvester):'); - const legacyTrain = benchmarkTraining(LogisticRegressionLegacy, trainData, 'Legacy'); - console.log(` Training time: ${legacyTrain.trainingTime} ms`); - console.log(` Memory used: ${legacyTrain.memoryUsed} MB`); - - const legacyClass = benchmarkClassification(legacyTrain.classifier, testData, 'Legacy'); - console.log(` Classification time: ${legacyClass.classificationTime} ms (${testData.length} samples)`); - console.log(` Memory used: ${legacyClass.memoryUsed} MB`); - console.log(` Accuracy: ${legacyClass.accuracy}%`); - console.log(); - - // Benchmark modernized - console.log('Modernized Classifier (Direct arrays):'); - const modernTrain = benchmarkTraining(LogisticRegressionModernized, trainData, 'Modernized'); - console.log(` Training time: ${modernTrain.trainingTime} ms`); - console.log(` Memory used: ${modernTrain.memoryUsed} MB`); - - const modernClass = benchmarkClassification(modernTrain.classifier, testData, 'Modernized'); - console.log(` Classification time: ${modernClass.classificationTime} ms (${testData.length} samples)`); - console.log(` Memory used: ${modernClass.memoryUsed} MB`); - console.log(` Accuracy: ${modernClass.accuracy}%`); - console.log(); - - // Calculate improvements - const speedupTrain = Math.round((legacyTrain.trainingTime / modernTrain.trainingTime) * 100) / 100; - const speedupClass = Math.round((legacyClass.classificationTime / modernClass.classificationTime) * 100) / 100; - const memSavingsTrain = Math.round((legacyTrain.memoryUsed - modernTrain.memoryUsed) * 100) / 100; - const memSavingsClass = Math.round((legacyClass.memoryUsed - modernClass.memoryUsed) * 100) / 100; - - console.log('IMPROVEMENTS (Modernized vs Legacy):'); - console.log(` Training speedup: ${speedupTrain}x faster`); - console.log(` Classification speedup: ${speedupClass}x faster`); - console.log(` Training memory saving: ${memSavingsTrain} MB (${Math.round((memSavingsTrain / Math.abs(legacyTrain.memoryUsed)) * 100)}%)`); - console.log(` Classification memory saving: ${memSavingsClass} MB (${Math.round((memSavingsClass / Math.abs(legacyClass.memoryUsed)) * 100)}%)`); - } - - console.log(); - console.log('='.repeat(80)); - console.log('Benchmark Complete'); - console.log('='.repeat(80)); +function runBenchmark () { + console.log('='.repeat(80)) + console.log('Logistic Regression Classifier - Performance Benchmark') + console.log('='.repeat(80)) + console.log() + + const testConfigs = [ + { samples: 100, features: 10, classes: 2, name: 'Small (100 samples)' }, + { samples: 500, features: 20, classes: 3, name: 'Medium (500 samples)' }, + { samples: 2000, features: 50, classes: 5, name: 'Large (2000 samples)' } + ] + + for (const config of testConfigs) { + console.log(`\n${'='.repeat(80)}`) + console.log(`Dataset: ${config.name}`) + console.log(`Features: ${config.features}, Classes: ${config.classes}`) + console.log('-'.repeat(80)) + + // Generate data split + const allData = generateTrainingData(config.samples, config.features, config.classes) + const trainSize = Math.floor(allData.length * 0.8) + const trainData = allData.slice(0, trainSize) + const testData = allData.slice(trainSize) + + console.log(`Training samples: ${trainData.length}, Test samples: ${testData.length}`) + console.log() + + // Benchmark legacy (Sylvester) + console.log('Legacy Classifier (Sylvester):') + const legacyTrain = benchmarkTraining(LogisticRegressionLegacy, trainData, 'Legacy') + console.log(` Training time: ${legacyTrain.trainingTime} ms`) + console.log(` Memory used: ${legacyTrain.memoryUsed} MB`) + + const legacyClass = benchmarkClassification(legacyTrain.classifier, testData, 'Legacy') + console.log(` Classification time: ${legacyClass.classificationTime} ms (${testData.length} samples)`) + console.log(` Memory used: ${legacyClass.memoryUsed} MB`) + console.log(` Accuracy: ${legacyClass.accuracy}%`) + console.log() + + // Benchmark modernized + console.log('Modernized Classifier (Direct arrays):') + const modernTrain = benchmarkTraining(LogisticRegressionModernized, trainData, 'Modernized') + console.log(` Training time: ${modernTrain.trainingTime} ms`) + console.log(` Memory used: ${modernTrain.memoryUsed} MB`) + + const modernClass = benchmarkClassification(modernTrain.classifier, testData, 'Modernized') + console.log(` Classification time: ${modernClass.classificationTime} ms (${testData.length} samples)`) + console.log(` Memory used: ${modernClass.memoryUsed} MB`) + console.log(` Accuracy: ${modernClass.accuracy}%`) + console.log() + + // Calculate improvements + const speedupTrain = Math.round((legacyTrain.trainingTime / modernTrain.trainingTime) * 100) / 100 + const speedupClass = Math.round((legacyClass.classificationTime / modernClass.classificationTime) * 100) / 100 + const memSavingsTrain = Math.round((legacyTrain.memoryUsed - modernTrain.memoryUsed) * 100) / 100 + const memSavingsClass = Math.round((legacyClass.memoryUsed - modernClass.memoryUsed) * 100) / 100 + + console.log('IMPROVEMENTS (Modernized vs Legacy):') + console.log(` Training speedup: ${speedupTrain}x faster`) + console.log(` Classification speedup: ${speedupClass}x faster`) + console.log(` Training memory saving: ${memSavingsTrain} MB (${Math.round((memSavingsTrain / Math.abs(legacyTrain.memoryUsed)) * 100)}%)`) + console.log(` Classification memory saving: ${memSavingsClass} MB (${Math.round((memSavingsClass / Math.abs(legacyClass.memoryUsed)) * 100)}%)`) + } + + console.log() + console.log('='.repeat(80)) + console.log('Benchmark Complete') + console.log('='.repeat(80)) } // Run benchmark -console.log('\nNote: For accurate memory measurements, run with: node --expose-gc benchmark/logistic_regression_benchmark.js\n'); -runBenchmark(); +console.log('\nNote: For accurate memory measurements, run with: node --expose-gc benchmark/logistic_regression_benchmark.js\n') +runBenchmark() diff --git a/index.js b/index.js index a4de1e2..48e3295 100644 --- a/index.js +++ b/index.js @@ -1,2 +1 @@ - -module.exports = require('./lib/apparatus/'); +module.exports = require('./lib/apparatus/') diff --git a/lib/apparatus/classifier/bayes_classifier.js b/lib/apparatus/classifier/bayes_classifier.js index f88b588..d98f80c 100644 --- a/lib/apparatus/classifier/bayes_classifier.js +++ b/lib/apparatus/classifier/bayes_classifier.js @@ -20,113 +20,115 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var util = require('util'), -Classifier = require('./classifier'); - -var BayesClassifier = function(smoothing) { - Classifier.call(this); - this.classFeatures = {}; - this.classTotals = {}; - this.totalExamples = 1; // start at one to smooth - this.smoothing = smoothing === undefined ? 1.0 : smoothing; -}; - -util.inherits(BayesClassifier, Classifier); - -function addExample(observation, label) { - if(!this.classFeatures[label]) { - this.classFeatures[label] = {}; - this.classTotals[label] = 1; // give an extra for smoothing - } +const util = require('util') +const Classifier = require('./classifier') + +const BayesClassifier = function (smoothing) { + Classifier.call(this) + this.classFeatures = {} + this.classTotals = {} + this.totalExamples = 1 // start at one to smooth + this.smoothing = smoothing === undefined ? 1.0 : smoothing +} - if(observation instanceof Array) { - var i = observation.length; - this.totalExamples++; - this.classTotals[label]++; - - while(i--) { - if(observation[i]) { - if(this.classFeatures[label][i]) { - this.classFeatures[label][i]++; - } else { - // give an extra for smoothing - this.classFeatures[label][i] = 1 + this.smoothing; - } - } - } - } else { - // sparse observation - for(var key in observation){ - value = observation[key]; - - if(this.classFeatures[label][value]) { - this.classFeatures[label][value]++; - } else { - // give an extra for smoothing - this.classFeatures[label][value] = 1 + this.smoothing; - } +util.inherits(BayesClassifier, Classifier) + +function addExample (observation, label) { + if (!this.classFeatures[label]) { + this.classFeatures[label] = {} + this.classTotals[label] = 1 // give an extra for smoothing + } + + if (observation instanceof Array) { + let i = observation.length + this.totalExamples++ + this.classTotals[label]++ + + while (i--) { + if (observation[i]) { + if (this.classFeatures[label][i]) { + this.classFeatures[label][i]++ + } else { + // give an extra for smoothing + this.classFeatures[label][i] = 1 + this.smoothing } + } + } + } else { + // sparse observation + for (const key in observation) { + value = observation[key] + + if (this.classFeatures[label][value]) { + this.classFeatures[label][value]++ + } else { + // give an extra for smoothing + this.classFeatures[label][value] = 1 + this.smoothing + } } + } } -function train() { +function train () { } -function probabilityOfClass(observation, label) { - var prob = 0; +function probabilityOfClass (observation, label) { + let prob = 0 - if(observation instanceof Array){ - var i = observation.length; + if (observation instanceof Array) { + let i = observation.length - while(i--) { - if(observation[i]) { - var count = this.classFeatures[label][i] || this.smoothing; - // numbers are tiny, add logs rather than take product - prob += Math.log(count / this.classTotals[label]); - } - } - } else { - // sparse observation - for(var key in observation){ - var count = this.classFeatures[label][observation[key]] || this.smoothing; - // numbers are tiny, add logs rather than take product - prob += Math.log(count / this.classTotals[label]); - } + while (i--) { + if (observation[i]) { + var count = this.classFeatures[label][i] || this.smoothing + // numbers are tiny, add logs rather than take product + prob += Math.log(count / this.classTotals[label]) + } } + } else { + // sparse observation + for (const key in observation) { + var count = this.classFeatures[label][observation[key]] || this.smoothing + // numbers are tiny, add logs rather than take product + prob += Math.log(count / this.classTotals[label]) + } + } - // p(C) * unlogging the above calculation P(X|C) - prob = (this.classTotals[label] / this.totalExamples) * Math.exp(prob); + // p(C) * unlogging the above calculation P(X|C) + prob = (this.classTotals[label] / this.totalExamples) * Math.exp(prob) - return prob; + return prob } -function getClassifications(observation) { - var classifier = this; - var labels = []; +function getClassifications (observation) { + const classifier = this + const labels = [] - for(var className in this.classFeatures) { - labels.push({label: className, - value: classifier.probabilityOfClass(observation, className)}); - } + for (const className in this.classFeatures) { + labels.push({ + label: className, + value: classifier.probabilityOfClass(observation, className) + }) + } - return labels.sort(function(x, y) { - return y.value - x.value; - }); + return labels.sort(function (x, y) { + return y.value - x.value + }) } -function restore(classifier) { - classifier = Classifier.restore(classifier); - classifier.__proto__ = BayesClassifier.prototype; +function restore (classifier) { + classifier = Classifier.restore(classifier) + classifier.__proto__ = BayesClassifier.prototype - return classifier; + return classifier } -BayesClassifier.prototype.addExample = addExample; -BayesClassifier.prototype.train = train; -BayesClassifier.prototype.getClassifications = getClassifications; -BayesClassifier.prototype.probabilityOfClass = probabilityOfClass; +BayesClassifier.prototype.addExample = addExample +BayesClassifier.prototype.train = train +BayesClassifier.prototype.getClassifications = getClassifications +BayesClassifier.prototype.probabilityOfClass = probabilityOfClass -BayesClassifier.restore = restore; +BayesClassifier.restore = restore -module.exports = BayesClassifier; \ No newline at end of file +module.exports = BayesClassifier diff --git a/lib/apparatus/classifier/classifier.js b/lib/apparatus/classifier/classifier.js index 1725add..7e4bd83 100644 --- a/lib/apparatus/classifier/classifier.js +++ b/lib/apparatus/classifier/classifier.js @@ -20,35 +20,35 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -function Classifier() { +function Classifier () { } -function restore(classifier) { - classifier = typeof classifier == 'string' ? JSON.parse(classifier) : classifier; +function restore (classifier) { + classifier = typeof classifier === 'string' ? JSON.parse(classifier) : classifier - return classifier; + return classifier } -function addExample(observation, classification) { - throw 'Not implemented'; +function addExample (observation, classification) { + throw 'Not implemented' } -function classify(observation) { - var classifications = this.getClassifications(observation); - if(!classifications || classifications.length === 0) { - throw "Not Trained"; - } - return classifications[0].label; +function classify (observation) { + const classifications = this.getClassifications(observation) + if (!classifications || classifications.length === 0) { + throw 'Not Trained' + } + return classifications[0].label } -function train() { - throw 'Not implemented'; +function train () { + throw 'Not implemented' } -Classifier.prototype.addExample = addExample; -Classifier.prototype.train = train; -Classifier.prototype.classify = classify; +Classifier.prototype.addExample = addExample +Classifier.prototype.train = train +Classifier.prototype.classify = classify -Classifier.restore = restore; +Classifier.restore = restore -module.exports = Classifier; +module.exports = Classifier diff --git a/lib/apparatus/classifier/logistic-regression-legacy.js b/lib/apparatus/classifier/logistic-regression-legacy.js index 043e898..f1c6b34 100644 --- a/lib/apparatus/classifier/logistic-regression-legacy.js +++ b/lib/apparatus/classifier/logistic-regression-legacy.js @@ -20,173 +20,169 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var util = require('util'), - Classifier = require('./classifier'); +const util = require('util') +const Classifier = require('./classifier') -var sylvester = require('sylvester'), -Matrix = sylvester.Matrix, -Vector = sylvester.Vector; +const sylvester = require('sylvester') +const Matrix = sylvester.Matrix +const Vector = sylvester.Vector -function sigmoid(z) { - return 1 / (1 + Math.exp(0 - z)); +function sigmoid (z) { + return 1 / (1 + Math.exp(0 - z)) } -function hypothesis(theta, Observations) { - return Observations.x(theta).map(sigmoid); +function hypothesis (theta, Observations) { + return Observations.x(theta).map(sigmoid) } -function cost(theta, Examples, classifications) { - var hypothesisResult = hypothesis(theta, Examples); +function cost (theta, Examples, classifications) { + const hypothesisResult = hypothesis(theta, Examples) - var ones = Vector.One(Examples.rows()); - var cost_1 = Vector.Zero(Examples.rows()).subtract(classifications).elementMultiply(hypothesisResult.log()); - var cost_0 = ones.subtract(classifications).elementMultiply(ones.subtract(hypothesisResult).log()); + const ones = Vector.One(Examples.rows()) + const cost_1 = Vector.Zero(Examples.rows()).subtract(classifications).elementMultiply(hypothesisResult.log()) + const cost_0 = ones.subtract(classifications).elementMultiply(ones.subtract(hypothesisResult).log()) - return (1 / Examples.rows()) * cost_1.subtract(cost_0).sum(); + return (1 / Examples.rows()) * cost_1.subtract(cost_0).sum() } -function descendGradient(theta, Examples, classifications) { - var maxIt = 500 * Examples.rows(); - var last; - var current; - var learningRate = 3; - var learningRateFound = false; +function descendGradient (theta, Examples, classifications) { + const maxIt = 500 * Examples.rows() + let last + let current + let learningRate = 3 + let learningRateFound = false - Examples = Matrix.One(Examples.rows(), 1).augment(Examples); - theta = theta.augment([0]); + Examples = Matrix.One(Examples.rows(), 1).augment(Examples) + theta = theta.augment([0]) - while(!learningRateFound && learningRate !== 0) { - var i = 0; - last = null; + while (!learningRateFound && learningRate !== 0) { + let i = 0 + last = null - while(true) { - var hypothesisResult = hypothesis(theta, Examples); - theta = theta.subtract(Examples.transpose().x( - hypothesisResult.subtract(classifications)).x(1 / Examples.rows()).x(learningRate)); - current = cost(theta, Examples, classifications); + while (true) { + const hypothesisResult = hypothesis(theta, Examples) + theta = theta.subtract(Examples.transpose().x( + hypothesisResult.subtract(classifications)).x(1 / Examples.rows()).x(learningRate)) + current = cost(theta, Examples, classifications) - i++; + i++ - if(last) { - if(current < last) - learningRateFound = true; - else - break; + if (last) { + if (current < last) { learningRateFound = true } else { break } - if(last - current < 0.0001) - break; - } + if (last - current < 0.0001) { break } + } - if(i >= maxIt) { - throw 'unable to find minimum'; - } + if (i >= maxIt) { + throw 'unable to find minimum' + } - last = current; - } - - learningRate /= 3; + last = current } - return theta.chomp(1); -} + learningRate /= 3 + } -var LogisticRegressionClassifier = function() { - Classifier.call(this); - this.examples = {}; - this.features = []; - this.featurePositions = {}; - this.maxFeaturePosition = 0; - this.classifications = []; - this.exampleCount = 0; -}; + return theta.chomp(1) +} -util.inherits(LogisticRegressionClassifier, Classifier); +const LogisticRegressionClassifier = function () { + Classifier.call(this) + this.examples = {} + this.features = [] + this.featurePositions = {} + this.maxFeaturePosition = 0 + this.classifications = [] + this.exampleCount = 0 +} -function createClassifications() { - var classifications = []; +util.inherits(LogisticRegressionClassifier, Classifier) - for(var i = 0; i < this.exampleCount; i++) { - var classification = []; +function createClassifications () { + const classifications = [] - for(var _ in this.examples) { - classification.push(0); - } + for (let i = 0; i < this.exampleCount; i++) { + const classification = [] - classifications.push(classification); + for (const _ in this.examples) { + classification.push(0) } - return classifications; + classifications.push(classification) + } + + return classifications } -function computeThetas(Examples, Classifications) { - this.theta = []; +function computeThetas (Examples, Classifications) { + this.theta = [] - // each class will have it's own theta. - var zero = function() { return 0; }; - for(var i = 1; i <= this.classifications.length; i++) { - var theta = Examples.row(1).map(zero); - this.theta.push(descendGradient(theta, Examples, Classifications.column(i))); - } + // each class will have it's own theta. + const zero = function () { return 0 } + for (let i = 1; i <= this.classifications.length; i++) { + const theta = Examples.row(1).map(zero) + this.theta.push(descendGradient(theta, Examples, Classifications.column(i))) + } } -function train() { - var examples = []; - var classifications = this.createClassifications(); - var d = 0, c = 0; - - for(var classification in this.examples) { - for(var i = 0; i < this.examples[classification].length; i++) { - var doc = this.examples[classification][i]; - var example = doc; +function train () { + const examples = [] + const classifications = this.createClassifications() + let d = 0; let c = 0 - examples.push(example); - classifications[d][c] = 1; - d++; - } + for (const classification in this.examples) { + for (let i = 0; i < this.examples[classification].length; i++) { + const doc = this.examples[classification][i] + const example = doc - c++; + examples.push(example) + classifications[d][c] = 1 + d++ } - this.computeThetas($M(examples), $M(classifications)); + c++ + } + + this.computeThetas($M(examples), $M(classifications)) } -function addExample(data, classification) { - if(!this.examples[classification]) { - this.examples[classification] = []; - this.classifications.push(classification); - } +function addExample (data, classification) { + if (!this.examples[classification]) { + this.examples[classification] = [] + this.classifications.push(classification) + } - this.examples[classification].push(data); - this.exampleCount++; + this.examples[classification].push(data) + this.exampleCount++ } -function getClassifications(observation) { - observation = $V(observation); - var classifications = []; +function getClassifications (observation) { + observation = $V(observation) + const classifications = [] - for(var i = 0; i < this.theta.length; i++) { - classifications.push({label: this.classifications[i], value: sigmoid(observation.dot(this.theta[i])) }); - } + for (let i = 0; i < this.theta.length; i++) { + classifications.push({ label: this.classifications[i], value: sigmoid(observation.dot(this.theta[i])) }) + } - return classifications.sort(function(x, y) { - return y.value - x.value; - }); + return classifications.sort(function (x, y) { + return y.value - x.value + }) } -function restore(classifier) { - classifier = Classifier.restore(classifier); - classifier.__proto__ = LogisticRegressionClassifier.prototype; +function restore (classifier) { + classifier = Classifier.restore(classifier) + classifier.__proto__ = LogisticRegressionClassifier.prototype - return classifier; + return classifier } -LogisticRegressionClassifier.prototype.addExample = addExample; -LogisticRegressionClassifier.prototype.restore = restore; -LogisticRegressionClassifier.prototype.train = train; -LogisticRegressionClassifier.prototype.createClassifications = createClassifications; -LogisticRegressionClassifier.prototype.computeThetas = computeThetas; -LogisticRegressionClassifier.prototype.getClassifications = getClassifications; +LogisticRegressionClassifier.prototype.addExample = addExample +LogisticRegressionClassifier.prototype.restore = restore +LogisticRegressionClassifier.prototype.train = train +LogisticRegressionClassifier.prototype.createClassifications = createClassifications +LogisticRegressionClassifier.prototype.computeThetas = computeThetas +LogisticRegressionClassifier.prototype.getClassifications = getClassifications -LogisticRegressionClassifier.restore = restore; +LogisticRegressionClassifier.restore = restore -module.exports = LogisticRegressionClassifier; +module.exports = LogisticRegressionClassifier diff --git a/lib/apparatus/classifier/logistic-regression-modernized.js b/lib/apparatus/classifier/logistic-regression-modernized.js index 46d8a29..dc3dd73 100644 --- a/lib/apparatus/classifier/logistic-regression-modernized.js +++ b/lib/apparatus/classifier/logistic-regression-modernized.js @@ -20,25 +20,25 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var util = require('util'), - Classifier = require('./classifier'); +const util = require('util') +const Classifier = require('./classifier') /** * Sigmoid function */ -function sigmoid(z) { - return 1 / (1 + Math.exp(-z)); +function sigmoid (z) { + return 1 / (1 + Math.exp(-z)) } /** * Dot product of two vectors (arrays) */ -function dotProduct(a, b) { - let sum = 0; - for (let i = 0; i < a.length; i++) { - sum += a[i] * b[i]; - } - return sum; +function dotProduct (a, b) { + let sum = 0 + for (let i = 0; i < a.length; i++) { + sum += a[i] * b[i] + } + return sum } /** @@ -47,12 +47,12 @@ function dotProduct(a, b) { * vector: array (cols) * returns: array (rows) */ -function matrixVectorMultiply(matrix, vector) { - const result = new Array(matrix.length); - for (let i = 0; i < matrix.length; i++) { - result[i] = dotProduct(matrix[i], vector); - } - return result; +function matrixVectorMultiply (matrix, vector) { + const result = new Array(matrix.length) + for (let i = 0; i < matrix.length; i++) { + result[i] = dotProduct(matrix[i], vector) + } + return result } /** @@ -60,20 +60,20 @@ function matrixVectorMultiply(matrix, vector) { * matrix: array of arrays (rows x cols) * returns: array of arrays (cols x rows) */ -function matrixTranspose(matrix) { - if (matrix.length === 0) return []; - - const rows = matrix.length; - const cols = matrix[0].length; - const result = Array(cols); - - for (let j = 0; j < cols; j++) { - result[j] = new Array(rows); - for (let i = 0; i < rows; i++) { - result[j][i] = matrix[i][j]; - } +function matrixTranspose (matrix) { + if (matrix.length === 0) return [] + + const rows = matrix.length + const cols = matrix[0].length + const result = Array(cols) + + for (let j = 0; j < cols; j++) { + result[j] = new Array(rows) + for (let i = 0; i < rows; i++) { + result[j][i] = matrix[i][j] } - return result; + } + return result } /** @@ -82,208 +82,208 @@ function matrixTranspose(matrix) { * b: array of arrays (n x p) * returns: array of arrays (m x p) */ -function matrixMultiply(a, b) { - const m = a.length; - const n = a[0].length; - const p = b[0].length; - const result = Array(m); - - for (let i = 0; i < m; i++) { - result[i] = new Array(p); - for (let j = 0; j < p; j++) { - let sum = 0; - for (let k = 0; k < n; k++) { - sum += a[i][k] * b[k][j]; - } - result[i][j] = sum; - } +function matrixMultiply (a, b) { + const m = a.length + const n = a[0].length + const p = b[0].length + const result = Array(m) + + for (let i = 0; i < m; i++) { + result[i] = new Array(p) + for (let j = 0; j < p; j++) { + let sum = 0 + for (let k = 0; k < n; k++) { + sum += a[i][k] * b[k][j] + } + result[i][j] = sum } - return result; + } + return result } /** * Element-wise operations */ -function elementWiseSubtract(a, b) { - const result = new Array(a.length); - for (let i = 0; i < a.length; i++) { - result[i] = a[i] - b[i]; - } - return result; +function elementWiseSubtract (a, b) { + const result = new Array(a.length) + for (let i = 0; i < a.length; i++) { + result[i] = a[i] - b[i] + } + return result } -function elementWiseMultiply(a, b) { - const result = new Array(a.length); - for (let i = 0; i < a.length; i++) { - result[i] = a[i] * b[i]; - } - return result; +function elementWiseMultiply (a, b) { + const result = new Array(a.length) + for (let i = 0; i < a.length; i++) { + result[i] = a[i] * b[i] + } + return result } -function elementWiseLog(a) { - const result = new Array(a.length); - for (let i = 0; i < a.length; i++) { - result[i] = Math.log(a[i]); - } - return result; +function elementWiseLog (a) { + const result = new Array(a.length) + for (let i = 0; i < a.length; i++) { + result[i] = Math.log(a[i]) + } + return result } -function elementWiseApply(a, fn) { - const result = new Array(a.length); - for (let i = 0; i < a.length; i++) { - result[i] = fn(a[i]); - } - return result; +function elementWiseApply (a, fn) { + const result = new Array(a.length) + for (let i = 0; i < a.length; i++) { + result[i] = fn(a[i]) + } + return result } /** * Sum of array elements */ -function sum(arr) { - let total = 0; - for (let i = 0; i < arr.length; i++) { - total += arr[i]; - } - return total; +function sum (arr) { + let total = 0 + for (let i = 0; i < arr.length; i++) { + total += arr[i] + } + return total } /** * Scalar multiply */ -function scalarMultiply(arr, scalar) { - const result = new Array(arr.length); - for (let i = 0; i < arr.length; i++) { - result[i] = arr[i] * scalar; - } - return result; +function scalarMultiply (arr, scalar) { + const result = new Array(arr.length) + for (let i = 0; i < arr.length; i++) { + result[i] = arr[i] * scalar + } + return result } /** * Matrix scalar multiply */ -function matrixScalarMultiply(matrix, scalar) { - const result = new Array(matrix.length); - for (let i = 0; i < matrix.length; i++) { - result[i] = scalarMultiply(matrix[i], scalar); - } - return result; +function matrixScalarMultiply (matrix, scalar) { + const result = new Array(matrix.length) + for (let i = 0; i < matrix.length; i++) { + result[i] = scalarMultiply(matrix[i], scalar) + } + return result } /** * Augment matrix with ones column */ -function augmentWithOnes(matrix) { - const result = new Array(matrix.length); - for (let i = 0; i < matrix.length; i++) { - result[i] = [1, ...matrix[i]]; - } - return result; +function augmentWithOnes (matrix) { + const result = new Array(matrix.length) + for (let i = 0; i < matrix.length; i++) { + result[i] = [1, ...matrix[i]] + } + return result } /** * Remove first element from each column (inverse of augment) */ -function removeAugmentation(matrix) { - const result = new Array(matrix.length); - for (let i = 0; i < matrix.length; i++) { - result[i] = matrix[i].slice(1); - } - return result; +function removeAugmentation (matrix) { + const result = new Array(matrix.length) + for (let i = 0; i < matrix.length; i++) { + result[i] = matrix[i].slice(1) + } + return result } /** * Cost function */ -function cost(theta, Examples, classifications) { - const hypothesisResult = matrixVectorMultiply(Examples, theta); - const sigmoidResult = elementWiseApply(hypothesisResult, sigmoid); - - const numExamples = Examples.length; - const ones = Array(numExamples).fill(1); - - // cost_1 = (-classifications) .* log(sigmoidResult) - const negClassifications = elementWiseMultiply(classifications, Array(numExamples).fill(-1)); - const cost_1 = elementWiseMultiply( - negClassifications, - elementWiseLog(sigmoidResult) - ); - - // cost_0 = (1 - classifications) .* log(1 - sigmoidResult) - const oneMinusHypothesis = elementWiseSubtract(ones, sigmoidResult); - const oneMinusClassifications = elementWiseSubtract(ones, classifications); - const cost_0 = elementWiseMultiply( - oneMinusClassifications, - elementWiseLog(oneMinusHypothesis) - ); - - const totalCost = sum(elementWiseSubtract(cost_1, cost_0)); - return totalCost / numExamples; +function cost (theta, Examples, classifications) { + const hypothesisResult = matrixVectorMultiply(Examples, theta) + const sigmoidResult = elementWiseApply(hypothesisResult, sigmoid) + + const numExamples = Examples.length + const ones = Array(numExamples).fill(1) + + // cost_1 = (-classifications) .* log(sigmoidResult) + const negClassifications = elementWiseMultiply(classifications, Array(numExamples).fill(-1)) + const cost_1 = elementWiseMultiply( + negClassifications, + elementWiseLog(sigmoidResult) + ) + + // cost_0 = (1 - classifications) .* log(1 - sigmoidResult) + const oneMinusHypothesis = elementWiseSubtract(ones, sigmoidResult) + const oneMinusClassifications = elementWiseSubtract(ones, classifications) + const cost_0 = elementWiseMultiply( + oneMinusClassifications, + elementWiseLog(oneMinusHypothesis) + ) + + const totalCost = sum(elementWiseSubtract(cost_1, cost_0)) + return totalCost / numExamples } /** * Descending gradient function - trains the model * Optimized for speed with pre-computed matrix transpose */ -function descendGradient(theta, Examples, classifications) { - const maxIterPerRate = 100; - let learningRate = 1.0; - let currentTheta = theta.slice(); - let bestTheta = theta.slice(); - let bestCost = cost(currentTheta, Examples, classifications); - let learningRateFound = false; - const m = Examples.length; - - // Pre-compute transpose once - this is the key optimization! - const ExamplesTransposed = matrixTranspose(Examples); - - while (!learningRateFound && learningRate > 0.0001) { - let iterationCount = 0; - let lastCost = bestCost; - let improvementCount = 0; - - while (iterationCount < maxIterPerRate) { - // Compute hypothesis and error - const hypothesisResult = matrixVectorMultiply(Examples, currentTheta); - const sigmoidResult = elementWiseApply(hypothesisResult, sigmoid); - - // Gradient = X^T * (h(X) - y) / m - const error = elementWiseSubtract(sigmoidResult, classifications); - const gradient = matrixVectorMultiply(ExamplesTransposed, error); - const scaledGradient = scalarMultiply(gradient, learningRate / m); - - // Update theta - currentTheta = elementWiseSubtract(currentTheta, scaledGradient); - - // Evaluate cost - const currentCost = cost(currentTheta, Examples, classifications); - - if (currentCost < bestCost) { - bestCost = currentCost; - bestTheta = currentTheta.slice(); - improvementCount++; - } - - // Check for convergence with improved logic - if (lastCost - currentCost < 0.00001) { - learningRateFound = true; - break; - } - - lastCost = currentCost; - iterationCount++; - } - - // If we made progress, accept this learning rate - if (improvementCount > maxIterPerRate * 0.1) { - learningRateFound = true; - } else { - // Try smaller learning rate - learningRate *= 0.5; - currentTheta = bestTheta.slice(); // Reset to best known theta - } +function descendGradient (theta, Examples, classifications) { + const maxIterPerRate = 100 + let learningRate = 1.0 + let currentTheta = theta.slice() + let bestTheta = theta.slice() + let bestCost = cost(currentTheta, Examples, classifications) + let learningRateFound = false + const m = Examples.length + + // Pre-compute transpose once - this is the key optimization! + const ExamplesTransposed = matrixTranspose(Examples) + + while (!learningRateFound && learningRate > 0.0001) { + let iterationCount = 0 + let lastCost = bestCost + let improvementCount = 0 + + while (iterationCount < maxIterPerRate) { + // Compute hypothesis and error + const hypothesisResult = matrixVectorMultiply(Examples, currentTheta) + const sigmoidResult = elementWiseApply(hypothesisResult, sigmoid) + + // Gradient = X^T * (h(X) - y) / m + const error = elementWiseSubtract(sigmoidResult, classifications) + const gradient = matrixVectorMultiply(ExamplesTransposed, error) + const scaledGradient = scalarMultiply(gradient, learningRate / m) + + // Update theta + currentTheta = elementWiseSubtract(currentTheta, scaledGradient) + + // Evaluate cost + const currentCost = cost(currentTheta, Examples, classifications) + + if (currentCost < bestCost) { + bestCost = currentCost + bestTheta = currentTheta.slice() + improvementCount++ + } + + // Check for convergence with improved logic + if (lastCost - currentCost < 0.00001) { + learningRateFound = true + break + } + + lastCost = currentCost + iterationCount++ } - return bestTheta.slice(1); // Remove augmented 0 at the beginning + // If we made progress, accept this learning rate + if (improvementCount > maxIterPerRate * 0.1) { + learningRateFound = true + } else { + // Try smaller learning rate + learningRate *= 0.5 + currentTheta = bestTheta.slice() // Reset to best known theta + } + } + + return bestTheta.slice(1) // Remove augmented 0 at the beginning } /** @@ -310,7 +310,7 @@ class LogisticRegressionModernized extends Classifier { for (let i = 0; i < this.exampleCount; i++) { const classification = [] - for (let _ in this.examples) { + for (const _ in this.examples) { classification.push(0) } @@ -328,14 +328,14 @@ class LogisticRegressionModernized extends Classifier { // each class will have its own theta const zeroVector = new Array(Examples[0].length).fill(0) - + for (let i = 0; i < this.classifications.length; i++) { // Extract column i from Classifications const classColumn = new Array(Classifications.length) for (let j = 0; j < Classifications.length; j++) { classColumn[j] = Classifications[j][i] } - + this.theta.push(descendGradient(zeroVector, Examples, classColumn)) } } @@ -349,7 +349,7 @@ class LogisticRegressionModernized extends Classifier { let d = 0 let c = 0 - for (let classification in this.examples) { + for (const classification in this.examples) { for (let i = 0; i < this.examples[classification].length; i++) { const doc = this.examples[classification][i] examples.push(doc) diff --git a/lib/apparatus/classifier/logistic_regression_classifier.js b/lib/apparatus/classifier/logistic_regression_classifier.js index 65a4f9c..4669225 100644 --- a/lib/apparatus/classifier/logistic_regression_classifier.js +++ b/lib/apparatus/classifier/logistic_regression_classifier.js @@ -21,4 +21,4 @@ THE SOFTWARE. */ // Use the modernized implementation directly -module.exports = require('./logistic-regression-modernized'); +module.exports = require('./logistic-regression-modernized') diff --git a/lib/apparatus/classifier/randomforest_classifier.js b/lib/apparatus/classifier/randomforest_classifier.js index c3c0a2d..c49ccd9 100644 --- a/lib/apparatus/classifier/randomforest_classifier.js +++ b/lib/apparatus/classifier/randomforest_classifier.js @@ -21,411 +21,414 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var util = require('util'), - Classifier = require('./classifier'); - -var RandomForestClassifier = function(options) { - Classifier.call(this); - this.options = options; - this.examples = []; - this.example_size = 0; - this.labels = []; -}; - +const util = require('util') +const Classifier = require('./classifier') + +const RandomForestClassifier = function (options) { + Classifier.call(this) + this.options = options + this.examples = [] + this.example_size = 0 + this.labels = [] +} + /* data is 2D array of size N x D of examples labels is a 1D array of labels (only -1 or 1 for now). In future will support multiclass or maybe even regression options.numTrees can be used to customize number of trees to train (default = 100) options.maxDepth is the maximum depth of each tree in the forest (default = 4) options.numTries is the number of random hypotheses generated at each node during training (default = 10) - options.trainFun is a function with signature "function myWeakTrain(data, labels, ix, options)". Here, ix is a list of - indexes into data of the instances that should be payed attention to. Everything not in the list - should be ignored. This is done for efficiency. The function should return a model where you store + options.trainFun is a function with signature "function myWeakTrain(data, labels, ix, options)". Here, ix is a list of + indexes into data of the instances that should be payed attention to. Everything not in the list + should be ignored. This is done for efficiency. The function should return a model where you store variables. (i.e. model = {}; model.myvar = 5;) This will be passed to testFun. options.testFun is a function with signature "funtion myWeakTest(inst, model)" where inst is 1D array specifying an example, and model will be the same model that you return in options.trainFun. For example, model.myvar will be 5. see decisionStumpTrain() and decisionStumpTest() below for example. */ -function trainRandomForest(data, labels, options) { - options = options || {}; - this.numTrees = options.numTrees || 100; - - // initialize many trees and train them all independently - this.trees= new Array(this.numTrees); - for(var i=0;i %f, %f. Gain %f", thr, H, LH, RH, informationGain); - if(informationGain > bestGain || i === 0) { - bestGain= informationGain; - bestThr= thr; - } +function decisionStumpTrain (data, labels, ix, options) { + options = options || {} + const numtries = options.numTries || 10 + + // choose a dimension at random and pick a best split + const ri = randi(0, data[0].length) + const N = ix.length + + // evaluate class entropy of incoming data + const H = entropy(labels, ix) + let bestGain = 0 + let bestThr = 0 + for (let i = 0; i < numtries; i++) { + // pick a random splitting threshold + const ix1 = ix[randi(0, N)] + let ix2 = ix[randi(0, N)] + while (ix2 == ix1) ix2 = ix[randi(0, N)] // enforce distinctness of ix2 + + const a = Math.random() + const thr = data[ix1][ri] * a + data[ix2][ri] * (1 - a) + + // measure information gain we'd get from split with thr + let l1 = 1; let r1 = 1; let lm1 = 1; let rm1 = 1 // counts for Left and label 1, right and label 1, left and minus 1, right and minus 1 + for (let j = 0; j < ix.length; j++) { + if (data[ix[j]][ri] < thr) { + if (labels[ix[j]] == 1) l1++ + else lm1++ + } else { + if (labels[ix[j]] == 1) r1++ + else rm1++ + } } - - model= {}; - model.thr= bestThr; - model.ri= ri; - return model; + let t = l1 + lm1 // normalize the counts to obtain probability estimates + l1 = l1 / t + lm1 = lm1 / t + t = r1 + rm1 + r1 = r1 / t + rm1 = rm1 / t + + const LH = -l1 * Math.log(l1) - lm1 * Math.log(lm1) // left and right entropy + const RH = -r1 * Math.log(r1) - rm1 * Math.log(rm1) + + const informationGain = H - LH - RH + // console.log("Considering split %f, entropy %f -> %f, %f. Gain %f", thr, H, LH, RH, informationGain); + if (informationGain > bestGain || i === 0) { + bestGain = informationGain + bestThr = thr + } + } + + model = {} + model.thr = bestThr + model.ri = ri + return model } - + // returns a decision for a single data instance -function decisionStumpTest(inst, model) { - if(!model) { - // this is a leaf that never received any data... - return 1; - } - return inst[model.ri] < model.thr ? 1 : -1; +function decisionStumpTest (inst, model) { + if (!model) { + // this is a leaf that never received any data... + return 1 + } + return inst[model.ri] < model.thr ? 1 : -1 } - + // returns model. Code duplication with decisionStumpTrain :( -function decision2DStumpTrain(data, labels, ix, options) { - - options = options || {}; - var numtries = options.numTries || 10; - - // choose a dimension at random and pick a best split - var N = ix.length; - - var ri1= 0; - var ri2= 1; - if(data[0].length > 2) { - // more than 2D data. Pick 2 random dimensions - ri1= randi(0, data[0].length); - ri2= randi(0, data[0].length); - while(ri2 == ri1) ri2= randi(0, data[0].length); // must be distinct! +function decision2DStumpTrain (data, labels, ix, options) { + options = options || {} + const numtries = options.numTries || 10 + + // choose a dimension at random and pick a best split + const N = ix.length + + let ri1 = 0 + let ri2 = 1 + if (data[0].length > 2) { + // more than 2D data. Pick 2 random dimensions + ri1 = randi(0, data[0].length) + ri2 = randi(0, data[0].length) + while (ri2 == ri1) ri2 = randi(0, data[0].length) // must be distinct! + } + + // evaluate class entropy of incoming data + const H = entropy(labels, ix) + let bestGain = 0 + let bestw1, bestw2, bestthr + const dots = new Array(ix.length) + for (let i = 0; i < numtries; i++) { + // pick random line parameters + const alpha = randf(0, 2 * Math.PI) + const w1 = Math.cos(alpha) + const w2 = Math.sin(alpha) + + // project data on this line and get the dot products + for (var j = 0; j < ix.length; j++) { + dots[j] = w1 * data[ix[j]][ri1] + w2 * data[ix[j]][ri2] } - - // evaluate class entropy of incoming data - var H= entropy(labels, ix); - var bestGain=0; - var bestw1, bestw2, bestthr; - var dots= new Array(ix.length); - for(var i=0;i %f, %f. Gain %f", thr, H, LH, RH, informationGain); - if(informationGain > bestGain || i === 0) { - bestGain= informationGain; - bestw1= w1; - bestw2= w2; - bestthr= dotthr; - } + + // we are in a tricky situation because data dot product distribution + // can be skewed. So we don't want to select just randomly between + // min and max. But we also don't want to sort as that is too expensive + // let's pick two random points and make the threshold be somewhere between them. + // for skewed datasets, the selected points will with relatively high likelihood + // be in the high-desnity regions, so the thresholds will make sense + const ix1 = ix[randi(0, N)] + let ix2 = ix[randi(0, N)] + while (ix2 == ix1) ix2 = ix[randi(0, N)] // enforce distinctness of ix2 + const a = Math.random() + const dotthr = dots[ix1] * a + dots[ix2] * (1 - a) + + // measure information gain we'd get from split with thr + let l1 = 1; let r1 = 1; let lm1 = 1; let rm1 = 1 // counts for Left and label 1, right and label 1, left and minus 1, right and minus 1 + for (var j = 0; j < ix.length; j++) { + if (dots[j] < dotthr) { + if (labels[ix[j]] == 1) l1++ + else lm1++ + } else { + if (labels[ix[j]] == 1) r1++ + else rm1++ + } } - - model= {}; - model.w1= bestw1; - model.w2= bestw2; - model.dotthr= bestthr; - return model; + let t = l1 + lm1 + l1 = l1 / t + lm1 = lm1 / t + t = r1 + rm1 + r1 = r1 / t + rm1 = rm1 / t + + const LH = -l1 * Math.log(l1) - lm1 * Math.log(lm1) // left and right entropy + const RH = -r1 * Math.log(r1) - rm1 * Math.log(rm1) + + const informationGain = H - LH - RH + // console.log("Considering split %f, entropy %f -> %f, %f. Gain %f", thr, H, LH, RH, informationGain); + if (informationGain > bestGain || i === 0) { + bestGain = informationGain + bestw1 = w1 + bestw2 = w2 + bestthr = dotthr + } + } + + model = {} + model.w1 = bestw1 + model.w2 = bestw2 + model.dotthr = bestthr + return model } - + // returns label for a single data instance -function decision2DStumpTest(inst, model) { - if(!model) { - // this is a leaf that never received any data... - return 1; - } - return inst[0]*model.w1 + inst[1]*model.w2 < model.dotthr ? 1 : -1; +function decision2DStumpTest (inst, model) { + if (!model) { + // this is a leaf that never received any data... + return 1 + } + return inst[0] * model.w1 + inst[1] * model.w2 < model.dotthr ? 1 : -1 } - + // Misc utility functions -function entropy(labels, ix) { - var N= ix.length; - var p=0.0; - for(var i=0;i this.example_size) { - var new_size = observation.length; - this.example_size = new_size; - for(var i = 0; i < this.examples.length; i++){ - var e = this.examples[i]; - for(var j=e.length; e this.example_size) { + const new_size = observation.length + this.example_size = new_size + for (let i = 0; i < this.examples.length; i++) { + const e = this.examples[i] + for (let j = e.length; e < new_size; j++) { + e.push(0.0) } - } } - this.examples.push(observation); - this.labels.push(classification); + } + this.examples.push(observation) + this.labels.push(classification) } -function train() { - this.trainRandomForest(this.examples, this.labels, this.options); +function train () { + this.trainRandomForest(this.examples, this.labels, this.options) } -function getClassifications(observation) { - if(observation.length != this.example_size) { - throw 'Observation should be of length ' + this.example_size; - } - var prob = this.predictOne(observation); - if(prob > 0.5) - return [ { value: prob, - label: 1 }, - { value: (1-prob), - label: -1 } ]; - else - return [ { value: (1-prob), - label: -1 }, - { value: prob, - label: 1 } ]; +function getClassifications (observation) { + if (observation.length != this.example_size) { + throw 'Observation should be of length ' + this.example_size + } + const prob = this.predictOne(observation) + if (prob > 0.5) { + return [{ + value: prob, + label: 1 + }, + { + value: (1 - prob), + label: -1 + }] + } else { + return [{ + value: (1 - prob), + label: -1 + }, + { + value: prob, + label: 1 + }] + } } -RandomForestClassifier.prototype.train = train; -RandomForestClassifier.prototype.trainRandomForest = trainRandomForest; -RandomForestClassifier.prototype.predictOne = predictOne; -RandomForestClassifier.prototype.restore = restore; -RandomForestClassifier.prototype.addExample = addExample; -RandomForestClassifier.prototype.getClassifications = getClassifications; +RandomForestClassifier.prototype.train = train +RandomForestClassifier.prototype.trainRandomForest = trainRandomForest +RandomForestClassifier.prototype.predictOne = predictOne +RandomForestClassifier.prototype.restore = restore +RandomForestClassifier.prototype.addExample = addExample +RandomForestClassifier.prototype.getClassifications = getClassifications -DecisionTree.prototype.train = trainDT; -DecisionTree.prototype.predictOne = predictOneDT; +DecisionTree.prototype.train = trainDT +DecisionTree.prototype.predictOne = predictOneDT -RandomForestClassifier.DecisionTree = DecisionTree; -RandomForestClassifier.decisionStumpTrain = decisionStumpTrain; -RandomForestClassifier.decisionStumpTest = decisionStumpTest; -RandomForestClassifier.decision2DStumpTrain = decision2DStumpTrain; -RandomForestClassifier.decision2DStumpTest = decision2DStumpTest; +RandomForestClassifier.DecisionTree = DecisionTree +RandomForestClassifier.decisionStumpTrain = decisionStumpTrain +RandomForestClassifier.decisionStumpTest = decisionStumpTest +RandomForestClassifier.decision2DStumpTrain = decision2DStumpTrain +RandomForestClassifier.decision2DStumpTest = decision2DStumpTest -module.exports = RandomForestClassifier; +module.exports = RandomForestClassifier diff --git a/lib/apparatus/clusterer/kmeans-legacy.js b/lib/apparatus/clusterer/kmeans-legacy.js index d862b2d..5099e18 100644 --- a/lib/apparatus/clusterer/kmeans-legacy.js +++ b/lib/apparatus/clusterer/kmeans-legacy.js @@ -20,99 +20,98 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var Sylvester = require('sylvester'), -Matrix = Sylvester.Matrix, -Vector = Sylvester.Vector; +const Sylvester = require('sylvester') +const Matrix = Sylvester.Matrix +const Vector = Sylvester.Vector -function KMeans(Observations) { - if(!Observations.elements) - Observations = $M(Observations); +function KMeans (Observations) { + if (!Observations.elements) { Observations = $M(Observations) } - this.Observations = Observations; + this.Observations = Observations } // create an initial centroid matrix with initial values between // 0 and the max of feature data X. -function createCentroids(k) { - var Centroid = []; - var maxes = this.Observations.maxColumns(); - //console.log(maxes); - - for(var i = 1; i <= k; i++) { - var centroid = []; - for(var j = 1; j <= this.Observations.cols(); j++) { - centroid.push(Math.random() * maxes.e(j)); - } - - Centroid.push(centroid); +function createCentroids (k) { + const Centroid = [] + const maxes = this.Observations.maxColumns() + // console.log(maxes); + + for (let i = 1; i <= k; i++) { + const centroid = [] + for (let j = 1; j <= this.Observations.cols(); j++) { + centroid.push(Math.random() * maxes.e(j)) } - //console.log(centroid) + Centroid.push(centroid) + } - return $M(Centroid); + // console.log(centroid) + + return $M(Centroid) } // get the euclidian distance between the feature data X and // a given centroid matrix C. -function distanceFrom(Centroids) { - var distances = []; - - for(var i = 1; i <= this.Observations.rows(); i++) { - var distance = []; +function distanceFrom (Centroids) { + const distances = [] - for(var j = 1; j <= Centroids.rows(); j++) { - distance.push(this.Observations.row(i).distanceFrom(Centroids.row(j))); - } + for (let i = 1; i <= this.Observations.rows(); i++) { + const distance = [] - distances.push(distance); + for (let j = 1; j <= Centroids.rows(); j++) { + distance.push(this.Observations.row(i).distanceFrom(Centroids.row(j))) } - return $M(distances); + distances.push(distance) + } + + return $M(distances) } // categorize the feature data X into k clusters. return a vector // containing the results. -function cluster(k) { - var Centroids = this.createCentroids(k); - var LastDistances = Matrix.Zero(this.Observations.rows(), this.Observations.cols()); - var Distances = this.distanceFrom(Centroids); - var Groups; - - while(!(LastDistances.eql(Distances))) { - Groups = Distances.minColumnIndexes(); - LastDistances = Distances; - - var newCentroids = []; - - for(var i = 1; i <= Centroids.rows(); i++) { - var centroid = []; - - for(var j = 1; j <= Centroids.cols(); j++) { - var sum = 0; - var count = 0; - - for(var l = 1; l <= this.Observations.rows(); l++) { - if(Groups.e(l) == i) { - count++; - sum += this.Observations.e(l, j); - } +function cluster (k) { + let Centroids = this.createCentroids(k) + let LastDistances = Matrix.Zero(this.Observations.rows(), this.Observations.cols()) + let Distances = this.distanceFrom(Centroids) + let Groups + + while (!(LastDistances.eql(Distances))) { + Groups = Distances.minColumnIndexes() + LastDistances = Distances + + const newCentroids = [] + + for (let i = 1; i <= Centroids.rows(); i++) { + const centroid = [] + + for (let j = 1; j <= Centroids.cols(); j++) { + let sum = 0 + let count = 0 + + for (let l = 1; l <= this.Observations.rows(); l++) { + if (Groups.e(l) == i) { + count++ + sum += this.Observations.e(l, j) + } } - centroid.push(sum / count); - } + centroid.push(sum / count) + } - newCentroids.push(centroid); + newCentroids.push(centroid) } - Centroids = $M(newCentroids); - Distances = this.distanceFrom(Centroids); - } + Centroids = $M(newCentroids) + Distances = this.distanceFrom(Centroids) + } - return Groups; + return Groups } -KMeans.prototype.createCentroids = createCentroids; -KMeans.prototype.distanceFrom = distanceFrom; -KMeans.prototype.cluster = cluster; +KMeans.prototype.createCentroids = createCentroids +KMeans.prototype.distanceFrom = distanceFrom +KMeans.prototype.cluster = cluster -module.exports = KMeans; +module.exports = KMeans diff --git a/lib/apparatus/clusterer/kmeans-modernized.js b/lib/apparatus/clusterer/kmeans-modernized.js index c871c35..f5e5f7a 100644 --- a/lib/apparatus/clusterer/kmeans-modernized.js +++ b/lib/apparatus/clusterer/kmeans-modernized.js @@ -85,7 +85,7 @@ function calculateMean (vectors) { /** * K-Means clustering algorithm (Modernized version) - * + * * This is the advanced implementation with k-means++, multiple restarts, * and configurable options. For the original Apparatus API, use KMeans * from kmeans.js instead. diff --git a/lib/apparatus/clusterer/kmeans.js b/lib/apparatus/clusterer/kmeans.js index e597011..e571691 100644 --- a/lib/apparatus/clusterer/kmeans.js +++ b/lib/apparatus/clusterer/kmeans.js @@ -41,7 +41,7 @@ const { euclideanDistance, createRng } = KMeansModernized /** * KMeans - Original Apparatus API wrapper - * + * * Extends KMeansModernized to provide backward-compatible API: * new KMeans(observations) * kmeans.cluster(k) @@ -56,19 +56,19 @@ class KMeans extends KMeansModernized { constructor (observations) { // Call parent with placeholder k (will be set in cluster()) super(1) - + if (!Array.isArray(observations)) { throw new Error('KMeans expects an array of observations') } - + if (observations.length === 0) { throw new Error('Observations cannot be empty') } - + if (!Array.isArray(observations[0])) { throw new Error('Observations must be an array of arrays') } - + this.Observations = observations } @@ -81,15 +81,15 @@ class KMeans extends KMeansModernized { if (!isFinite(k) || k < 1) { throw new Error('k must be a positive integer') } - + if (k > this.Observations.length) { throw new Error(`k (${k}) cannot be greater than number of observations (${this.Observations.length})`) } - + // Set k and fit using parent's fit method this.k = k this.fit(this.Observations) - + // Return assignments wrapped in Sylvester-like Vector for backwards compatibility // VectorLike handles 0->1 index conversion lazily return new VectorLike(this.getAssignments()) @@ -104,7 +104,7 @@ class KMeans extends KMeansModernized { if (!isFinite(k) || k < 1) { throw new Error('k must be a positive integer') } - + const rng = createRng() return this.initializeRandomCentroids(this.Observations, rng) } @@ -118,23 +118,23 @@ class KMeans extends KMeansModernized { if (!Array.isArray(centroids)) { throw new Error('centroids must be an array') } - + if (centroids.length === 0) { throw new Error('centroids cannot be empty') } - + const distances = [] - + for (let i = 0; i < this.Observations.length; i++) { const distRow = [] - + for (let j = 0; j < centroids.length; j++) { distRow.push(euclideanDistance(this.Observations[i], centroids[j])) } - + distances.push(distRow) } - + return distances } } diff --git a/lib/apparatus/clusterer/vector-like.js b/lib/apparatus/clusterer/vector-like.js index 8c3ae8d..e648c4f 100644 --- a/lib/apparatus/clusterer/vector-like.js +++ b/lib/apparatus/clusterer/vector-like.js @@ -36,7 +36,7 @@ class VectorLike { // Store 0-indexed array internally for efficiency this._array = zeroIndexedArray } - + /** * Get element at 1-based index (Sylvester compatibility) * Converts from 0-indexed to 1-indexed on the fly @@ -46,7 +46,7 @@ class VectorLike { e (i) { return this._array[i - 1] + 1 } - + /** * Getter for elements array * Converts from 0-indexed to 1-indexed for Sylvester compatibility diff --git a/lib/apparatus/index.js b/lib/apparatus/index.js index a301bf2..51f122b 100644 --- a/lib/apparatus/index.js +++ b/lib/apparatus/index.js @@ -1,5 +1,4 @@ - -exports.BayesClassifier = require('./classifier/bayes_classifier'); -exports.LogisticRegressionClassifier = require('./classifier/logistic_regression_classifier'); -exports.RandomForestClassifier = require('./classifier/randomforest_classifier'); -exports.KMeans = require('./clusterer/kmeans'); \ No newline at end of file +exports.BayesClassifier = require('./classifier/bayes_classifier') +exports.LogisticRegressionClassifier = require('./classifier/logistic_regression_classifier') +exports.RandomForestClassifier = require('./classifier/randomforest_classifier') +exports.KMeans = require('./clusterer/kmeans') diff --git a/package-lock.json b/package-lock.json index e12144b..8111fad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,162 @@ "sylvester": ">= 0.0.8" }, "devDependencies": { - "jasmine": "^6.0.0" + "jasmine": "^6.0.0", + "standard": "^17.1.0" }, "engines": { "node": ">=0.2.6" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -43,6 +193,44 @@ "dev": true, "license": "MIT" }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -54,6 +242,67 @@ "node": ">=14" } }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -80,173 +329,2527 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "MIT" + "license": "Python-2.0" }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { - "node": ">=7.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" }, "engines": { - "node": ">= 8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "engines": { + "node": ">= 0.4" } }, - "node_modules/jasmine": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-6.0.0.tgz", - "integrity": "sha512-eSPL6LPWT39WwvHSEEbRXuSvioXMTheNhIPaeUT1OPmSprDZwj4S29884DkTx6/tyiOWTWB1N+LdW2ZSg74aEA==", + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "license": "MIT", "dependencies": { - "@jasminejs/reporters": "^1.0.0", - "glob": "^10.2.2 || ^11.0.3 || ^12.0.0 || ^13.0.0", - "jasmine-core": "~6.0.0" + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, - "bin": { - "jasmine": "bin/jasmine.js" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jasmine-core": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-6.0.1.tgz", - "integrity": "sha512-gUtzV5ASR0MLBwDNqri4kBsgKNCcRQd9qOlNw/w/deavD0cl3JmWXXfH8JhKM4LTg6LPTt2IOQ4px3YYfgh2Xg==", + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 0.4" + } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, - "license": "ISC" - }, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/builtins": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/builtins/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-standard": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", + "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-promise": "^6.0.0" + } + }, + "node_modules/eslint-config-standard-jsx": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-11.0.0.tgz", + "integrity": "sha512-+1EV/R0JxEK1L0NGolAr8Iktm3Rgotx3BKwgaX+eAuSX8D952LULKtjgZD3F+e6SvibONnhLwoTi9DPxN5LvvQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peerDependencies": { + "eslint": "^8.8.0", + "eslint-plugin-react": "^7.28.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-n": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz", + "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtins": "^5.0.1", + "eslint-plugin-es": "^4.1.0", + "eslint-utils": "^3.0.0", + "ignore": "^5.1.1", + "is-core-module": "^2.11.0", + "minimatch": "^3.1.2", + "resolve": "^1.22.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-n/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-n/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-promise": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", + "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-6.0.0.tgz", + "integrity": "sha512-eSPL6LPWT39WwvHSEEbRXuSvioXMTheNhIPaeUT1OPmSprDZwj4S29884DkTx6/tyiOWTWB1N+LdW2ZSg74aEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jasminejs/reporters": "^1.0.0", + "glob": "^10.2.2 || ^11.0.3 || ^12.0.0 || ^13.0.0", + "jasmine-core": "~6.0.0" + }, + "bin": { + "jasmine": "bin/jasmine.js" + } + }, + "node_modules/jasmine-core": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-6.0.1.tgz", + "integrity": "sha512-gUtzV5ASR0MLBwDNqri4kBsgKNCcRQd9qOlNw/w/deavD0cl3JmWXXfH8JhKM4LTg6LPTt2IOQ4px3YYfgh2Xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/load-json-file": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz", + "integrity": "sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.15", + "parse-json": "^4.0.0", + "pify": "^4.0.1", + "strip-bom": "^3.0.0", + "type-fest": "^0.3.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/load-json-file/node_modules/type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -254,80 +2857,924 @@ "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-3.1.0.tgz", + "integrity": "sha512-m0OTbR/5VPNPqO1ph6Fqbj7Hv6QU7gR/tQW40ZqrL1rjgCU85W6C1bJn0BItuJqnR98PWzw7Z8hHeChD1WrgdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0", + "load-json-file": "^5.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-conf/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-conf/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "*" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, - "license": "BlueOak-1.0.0" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/signal-exit": { @@ -343,6 +3790,88 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/standard": { + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/standard/-/standard-17.1.2.tgz", + "integrity": "sha512-WLm12WoXveKkvnPnPnaFUUHuOB2cUdAsJ4AiGHL2G0UNMrcRAWY2WriQaV8IQ3oRmYr0AWUbLNr94ekYFAHOrA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "eslint": "^8.41.0", + "eslint-config-standard": "17.1.0", + "eslint-config-standard-jsx": "^11.0.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-n": "^15.7.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-react": "^7.36.1", + "standard-engine": "^15.1.0", + "version-guard": "^1.1.1" + }, + "bin": { + "standard": "bin/cmd.cjs" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/standard-engine": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-15.1.0.tgz", + "integrity": "sha512-VHysfoyxFu/ukT+9v49d4BRXIokFRZuH3z1VRxzFArZdjSCFpro6rEIU3ji7e4AoAtuSfKBkiOmsrDqKW5ZSRw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "get-stdin": "^8.0.0", + "minimist": "^1.2.6", + "pkg-conf": "^3.1.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -407,6 +3936,104 @@ "node": ">=8" } }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", @@ -447,6 +4074,55 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/sylvester": { "version": "0.0.21", "resolved": "https://registry.npmjs.org/sylvester/-/sylvester-0.0.21.tgz", @@ -455,6 +4131,169 @@ "node": ">=0.2.6" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/version-guard": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/version-guard/-/version-guard-1.1.3.tgz", + "integrity": "sha512-JwPr6erhX53EWH/HCSzfy1tTFrtPXUe927wdM1jqBBeYp1OM+qPHjWbsvv6pIBduqdgxxS+ScfG7S28pzyr2DQ==", + "dev": true, + "license": "0BSD", + "engines": { + "node": ">=0.10.48" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -471,6 +4310,105 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -568,6 +4506,36 @@ "engines": { "node": ">=8" } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 9954d33..9f3df01 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,14 @@ "sylvester": ">= 0.0.8" }, "devDependencies": { - "jasmine": "^6.0.0" + "jasmine": "^6.0.0", + "standard": "^17.1.0" }, "scripts": { "test": "jasmine", - "benchmark": "node benchmark/kmeans_benchmark.js && node benchmark/logistic_regression_benchmark.js" + "benchmark": "node benchmark/kmeans_benchmark.js && node benchmark/logistic_regression_benchmark.js", + "lint": "standard", + "lint:fix": "standard --fix" }, "author": "Chris Umbel ", "keywords": [ diff --git a/spec/bayes_classifier_spec.js b/spec/bayes_classifier_spec.js index 215c74e..2b74a67 100644 --- a/spec/bayes_classifier_spec.js +++ b/spec/bayes_classifier_spec.js @@ -20,102 +20,101 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var BayesClassifier = new require('../lib/apparatus/classifier/bayes_classifier'); - -describe('bayes', function() { - it('should throw if not trained', function() { - var bayes = new BayesClassifier(); - expect(function() { bayes.classify([0,0,0,0,1,1]) }).toThrow(); - }); - - it('should perform binary classifcation', function() { - var bayes = new BayesClassifier(); - bayes.addExample([1,1,1,0,0,0], 'one'); - bayes.addExample([1,0,1,0,0,0], 'one'); - bayes.addExample([1,1,1,0,0,0], 'one'); - bayes.addExample([0,0,0,1,1,1], 'two'); - bayes.addExample([0,0,0,1,0,1], 'two'); - bayes.addExample([0,0,0,1,1,0], 'two'); - - bayes.train(); - - expect(bayes.classify([1,1,0,0,0,0])).toBe('one'); - expect(bayes.classify([0,0,0,0,1,1])).toBe('two'); - }); - - it('should classify', function() { - var bayes = new BayesClassifier(); - bayes.addExample([1,1,1,0,0,0,0,0,0], 'one'); - bayes.addExample([1,0,1,0,0,0,0,0,0], 'one'); - bayes.addExample([1,1,1,0,0,0,0,0,0], 'one'); - bayes.addExample([0,0,0,1,1,1,0,0,0], 'two'); - bayes.addExample([0,0,0,1,0,1,0,0,0], 'two'); - bayes.addExample([0,0,0,1,1,0,0,0,0], 'two'); - bayes.addExample([0,0,0,0,0,0,1,1,1], 'three'); - bayes.addExample([0,0,0,0,0,0,1,0,1], 'three'); - bayes.addExample([0,0,0,0,0,0,1,1,0], 'three'); - - bayes.train(); - - expect(bayes.classify([1,1,0,0,0,0,1,0,0])).toBe('one'); - expect(bayes.classify([0,0,1,1,1,0,0,0,1])).toBe('two'); - expect(bayes.classify([1,0,0,0,1,0,0,1,1])).toBe('three'); - - }); - - it('should classify with deserialized classifier', function() { - var bayes = new BayesClassifier(); - bayes.addExample([1,1,1,0,0,0,0,0,0], 'one'); - bayes.addExample([1,0,1,0,0,0,0,0,0], 'one'); - bayes.addExample([1,1,1,0,0,0,0,0,0], 'one'); - bayes.addExample([0,0,0,1,1,1,0,0,0], 'two'); - bayes.addExample([0,0,0,1,0,1,0,0,0], 'two'); - bayes.addExample([0,0,0,1,1,0,0,0,0], 'two'); - bayes.addExample([0,0,0,0,0,0,1,1,1], 'three'); - bayes.addExample([0,0,0,0,0,0,1,0,1], 'three'); - bayes.addExample([0,0,0,0,0,0,1,1,0], 'three'); - - bayes.train(); - - var obj = JSON.stringify(bayes); - var newBayes = BayesClassifier.restore(JSON.parse(obj)); - - expect(newBayes.classify([1,1,0,0,0,0,1,0,0])).toBe('one'); - expect(newBayes.classify([0,0,1,1,1,0,0,0,1])).toBe('two'); - expect(newBayes.classify([1,0,0,0,1,0,0,1,1])).toBe('three'); - }); - - it('should classify with smoothing', function() { - var bayes = new BayesClassifier(0.3); - bayes.addExample([1,1,1,0,0,0,0,0,0], 'one'); - bayes.addExample([0,0,1,0,0,0,0,0,0], 'one'); - bayes.addExample([0,0,1,0,0,0,0,0,0], 'one'); - bayes.addExample([0,0,0,1,1,1,0,0,0], 'two'); - bayes.addExample([0,0,0,0,0,1,0,0,0], 'two'); - bayes.addExample([0,0,0,0,1,0,0,0,0], 'two'); - bayes.addExample([0,0,0,0,0,0,1,1,1], 'three'); - bayes.addExample([0,0,0,0,0,0,0,0,1], 'three'); - bayes.addExample([0,0,0,0,0,0,0,1,0], 'three'); - - bayes.train(); - - expect(bayes.classify([1,0,0,0,0,0,1,0,0])).toBe('one'); - expect(bayes.classify([0,0,1,1,1,0,0,0,1])).toBe('two'); - expect(bayes.classify([1,0,0,0,1,0,0,1,1])).toBe('three'); - }); - - it('should classify with sparse observations', function() { - var bayes = new BayesClassifier(); - bayes.addExample({'a': 1, 'b': 'a', 'c': false}, 'one'); - bayes.addExample({'a': 1, 'b': 'b', 'c': false}, 'one'); - bayes.addExample({'a': 4, 'b': 'c', 'c': true}, 'one'); - bayes.addExample({'a': 2, 'b': 'c', 'c': false}, 'two'); - bayes.addExample({'a': 2, 'b': 'd', 'c': false}, 'two'); - bayes.addExample({'a': 2, 'b': 'e'}, 'two'); - - bayes.train(); - - expect(bayes.classify({'a': 1, 'f': 'e', 'c': true})).toBe('one'); - expect(bayes.classify({'a': 2, 'f': 'r'})).toBe('two'); - }); -}); +const BayesClassifier = new require('../lib/apparatus/classifier/bayes_classifier') + +describe('bayes', function () { + it('should throw if not trained', function () { + const bayes = new BayesClassifier() + expect(function () { bayes.classify([0, 0, 0, 0, 1, 1]) }).toThrow() + }) + + it('should perform binary classifcation', function () { + const bayes = new BayesClassifier() + bayes.addExample([1, 1, 1, 0, 0, 0], 'one') + bayes.addExample([1, 0, 1, 0, 0, 0], 'one') + bayes.addExample([1, 1, 1, 0, 0, 0], 'one') + bayes.addExample([0, 0, 0, 1, 1, 1], 'two') + bayes.addExample([0, 0, 0, 1, 0, 1], 'two') + bayes.addExample([0, 0, 0, 1, 1, 0], 'two') + + bayes.train() + + expect(bayes.classify([1, 1, 0, 0, 0, 0])).toBe('one') + expect(bayes.classify([0, 0, 0, 0, 1, 1])).toBe('two') + }) + + it('should classify', function () { + const bayes = new BayesClassifier() + bayes.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([1, 0, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([0, 0, 0, 1, 1, 1, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 1, 0, 1, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 1, 1, 0, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 0, 0, 0, 1, 1, 1], 'three') + bayes.addExample([0, 0, 0, 0, 0, 0, 1, 0, 1], 'three') + bayes.addExample([0, 0, 0, 0, 0, 0, 1, 1, 0], 'three') + + bayes.train() + + expect(bayes.classify([1, 1, 0, 0, 0, 0, 1, 0, 0])).toBe('one') + expect(bayes.classify([0, 0, 1, 1, 1, 0, 0, 0, 1])).toBe('two') + expect(bayes.classify([1, 0, 0, 0, 1, 0, 0, 1, 1])).toBe('three') + }) + + it('should classify with deserialized classifier', function () { + const bayes = new BayesClassifier() + bayes.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([1, 0, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([0, 0, 0, 1, 1, 1, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 1, 0, 1, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 1, 1, 0, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 0, 0, 0, 1, 1, 1], 'three') + bayes.addExample([0, 0, 0, 0, 0, 0, 1, 0, 1], 'three') + bayes.addExample([0, 0, 0, 0, 0, 0, 1, 1, 0], 'three') + + bayes.train() + + const obj = JSON.stringify(bayes) + const newBayes = BayesClassifier.restore(JSON.parse(obj)) + + expect(newBayes.classify([1, 1, 0, 0, 0, 0, 1, 0, 0])).toBe('one') + expect(newBayes.classify([0, 0, 1, 1, 1, 0, 0, 0, 1])).toBe('two') + expect(newBayes.classify([1, 0, 0, 0, 1, 0, 0, 1, 1])).toBe('three') + }) + + it('should classify with smoothing', function () { + const bayes = new BayesClassifier(0.3) + bayes.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([0, 0, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([0, 0, 1, 0, 0, 0, 0, 0, 0], 'one') + bayes.addExample([0, 0, 0, 1, 1, 1, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 0, 0, 1, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 0, 1, 0, 0, 0, 0], 'two') + bayes.addExample([0, 0, 0, 0, 0, 0, 1, 1, 1], 'three') + bayes.addExample([0, 0, 0, 0, 0, 0, 0, 0, 1], 'three') + bayes.addExample([0, 0, 0, 0, 0, 0, 0, 1, 0], 'three') + + bayes.train() + + expect(bayes.classify([1, 0, 0, 0, 0, 0, 1, 0, 0])).toBe('one') + expect(bayes.classify([0, 0, 1, 1, 1, 0, 0, 0, 1])).toBe('two') + expect(bayes.classify([1, 0, 0, 0, 1, 0, 0, 1, 1])).toBe('three') + }) + + it('should classify with sparse observations', function () { + const bayes = new BayesClassifier() + bayes.addExample({ a: 1, b: 'a', c: false }, 'one') + bayes.addExample({ a: 1, b: 'b', c: false }, 'one') + bayes.addExample({ a: 4, b: 'c', c: true }, 'one') + bayes.addExample({ a: 2, b: 'c', c: false }, 'two') + bayes.addExample({ a: 2, b: 'd', c: false }, 'two') + bayes.addExample({ a: 2, b: 'e' }, 'two') + + bayes.train() + + expect(bayes.classify({ a: 1, f: 'e', c: true })).toBe('one') + expect(bayes.classify({ a: 2, f: 'r' })).toBe('two') + }) +}) diff --git a/spec/kmeans_comparison_spec.js b/spec/kmeans_comparison_spec.js index 0561608..048a3f8 100644 --- a/spec/kmeans_comparison_spec.js +++ b/spec/kmeans_comparison_spec.js @@ -87,7 +87,7 @@ describe('KMeans vs KMeans-Legacy Comparison', () => { // Point 0: [1, 2] const cluster0 = assignments[0] const cluster1 = assignments[1] // [1.5, 1.8] - + // These two close points should be in the same cluster expect(cluster0).toBe(cluster1) }) diff --git a/spec/kmeans_spec.js b/spec/kmeans_spec.js index e1a3aa8..df794fb 100644 --- a/spec/kmeans_spec.js +++ b/spec/kmeans_spec.js @@ -20,70 +20,70 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var KMeans = require('.././lib/apparatus/clusterer/kmeans'); - -describe('kmeans', function() { - it('should return cluster assignments for all observations', function() { - var observations = [ - [1, 1], - [2, 2], - [10, 10], - [11, 11] - ]; - - var kmeans = new KMeans(observations); - var clusters = kmeans.cluster(2); - - // Verify we got cluster assignments for all points - expect(clusters.elements.length).toBe(4); - - // All cluster assignments should be valid numbers (1 or 2) - for (var i = 1; i <= 4; i++) { - expect(clusters.e(i)).toBeGreaterThan(0); - expect(clusters.e(i)).toBeLessThanOrEqual(2); - } - }); - - it('should handle single cluster', function() { - var observations = [ - [1, 1], - [1.5, 1.5], - [2, 2] - ]; - - var kmeans = new KMeans(observations); - var clusters = kmeans.cluster(1); - - // All points should be in the same cluster - expect(clusters.e(1)).toBe(1); - expect(clusters.e(2)).toBe(1); - expect(clusters.e(3)).toBe(1); - }); - - it('should cluster nearby points together', function() { - var observations = [ - [0, 0], - [0.1, 0.1], - [0.2, 0.2], - [100, 100], - [100.1, 100.1], - [100.2, 100.2] - ]; - - var kmeans = new KMeans(observations); - var clusters = kmeans.cluster(2); - - // Points 1-3 should be in the same cluster (they're very close) - var cluster1 = clusters.e(1); - expect(clusters.e(2)).toBe(cluster1); - expect(clusters.e(3)).toBe(cluster1); - - // Points 4-6 should be in the same cluster (they're very close) - var cluster2 = clusters.e(4); - expect(clusters.e(5)).toBe(cluster2); - expect(clusters.e(6)).toBe(cluster2); - - // The two clusters should be different - expect(cluster1).not.toBe(cluster2); - }); -}); +const KMeans = require('.././lib/apparatus/clusterer/kmeans') + +describe('kmeans', function () { + it('should return cluster assignments for all observations', function () { + const observations = [ + [1, 1], + [2, 2], + [10, 10], + [11, 11] + ] + + const kmeans = new KMeans(observations) + const clusters = kmeans.cluster(2) + + // Verify we got cluster assignments for all points + expect(clusters.elements.length).toBe(4) + + // All cluster assignments should be valid numbers (1 or 2) + for (let i = 1; i <= 4; i++) { + expect(clusters.e(i)).toBeGreaterThan(0) + expect(clusters.e(i)).toBeLessThanOrEqual(2) + } + }) + + it('should handle single cluster', function () { + const observations = [ + [1, 1], + [1.5, 1.5], + [2, 2] + ] + + const kmeans = new KMeans(observations) + const clusters = kmeans.cluster(1) + + // All points should be in the same cluster + expect(clusters.e(1)).toBe(1) + expect(clusters.e(2)).toBe(1) + expect(clusters.e(3)).toBe(1) + }) + + it('should cluster nearby points together', function () { + const observations = [ + [0, 0], + [0.1, 0.1], + [0.2, 0.2], + [100, 100], + [100.1, 100.1], + [100.2, 100.2] + ] + + const kmeans = new KMeans(observations) + const clusters = kmeans.cluster(2) + + // Points 1-3 should be in the same cluster (they're very close) + const cluster1 = clusters.e(1) + expect(clusters.e(2)).toBe(cluster1) + expect(clusters.e(3)).toBe(cluster1) + + // Points 4-6 should be in the same cluster (they're very close) + const cluster2 = clusters.e(4) + expect(clusters.e(5)).toBe(cluster2) + expect(clusters.e(6)).toBe(cluster2) + + // The two clusters should be different + expect(cluster1).not.toBe(cluster2) + }) +}) diff --git a/spec/logistic_regression_classifier_spec.js b/spec/logistic_regression_classifier_spec.js index 50ed6b0..9af87e0 100644 --- a/spec/logistic_regression_classifier_spec.js +++ b/spec/logistic_regression_classifier_spec.js @@ -20,99 +20,98 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var LogisticRegressionClassifier = new require('../lib/apparatus/classifier/logistic_regression_classifier'); - -describe('logistic regression', function() { - it('should classify with examples added in groups', function() { - var logistic = new LogisticRegressionClassifier(); - logistic.addExample([1,1,1,0,0,0], 'one'); - logistic.addExample([1,0,1,0,0,0], 'one'); - logistic.addExample([1,1,1,0,0,0], 'one'); - logistic.addExample([0,0,0,1,1,1], 'two'); - logistic.addExample([0,0,0,1,0,1], 'two'); - logistic.addExample([0,0,0,1,1,0], 'two'); - - logistic.train(); - - expect(logistic.classify([0,1,1,0,0,0])).toBe('one'); - expect(logistic.classify([0,0,0,0,1,1])).toBe('two'); - }); - - it('should classify - part 1', function() { - var logistic = new LogisticRegressionClassifier(); - logistic.addExample([1,1,1,0,0,0,0,0,0], 'one'); - logistic.addExample([1,0,1,0,0,0,0,0,0], 'one'); - logistic.addExample([1,1,1,0,0,0,0,0,0], 'one'); - logistic.addExample([0,0,0,1,1,1,0,0,0], 'two'); - logistic.addExample([0,0,0,1,0,1,0,0,0], 'two'); - logistic.addExample([0,0,0,1,1,0,0,0,0], 'two'); - logistic.addExample([0,0,0,0,0,0,1,1,1], 'three'); - logistic.addExample([0,0,0,0,0,0,1,0,1], 'three'); - logistic.addExample([0,0,0,0,0,0,1,1,0], 'three'); - - logistic.train(); - - expect(logistic.classify([1,1,0,0,0,0,1,0,0])).toBe('one'); - expect(logistic.classify([0,0,1,1,1,0,0,0,1])).toBe('two'); - expect(logistic.classify([1,0,0,0,1,0,0,1,1])).toBe('three'); - - }); - - it('should allow retraining', function() { - var logistic = new LogisticRegressionClassifier(); - logistic.addExample([1,1,1,0,0,0,0,0,0], 'one'); - logistic.addExample([1,0,1,0,0,0,0,0,0], 'one'); - logistic.addExample([1,1,1,0,0,0,0,0,0], 'one'); - logistic.addExample([0,0,0,1,1,1,0,0,0], 'two'); - logistic.addExample([0,0,0,1,0,1,0,0,0], 'two'); - logistic.addExample([0,0,0,1,1,0,0,0,0], 'two'); - logistic.train(); - logistic.addExample([0,0,0,0,0,0,1,1,1], 'three'); - logistic.addExample([0,0,0,0,0,0,1,0,1], 'three'); - logistic.addExample([0,0,0,0,0,0,1,1,0], 'three'); - logistic.train(); - - expect(logistic.classify([1,1,0,0,0,0,1,0,0])).toBe('one'); - expect(logistic.classify([0,0,1,1,1,0,0,0,1])).toBe('two'); - expect(logistic.classify([1,0,0,0,1,0,0,1,1])).toBe('three'); - }); - - it('should classify part - part 2', function() { - var logistic = new LogisticRegressionClassifier(); - logistic.addExample([1,1,1,0,0,0,0,0,0], 'one'); - logistic.addExample([1,0,1,0,0,0,0,0,0], 'one'); - logistic.addExample([1,1,1,0,0,0,0,0,0], 'one'); - logistic.addExample([0,0,0,1,1,1,0,0,0], 'two'); - logistic.addExample([0,0,0,1,0,1,0,0,0], 'two'); - logistic.addExample([0,0,0,1,1,0,0,0,0], 'two'); - logistic.addExample([0,0,0,0,0,0,1,1,1], 'three'); - logistic.addExample([0,0,0,0,0,0,1,0,1], 'three'); - logistic.addExample([0,0,0,0,0,0,1,1,0], 'three'); - - logistic.train(); - - var obj = JSON.stringify(logistic); - var newLogistic = LogisticRegressionClassifier.restore(JSON.parse(obj)); - - expect(newLogistic.classify([1,1,0,0,0,0,1,0,0])).toBe('one'); - expect(newLogistic.classify([0,0,1,1,1,0,0,0,1])).toBe('two'); - expect(newLogistic.classify([1,0,0,0,1,0,0,1,1])).toBe('three'); - }); - - it('should not run into an infinite loop (1)', function () { - var logistic = new LogisticRegressionClassifier(); - logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one'); - logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'two'); - - logistic.train(); - }); - - it('should not run into an infinite loop (2)', function () { - var logistic = new LogisticRegressionClassifier(); - logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one'); - logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'two'); - logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'three'); - - logistic.train(); - }); -}); +const LogisticRegressionClassifier = new require('../lib/apparatus/classifier/logistic_regression_classifier') + +describe('logistic regression', function () { + it('should classify with examples added in groups', function () { + const logistic = new LogisticRegressionClassifier() + logistic.addExample([1, 1, 1, 0, 0, 0], 'one') + logistic.addExample([1, 0, 1, 0, 0, 0], 'one') + logistic.addExample([1, 1, 1, 0, 0, 0], 'one') + logistic.addExample([0, 0, 0, 1, 1, 1], 'two') + logistic.addExample([0, 0, 0, 1, 0, 1], 'two') + logistic.addExample([0, 0, 0, 1, 1, 0], 'two') + + logistic.train() + + expect(logistic.classify([0, 1, 1, 0, 0, 0])).toBe('one') + expect(logistic.classify([0, 0, 0, 0, 1, 1])).toBe('two') + }) + + it('should classify - part 1', function () { + const logistic = new LogisticRegressionClassifier() + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 0, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([0, 0, 0, 1, 1, 1, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 1, 0, 1, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 1, 1, 0, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 1, 1], 'three') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 0, 1], 'three') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 1, 0], 'three') + + logistic.train() + + expect(logistic.classify([1, 1, 0, 0, 0, 0, 1, 0, 0])).toBe('one') + expect(logistic.classify([0, 0, 1, 1, 1, 0, 0, 0, 1])).toBe('two') + expect(logistic.classify([1, 0, 0, 0, 1, 0, 0, 1, 1])).toBe('three') + }) + + it('should allow retraining', function () { + const logistic = new LogisticRegressionClassifier() + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 0, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([0, 0, 0, 1, 1, 1, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 1, 0, 1, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 1, 1, 0, 0, 0, 0], 'two') + logistic.train() + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 1, 1], 'three') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 0, 1], 'three') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 1, 0], 'three') + logistic.train() + + expect(logistic.classify([1, 1, 0, 0, 0, 0, 1, 0, 0])).toBe('one') + expect(logistic.classify([0, 0, 1, 1, 1, 0, 0, 0, 1])).toBe('two') + expect(logistic.classify([1, 0, 0, 0, 1, 0, 0, 1, 1])).toBe('three') + }) + + it('should classify part - part 2', function () { + const logistic = new LogisticRegressionClassifier() + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 0, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([0, 0, 0, 1, 1, 1, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 1, 0, 1, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 1, 1, 0, 0, 0, 0], 'two') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 1, 1], 'three') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 0, 1], 'three') + logistic.addExample([0, 0, 0, 0, 0, 0, 1, 1, 0], 'three') + + logistic.train() + + const obj = JSON.stringify(logistic) + const newLogistic = LogisticRegressionClassifier.restore(JSON.parse(obj)) + + expect(newLogistic.classify([1, 1, 0, 0, 0, 0, 1, 0, 0])).toBe('one') + expect(newLogistic.classify([0, 0, 1, 1, 1, 0, 0, 0, 1])).toBe('two') + expect(newLogistic.classify([1, 0, 0, 0, 1, 0, 0, 1, 1])).toBe('three') + }) + + it('should not run into an infinite loop (1)', function () { + const logistic = new LogisticRegressionClassifier() + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'two') + + logistic.train() + }) + + it('should not run into an infinite loop (2)', function () { + const logistic = new LogisticRegressionClassifier() + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'one') + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'two') + logistic.addExample([1, 1, 1, 0, 0, 0, 0, 0, 0], 'three') + + logistic.train() + }) +}) diff --git a/spec/logistic_regression_comparison_spec.js b/spec/logistic_regression_comparison_spec.js index 1f3ecc9..35e97ea 100644 --- a/spec/logistic_regression_comparison_spec.js +++ b/spec/logistic_regression_comparison_spec.js @@ -150,7 +150,7 @@ describe('LogisticRegressionClassifier vs Legacy Comparison', () => { // Test on a training example const positiveExample = trainingData[0].text - + const modernResult = modernClassifier.getClassifications(positiveExample) const legacyResult = legacyClassifier.getClassifications(positiveExample) diff --git a/spec/randomforest_classifier_spec.js b/spec/randomforest_classifier_spec.js index 9ca7125..67b99d0 100644 --- a/spec/randomforest_classifier_spec.js +++ b/spec/randomforest_classifier_spec.js @@ -21,35 +21,35 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var RandomForestClassifier = new require('../lib/apparatus/classifier/randomforest_classifier'); - -describe('randomforest', function() { - it('should perform binary classifcation', function() { - var randomforest = new RandomForestClassifier(); - - randomforest.addExample([-0.4326, 1.1909 ], 1); - randomforest.addExample([1.5 , 3.0 ], 1); - randomforest.addExample([0.1253 , -0.0376 ], 1); - randomforest.addExample([0.2877 , 0.3273 ], 1); - randomforest.addExample([-1.1465, 0.1746 ], 1); - randomforest.addExample([1.8133 , 2.1139 ], -1); - randomforest.addExample([2.7258 , 3.0668 ], -1); - randomforest.addExample([1.4117 , 2.0593 ], -1); - randomforest.addExample([4.1832 , 1.9044 ], -1); - randomforest.addExample([1.8636 , 1.1677 ], -1); - - randomforest.train(); - - expect(randomforest.classify([-0.5 , -0.5 ])).toBe(1); - - // random forest are not deterministic, check on average it works - var count = 0; - for(var tests=0; tests<200; tests++){ - randomforest.train(); - if(randomforest.classify([1.0, 2.0]) == 1) { - count++; - } - } - expect(count).toBeGreaterThan(50); - }); - }); +const RandomForestClassifier = new require('../lib/apparatus/classifier/randomforest_classifier') + +describe('randomforest', function () { + it('should perform binary classifcation', function () { + const randomforest = new RandomForestClassifier() + + randomforest.addExample([-0.4326, 1.1909], 1) + randomforest.addExample([1.5, 3.0], 1) + randomforest.addExample([0.1253, -0.0376], 1) + randomforest.addExample([0.2877, 0.3273], 1) + randomforest.addExample([-1.1465, 0.1746], 1) + randomforest.addExample([1.8133, 2.1139], -1) + randomforest.addExample([2.7258, 3.0668], -1) + randomforest.addExample([1.4117, 2.0593], -1) + randomforest.addExample([4.1832, 1.9044], -1) + randomforest.addExample([1.8636, 1.1677], -1) + + randomforest.train() + + expect(randomforest.classify([-0.5, -0.5])).toBe(1) + + // random forest are not deterministic, check on average it works + let count = 0 + for (let tests = 0; tests < 200; tests++) { + randomforest.train() + if (randomforest.classify([1.0, 2.0]) == 1) { + count++ + } + } + expect(count).toBeGreaterThan(50) + }) +}) From 458053402ce89178bdfed608481d881029c2080b Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Tue, 3 Mar 2026 18:12:18 +0100 Subject: [PATCH 15/19] add lint job --- .github/workflows/ci.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e02547f..3795440 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,24 @@ on: branches: [ main, master, develop ] jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + test: runs-on: ubuntu-latest From f42987be9a10d6e7a95aeb93b0683a34eabe3c69 Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Tue, 3 Mar 2026 18:26:45 +0100 Subject: [PATCH 16/19] Standard js --- lib/apparatus/classifier/bayes_classifier.js | 8 +- lib/apparatus/classifier/classifier.js | 6 +- .../classifier/logistic-regression-legacy.js | 18 ++-- .../logistic-regression-modernized.js | 66 ++----------- .../classifier/randomforest_classifier.js | 92 ++++++++----------- lib/apparatus/clusterer/kmeans-legacy.js | 11 +-- spec/bayes_classifier_spec.js | 8 +- spec/kmeans_comparison_spec.js | 2 + spec/kmeans_spec.js | 2 +- spec/logistic_regression_classifier_spec.js | 4 +- spec/logistic_regression_comparison_spec.js | 2 + spec/randomforest_classifier_spec.js | 16 ++-- 12 files changed, 91 insertions(+), 144 deletions(-) diff --git a/lib/apparatus/classifier/bayes_classifier.js b/lib/apparatus/classifier/bayes_classifier.js index d98f80c..40aaead 100644 --- a/lib/apparatus/classifier/bayes_classifier.js +++ b/lib/apparatus/classifier/bayes_classifier.js @@ -57,7 +57,7 @@ function addExample (observation, label) { } else { // sparse observation for (const key in observation) { - value = observation[key] + const value = observation[key] if (this.classFeatures[label][value]) { this.classFeatures[label][value]++ @@ -81,7 +81,7 @@ function probabilityOfClass (observation, label) { while (i--) { if (observation[i]) { - var count = this.classFeatures[label][i] || this.smoothing + const count = this.classFeatures[label][i] || this.smoothing // numbers are tiny, add logs rather than take product prob += Math.log(count / this.classTotals[label]) } @@ -89,7 +89,7 @@ function probabilityOfClass (observation, label) { } else { // sparse observation for (const key in observation) { - var count = this.classFeatures[label][observation[key]] || this.smoothing + const count = this.classFeatures[label][observation[key]] || this.smoothing // numbers are tiny, add logs rather than take product prob += Math.log(count / this.classTotals[label]) } @@ -119,7 +119,7 @@ function getClassifications (observation) { function restore (classifier) { classifier = Classifier.restore(classifier) - classifier.__proto__ = BayesClassifier.prototype + Object.setPrototypeOf(classifier, BayesClassifier.prototype) return classifier } diff --git a/lib/apparatus/classifier/classifier.js b/lib/apparatus/classifier/classifier.js index 7e4bd83..661fa6b 100644 --- a/lib/apparatus/classifier/classifier.js +++ b/lib/apparatus/classifier/classifier.js @@ -30,19 +30,19 @@ function restore (classifier) { } function addExample (observation, classification) { - throw 'Not implemented' + throw new Error('Not implemented') } function classify (observation) { const classifications = this.getClassifications(observation) if (!classifications || classifications.length === 0) { - throw 'Not Trained' + throw new Error('Not Trained') } return classifications[0].label } function train () { - throw 'Not implemented' + throw new Error('Not implemented') } Classifier.prototype.addExample = addExample diff --git a/lib/apparatus/classifier/logistic-regression-legacy.js b/lib/apparatus/classifier/logistic-regression-legacy.js index f1c6b34..8bce728 100644 --- a/lib/apparatus/classifier/logistic-regression-legacy.js +++ b/lib/apparatus/classifier/logistic-regression-legacy.js @@ -39,10 +39,10 @@ function cost (theta, Examples, classifications) { const hypothesisResult = hypothesis(theta, Examples) const ones = Vector.One(Examples.rows()) - const cost_1 = Vector.Zero(Examples.rows()).subtract(classifications).elementMultiply(hypothesisResult.log()) - const cost_0 = ones.subtract(classifications).elementMultiply(ones.subtract(hypothesisResult).log()) + const cost1 = Vector.Zero(Examples.rows()).subtract(classifications).elementMultiply(hypothesisResult.log()) + const cost0 = ones.subtract(classifications).elementMultiply(ones.subtract(hypothesisResult).log()) - return (1 / Examples.rows()) * cost_1.subtract(cost_0).sum() + return (1 / Examples.rows()) * cost1.subtract(cost0).sum() } function descendGradient (theta, Examples, classifications) { @@ -74,7 +74,7 @@ function descendGradient (theta, Examples, classifications) { } if (i >= maxIt) { - throw 'unable to find minimum' + throw new Error('unable to find minimum') } last = current @@ -104,9 +104,9 @@ function createClassifications () { for (let i = 0; i < this.exampleCount; i++) { const classification = [] - for (const _ in this.examples) { + Object.keys(this.examples).forEach(() => { classification.push(0) - } + }) classifications.push(classification) } @@ -143,7 +143,7 @@ function train () { c++ } - this.computeThetas($M(examples), $M(classifications)) + this.computeThetas(Matrix.create(examples), Matrix.create(classifications)) } function addExample (data, classification) { @@ -157,7 +157,7 @@ function addExample (data, classification) { } function getClassifications (observation) { - observation = $V(observation) + observation = Vector.create(observation) const classifications = [] for (let i = 0; i < this.theta.length; i++) { @@ -171,7 +171,7 @@ function getClassifications (observation) { function restore (classifier) { classifier = Classifier.restore(classifier) - classifier.__proto__ = LogisticRegressionClassifier.prototype + Object.setPrototypeOf(classifier, LogisticRegressionClassifier.prototype) return classifier } diff --git a/lib/apparatus/classifier/logistic-regression-modernized.js b/lib/apparatus/classifier/logistic-regression-modernized.js index dc3dd73..9eb9bb4 100644 --- a/lib/apparatus/classifier/logistic-regression-modernized.js +++ b/lib/apparatus/classifier/logistic-regression-modernized.js @@ -20,7 +20,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const util = require('util') const Classifier = require('./classifier') /** @@ -76,31 +75,6 @@ function matrixTranspose (matrix) { return result } -/** - * Matrix multiplication - * a: array of arrays (m x n) - * b: array of arrays (n x p) - * returns: array of arrays (m x p) - */ -function matrixMultiply (a, b) { - const m = a.length - const n = a[0].length - const p = b[0].length - const result = Array(m) - - for (let i = 0; i < m; i++) { - result[i] = new Array(p) - for (let j = 0; j < p; j++) { - let sum = 0 - for (let k = 0; k < n; k++) { - sum += a[i][k] * b[k][j] - } - result[i][j] = sum - } - } - return result -} - /** * Element-wise operations */ @@ -158,17 +132,6 @@ function scalarMultiply (arr, scalar) { return result } -/** - * Matrix scalar multiply - */ -function matrixScalarMultiply (matrix, scalar) { - const result = new Array(matrix.length) - for (let i = 0; i < matrix.length; i++) { - result[i] = scalarMultiply(matrix[i], scalar) - } - return result -} - /** * Augment matrix with ones column */ @@ -180,17 +143,6 @@ function augmentWithOnes (matrix) { return result } -/** - * Remove first element from each column (inverse of augment) - */ -function removeAugmentation (matrix) { - const result = new Array(matrix.length) - for (let i = 0; i < matrix.length; i++) { - result[i] = matrix[i].slice(1) - } - return result -} - /** * Cost function */ @@ -201,22 +153,22 @@ function cost (theta, Examples, classifications) { const numExamples = Examples.length const ones = Array(numExamples).fill(1) - // cost_1 = (-classifications) .* log(sigmoidResult) + // cost1 = (-classifications) .* log(sigmoidResult) const negClassifications = elementWiseMultiply(classifications, Array(numExamples).fill(-1)) - const cost_1 = elementWiseMultiply( + const cost1 = elementWiseMultiply( negClassifications, elementWiseLog(sigmoidResult) ) - // cost_0 = (1 - classifications) .* log(1 - sigmoidResult) + // cost0 = (1 - classifications) .* log(1 - sigmoidResult) const oneMinusHypothesis = elementWiseSubtract(ones, sigmoidResult) const oneMinusClassifications = elementWiseSubtract(ones, classifications) - const cost_0 = elementWiseMultiply( + const cost0 = elementWiseMultiply( oneMinusClassifications, elementWiseLog(oneMinusHypothesis) ) - const totalCost = sum(elementWiseSubtract(cost_1, cost_0)) + const totalCost = sum(elementWiseSubtract(cost1, cost0)) return totalCost / numExamples } @@ -310,9 +262,9 @@ class LogisticRegressionModernized extends Classifier { for (let i = 0; i < this.exampleCount; i++) { const classification = [] - for (const _ in this.examples) { + Object.keys(this.examples).forEach(() => { classification.push(0) - } + }) classifications.push(classification) } @@ -401,12 +353,10 @@ class LogisticRegressionModernized extends Classifier { */ static restore (classifier) { classifier = Classifier.restore(classifier) - classifier.__proto__ = LogisticRegressionModernized.prototype + Object.setPrototypeOf(classifier, LogisticRegressionModernized.prototype) return classifier } } -LogisticRegressionModernized.restore = LogisticRegressionModernized.restore - module.exports = LogisticRegressionModernized diff --git a/lib/apparatus/classifier/randomforest_classifier.js b/lib/apparatus/classifier/randomforest_classifier.js index c49ccd9..5ac0e6f 100644 --- a/lib/apparatus/classifier/randomforest_classifier.js +++ b/lib/apparatus/classifier/randomforest_classifier.js @@ -72,20 +72,8 @@ function predictOne (inst) { return dec } -/* - convenience function. Here, data is NxD array. - returns probabilities of being 1 for all data in an array. -*/ -function predict (data) { - const probabilities = new Array(data.length) - for (let i = 0; i < data.length; i++) { - probabilities[i] = this.predictOne(data[i]) - } - return probabilities -} - // represents a single decision tree -var DecisionTree = function (options) { +const DecisionTree = function (options) { } function trainDT (data, labels, options) { @@ -99,11 +87,11 @@ function trainDT (data, labels, options) { if (options.trainFun) trainFun = options.trainFun if (options.testFun) testFun = options.testFun - if (weakType == 0) { + if (weakType === 0) { trainFun = decisionStumpTrain testFun = decisionStumpTest } - if (weakType == 1) { + if (weakType === 1) { trainFun = decision2DStumpTrain testFun = decision2DStumpTest } @@ -112,17 +100,17 @@ function trainDT (data, labels, options) { const numInternals = Math.pow(2, maxDepth) - 1 const numNodes = Math.pow(2, maxDepth + 1) - 1 const ixs = new Array(numNodes) - for (var i = 1; i < ixs.length; i++) ixs[i] = [] + for (let i = 1; i < ixs.length; i++) ixs[i] = [] ixs[0] = new Array(labels.length) - for (var i = 0; i < labels.length; i++) ixs[0][i] = i // root node starts out with all nodes as relevant + for (let i = 0; i < labels.length; i++) ixs[0][i] = i // root node starts out with all nodes as relevant const models = new Array(numInternals) // train - for (var n = 0; n < numInternals; n++) { + for (let n = 0; n < numInternals; n++) { // few base cases const ixhere = ixs[n] - if (ixhere.length == 0) { continue } - if (ixhere.length == 1) { ixs[n * 2 + 1] = [ixhere[0]]; continue } // arbitrary send it down left + if (ixhere.length === 0) { continue } + if (ixhere.length === 1) { ixs[n * 2 + 1] = [ixhere[0]]; continue } // arbitrary send it down left // learn a weak model on relevant data for this node const model = trainFun(data, labels, ixhere) @@ -131,7 +119,7 @@ function trainDT (data, labels, options) { // split the data according to the learned model const ixleft = [] const ixright = [] - for (var i = 0; i < ixhere.length; i++) { + for (let i = 0; i < ixhere.length; i++) { const label = testFun(data[ixhere[i]], model) if (label === 1) ixleft.push(ixhere[i]) else ixright.push(ixhere[i]) @@ -143,9 +131,9 @@ function trainDT (data, labels, options) { // compute data distributions at the leafs const leafPositives = new Array(numNodes) const leafNegatives = new Array(numNodes) - for (var n = numInternals; n < numNodes; n++) { + for (let n = numInternals; n < numNodes; n++) { let numones = 0 - for (var i = 0; i < ixs[n].length; i++) { + for (let i = 0; i < ixs[n].length; i++) { if (labels[ixs[n][i]] === 1) numones += 1 } leafPositives[n] = numones @@ -190,7 +178,7 @@ function decisionStumpTrain (data, labels, ix, options) { // pick a random splitting threshold const ix1 = ix[randi(0, N)] let ix2 = ix[randi(0, N)] - while (ix2 == ix1) ix2 = ix[randi(0, N)] // enforce distinctness of ix2 + while (ix2 === ix1) ix2 = ix[randi(0, N)] // enforce distinctness of ix2 const a = Math.random() const thr = data[ix1][ri] * a + data[ix2][ri] * (1 - a) @@ -199,10 +187,10 @@ function decisionStumpTrain (data, labels, ix, options) { let l1 = 1; let r1 = 1; let lm1 = 1; let rm1 = 1 // counts for Left and label 1, right and label 1, left and minus 1, right and minus 1 for (let j = 0; j < ix.length; j++) { if (data[ix[j]][ri] < thr) { - if (labels[ix[j]] == 1) l1++ + if (labels[ix[j]] === 1) l1++ else lm1++ } else { - if (labels[ix[j]] == 1) r1++ + if (labels[ix[j]] === 1) r1++ else rm1++ } } @@ -224,7 +212,7 @@ function decisionStumpTrain (data, labels, ix, options) { } } - model = {} + const model = {} model.thr = bestThr model.ri = ri return model @@ -253,7 +241,7 @@ function decision2DStumpTrain (data, labels, ix, options) { // more than 2D data. Pick 2 random dimensions ri1 = randi(0, data[0].length) ri2 = randi(0, data[0].length) - while (ri2 == ri1) ri2 = randi(0, data[0].length) // must be distinct! + while (ri2 === ri1) ri2 = randi(0, data[0].length) // must be distinct! } // evaluate class entropy of incoming data @@ -268,7 +256,7 @@ function decision2DStumpTrain (data, labels, ix, options) { const w2 = Math.sin(alpha) // project data on this line and get the dot products - for (var j = 0; j < ix.length; j++) { + for (let j = 0; j < ix.length; j++) { dots[j] = w1 * data[ix[j]][ri1] + w2 * data[ix[j]][ri2] } @@ -280,18 +268,18 @@ function decision2DStumpTrain (data, labels, ix, options) { // be in the high-desnity regions, so the thresholds will make sense const ix1 = ix[randi(0, N)] let ix2 = ix[randi(0, N)] - while (ix2 == ix1) ix2 = ix[randi(0, N)] // enforce distinctness of ix2 + while (ix2 === ix1) ix2 = ix[randi(0, N)] // enforce distinctness of ix2 const a = Math.random() const dotthr = dots[ix1] * a + dots[ix2] * (1 - a) // measure information gain we'd get from split with thr let l1 = 1; let r1 = 1; let lm1 = 1; let rm1 = 1 // counts for Left and label 1, right and label 1, left and minus 1, right and minus 1 - for (var j = 0; j < ix.length; j++) { + for (let j = 0; j < ix.length; j++) { if (dots[j] < dotthr) { - if (labels[ix[j]] == 1) l1++ + if (labels[ix[j]] === 1) l1++ else lm1++ } else { - if (labels[ix[j]] == 1) r1++ + if (labels[ix[j]] === 1) r1++ else rm1++ } } @@ -315,7 +303,7 @@ function decision2DStumpTrain (data, labels, ix, options) { } } - model = {} + const model = {} model.w1 = bestw1 model.w2 = bestw2 model.dotthr = bestthr @@ -336,10 +324,10 @@ function entropy (labels, ix) { const N = ix.length let p = 0.0 for (let i = 0; i < N; i++) { - if (labels[ix[i]] == 1) p += 1 + if (labels[ix[i]] === 1) p += 1 } p = (1 + p) / (N + 2) // let's be bayesian about this - q = (1 + N - p) / (N + 2) + const q = (1 + N - p) / (N + 2) return (-p * Math.log(p) - q * Math.log(q)) } @@ -359,7 +347,7 @@ util.inherits(RandomForestClassifier, Classifier) function restore (classifier) { classifier = Classifier.restore(classifier) - classifier.__proto__ = RandomForestClassifier.prototype + Object.setPrototypeOf(classifier, RandomForestClassifier.prototype) // change prototypes recursively for the trees? @@ -367,18 +355,18 @@ function restore (classifier) { } function addExample (observation, classification) { - if (classification != -1 && classification != 1) { - throw 'Only classes 1 and -1 are currently supported' + if (classification !== -1 && classification !== 1) { + throw new Error('Only classes 1 and -1 are currently supported') } if (observation.length > this.example_size) { - const new_size = observation.length - this.example_size = new_size + const newSize = observation.length + this.example_size = newSize for (let i = 0; i < this.examples.length; i++) { - const e = this.examples[i] - for (let j = e.length; e < new_size; j++) { + const e = this.examples[i] + for (let j = e.length; j < newSize; j++) { e.push(0.0) - } + } } } this.examples.push(observation) @@ -390,27 +378,27 @@ function train () { } function getClassifications (observation) { - if (observation.length != this.example_size) { - throw 'Observation should be of length ' + this.example_size + if (observation.length !== this.example_size) { + throw new Error('Observation should be of length ' + this.example_size) } const prob = this.predictOne(observation) if (prob > 0.5) { return [{ value: prob, - label: 1 + label: 1 }, - { + { value: (1 - prob), - label: -1 + label: -1 }] } else { return [{ value: (1 - prob), - label: -1 + label: -1 }, - { + { value: prob, - label: 1 + label: 1 }] } } diff --git a/lib/apparatus/clusterer/kmeans-legacy.js b/lib/apparatus/clusterer/kmeans-legacy.js index 5099e18..3f40ca9 100644 --- a/lib/apparatus/clusterer/kmeans-legacy.js +++ b/lib/apparatus/clusterer/kmeans-legacy.js @@ -22,10 +22,9 @@ THE SOFTWARE. const Sylvester = require('sylvester') const Matrix = Sylvester.Matrix -const Vector = Sylvester.Vector function KMeans (Observations) { - if (!Observations.elements) { Observations = $M(Observations) } + if (!Observations.elements) { Observations = Matrix.create(Observations) } this.Observations = Observations } @@ -48,7 +47,7 @@ function createCentroids (k) { // console.log(centroid) - return $M(Centroid) + return Matrix.create(Centroid) } // get the euclidian distance between the feature data X and @@ -66,7 +65,7 @@ function distanceFrom (Centroids) { distances.push(distance) } - return $M(distances) + return Matrix.create(distances) } // categorize the feature data X into k clusters. return a vector @@ -91,7 +90,7 @@ function cluster (k) { let count = 0 for (let l = 1; l <= this.Observations.rows(); l++) { - if (Groups.e(l) == i) { + if (Groups.e(l) === i) { count++ sum += this.Observations.e(l, j) } @@ -103,7 +102,7 @@ function cluster (k) { newCentroids.push(centroid) } - Centroids = $M(newCentroids) + Centroids = Matrix.create(newCentroids) Distances = this.distanceFrom(Centroids) } diff --git a/spec/bayes_classifier_spec.js b/spec/bayes_classifier_spec.js index 2b74a67..bf1c5cd 100644 --- a/spec/bayes_classifier_spec.js +++ b/spec/bayes_classifier_spec.js @@ -20,7 +20,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const BayesClassifier = new require('../lib/apparatus/classifier/bayes_classifier') +/* global describe, it, expect */ + +const BayesClassifier = require('../lib/apparatus/classifier/bayes_classifier') describe('bayes', function () { it('should throw if not trained', function () { @@ -76,8 +78,8 @@ describe('bayes', function () { bayes.train() - const obj = JSON.stringify(bayes) - const newBayes = BayesClassifier.restore(JSON.parse(obj)) + const obj = JSON.stringify(bayes) + const newBayes = BayesClassifier.restore(JSON.parse(obj)) expect(newBayes.classify([1, 1, 0, 0, 0, 0, 1, 0, 0])).toBe('one') expect(newBayes.classify([0, 0, 1, 1, 1, 0, 0, 0, 1])).toBe('two') diff --git a/spec/kmeans_comparison_spec.js b/spec/kmeans_comparison_spec.js index 048a3f8..de51e18 100644 --- a/spec/kmeans_comparison_spec.js +++ b/spec/kmeans_comparison_spec.js @@ -3,6 +3,8 @@ Comparison tests between KMeans (modernized) and KMeans-Legacy (Sylvester) Verifies both implementations produce equivalent results */ +/* global describe, it, expect */ + 'use strict' const KMeans = require('../lib/apparatus/clusterer/kmeans') diff --git a/spec/kmeans_spec.js b/spec/kmeans_spec.js index df794fb..7cc9ab1 100644 --- a/spec/kmeans_spec.js +++ b/spec/kmeans_spec.js @@ -19,7 +19,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - +/* global describe, it, expect */ const KMeans = require('.././lib/apparatus/clusterer/kmeans') describe('kmeans', function () { diff --git a/spec/logistic_regression_classifier_spec.js b/spec/logistic_regression_classifier_spec.js index 9af87e0..0952884 100644 --- a/spec/logistic_regression_classifier_spec.js +++ b/spec/logistic_regression_classifier_spec.js @@ -20,7 +20,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const LogisticRegressionClassifier = new require('../lib/apparatus/classifier/logistic_regression_classifier') +/* global describe, it, expect */ + +const LogisticRegressionClassifier = require('../lib/apparatus/classifier/logistic-regression-modernized') describe('logistic regression', function () { it('should classify with examples added in groups', function () { diff --git a/spec/logistic_regression_comparison_spec.js b/spec/logistic_regression_comparison_spec.js index 35e97ea..7cc53cc 100644 --- a/spec/logistic_regression_comparison_spec.js +++ b/spec/logistic_regression_comparison_spec.js @@ -3,6 +3,8 @@ Comparison tests between LogisticRegressionClassifier (modernized) and LogisticR Verifies both implementations produce equivalent results */ +/* global describe, it, expect */ + 'use strict' const LogisticRegressionClassifier = require('../lib/apparatus/classifier/logistic_regression_classifier') diff --git a/spec/randomforest_classifier_spec.js b/spec/randomforest_classifier_spec.js index 67b99d0..6854d3d 100644 --- a/spec/randomforest_classifier_spec.js +++ b/spec/randomforest_classifier_spec.js @@ -21,10 +21,12 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const RandomForestClassifier = new require('../lib/apparatus/classifier/randomforest_classifier') +/* global describe, it, expect */ + +const RandomForestClassifier = require('../lib/apparatus/classifier/randomforest_classifier') describe('randomforest', function () { - it('should perform binary classifcation', function () { + it('should perform binary classifcation', function () { const randomforest = new RandomForestClassifier() randomforest.addExample([-0.4326, 1.1909], 1) @@ -45,11 +47,11 @@ describe('randomforest', function () { // random forest are not deterministic, check on average it works let count = 0 for (let tests = 0; tests < 200; tests++) { - randomforest.train() - if (randomforest.classify([1.0, 2.0]) == 1) { - count++ - } + randomforest.train() + if (randomforest.classify([1.0, 2.0]) === 1) { + count++ + } } expect(count).toBeGreaterThan(50) - }) + }) }) From 40420dd440feec04873076ef000c24669b2ba69a Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Tue, 3 Mar 2026 20:25:45 +0100 Subject: [PATCH 17/19] Modernised the legacy versions into class-based syntax --- lib/apparatus/classifier/bayes_classifier.js | 154 +++++------ lib/apparatus/classifier/classifier.js | 41 ++- .../classifier/logistic-regression-legacy.js | 138 +++++----- .../classifier/randomforest_classifier.js | 257 +++++++++--------- 4 files changed, 278 insertions(+), 312 deletions(-) diff --git a/lib/apparatus/classifier/bayes_classifier.js b/lib/apparatus/classifier/bayes_classifier.js index 40aaead..f45b380 100644 --- a/lib/apparatus/classifier/bayes_classifier.js +++ b/lib/apparatus/classifier/bayes_classifier.js @@ -20,115 +20,107 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const util = require('util') const Classifier = require('./classifier') -const BayesClassifier = function (smoothing) { - Classifier.call(this) - this.classFeatures = {} - this.classTotals = {} - this.totalExamples = 1 // start at one to smooth - this.smoothing = smoothing === undefined ? 1.0 : smoothing -} - -util.inherits(BayesClassifier, Classifier) - -function addExample (observation, label) { - if (!this.classFeatures[label]) { - this.classFeatures[label] = {} - this.classTotals[label] = 1 // give an extra for smoothing +class BayesClassifier extends Classifier { + constructor (smoothing) { + super() + this.classFeatures = {} + this.classTotals = {} + this.totalExamples = 1 // start at one to smooth + this.smoothing = smoothing === undefined ? 1.0 : smoothing } - if (observation instanceof Array) { - let i = observation.length - this.totalExamples++ - this.classTotals[label]++ + addExample (observation, label) { + if (!this.classFeatures[label]) { + this.classFeatures[label] = {} + this.classTotals[label] = 1 // give an extra for smoothing + } + + if (observation instanceof Array) { + let i = observation.length + this.totalExamples++ + this.classTotals[label]++ + + while (i--) { + if (observation[i]) { + if (this.classFeatures[label][i]) { + this.classFeatures[label][i]++ + } else { + // give an extra for smoothing + this.classFeatures[label][i] = 1 + this.smoothing + } + } + } + } else { + // sparse observation + for (const key in observation) { + const value = observation[key] - while (i--) { - if (observation[i]) { - if (this.classFeatures[label][i]) { - this.classFeatures[label][i]++ + if (this.classFeatures[label][value]) { + this.classFeatures[label][value]++ } else { // give an extra for smoothing - this.classFeatures[label][i] = 1 + this.smoothing + this.classFeatures[label][value] = 1 + this.smoothing } } } - } else { - // sparse observation - for (const key in observation) { - const value = observation[key] - - if (this.classFeatures[label][value]) { - this.classFeatures[label][value]++ - } else { - // give an extra for smoothing - this.classFeatures[label][value] = 1 + this.smoothing - } - } } -} -function train () { + train () { -} + } -function probabilityOfClass (observation, label) { - let prob = 0 + probabilityOfClass (observation, label) { + let prob = 0 - if (observation instanceof Array) { - let i = observation.length + if (observation instanceof Array) { + let i = observation.length - while (i--) { - if (observation[i]) { - const count = this.classFeatures[label][i] || this.smoothing + while (i--) { + if (observation[i]) { + const count = this.classFeatures[label][i] || this.smoothing + // numbers are tiny, add logs rather than take product + prob += Math.log(count / this.classTotals[label]) + } + } + } else { + // sparse observation + for (const key in observation) { + const count = this.classFeatures[label][observation[key]] || this.smoothing // numbers are tiny, add logs rather than take product prob += Math.log(count / this.classTotals[label]) } } - } else { - // sparse observation - for (const key in observation) { - const count = this.classFeatures[label][observation[key]] || this.smoothing - // numbers are tiny, add logs rather than take product - prob += Math.log(count / this.classTotals[label]) - } - } - // p(C) * unlogging the above calculation P(X|C) - prob = (this.classTotals[label] / this.totalExamples) * Math.exp(prob) + // p(C) * unlogging the above calculation P(X|C) + prob = (this.classTotals[label] / this.totalExamples) * Math.exp(prob) - return prob -} + return prob + } + + getClassifications (observation) { + const classifier = this + const labels = [] -function getClassifications (observation) { - const classifier = this - const labels = [] + for (const className in this.classFeatures) { + labels.push({ + label: className, + value: classifier.probabilityOfClass(observation, className) + }) + } - for (const className in this.classFeatures) { - labels.push({ - label: className, - value: classifier.probabilityOfClass(observation, className) + return labels.sort(function (x, y) { + return y.value - x.value }) } - return labels.sort(function (x, y) { - return y.value - x.value - }) -} - -function restore (classifier) { - classifier = Classifier.restore(classifier) - Object.setPrototypeOf(classifier, BayesClassifier.prototype) + static restore (classifier) { + classifier = Classifier.restore(classifier) + Object.setPrototypeOf(classifier, BayesClassifier.prototype) - return classifier + return classifier + } } -BayesClassifier.prototype.addExample = addExample -BayesClassifier.prototype.train = train -BayesClassifier.prototype.getClassifications = getClassifications -BayesClassifier.prototype.probabilityOfClass = probabilityOfClass - -BayesClassifier.restore = restore - module.exports = BayesClassifier diff --git a/lib/apparatus/classifier/classifier.js b/lib/apparatus/classifier/classifier.js index 661fa6b..cefbd29 100644 --- a/lib/apparatus/classifier/classifier.js +++ b/lib/apparatus/classifier/classifier.js @@ -20,35 +20,28 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -function Classifier () { -} +class Classifier { + addExample (observation, classification) { + throw new Error('Not implemented') + } -function restore (classifier) { - classifier = typeof classifier === 'string' ? JSON.parse(classifier) : classifier + classify (observation) { + const classifications = this.getClassifications(observation) + if (!classifications || classifications.length === 0) { + throw new Error('Not Trained') + } + return classifications[0].label + } - return classifier -} + train () { + throw new Error('Not implemented') + } -function addExample (observation, classification) { - throw new Error('Not implemented') -} + static restore (classifier) { + classifier = typeof classifier === 'string' ? JSON.parse(classifier) : classifier -function classify (observation) { - const classifications = this.getClassifications(observation) - if (!classifications || classifications.length === 0) { - throw new Error('Not Trained') + return classifier } - return classifications[0].label -} - -function train () { - throw new Error('Not implemented') } -Classifier.prototype.addExample = addExample -Classifier.prototype.train = train -Classifier.prototype.classify = classify - -Classifier.restore = restore - module.exports = Classifier diff --git a/lib/apparatus/classifier/logistic-regression-legacy.js b/lib/apparatus/classifier/logistic-regression-legacy.js index 8bce728..4e9df25 100644 --- a/lib/apparatus/classifier/logistic-regression-legacy.js +++ b/lib/apparatus/classifier/logistic-regression-legacy.js @@ -20,7 +20,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const util = require('util') const Classifier = require('./classifier') const sylvester = require('sylvester') @@ -86,103 +85,94 @@ function descendGradient (theta, Examples, classifications) { return theta.chomp(1) } -const LogisticRegressionClassifier = function () { - Classifier.call(this) - this.examples = {} - this.features = [] - this.featurePositions = {} - this.maxFeaturePosition = 0 - this.classifications = [] - this.exampleCount = 0 -} +class LogisticRegressionClassifier extends Classifier { + constructor () { + super() + this.examples = {} + this.features = [] + this.featurePositions = {} + this.maxFeaturePosition = 0 + this.classifications = [] + this.exampleCount = 0 + } -util.inherits(LogisticRegressionClassifier, Classifier) + createClassifications () { + const classifications = [] -function createClassifications () { - const classifications = [] + for (let i = 0; i < this.exampleCount; i++) { + const classification = [] - for (let i = 0; i < this.exampleCount; i++) { - const classification = [] + Object.keys(this.examples).forEach(() => { + classification.push(0) + }) - Object.keys(this.examples).forEach(() => { - classification.push(0) - }) + classifications.push(classification) + } - classifications.push(classification) + return classifications } - return classifications -} - -function computeThetas (Examples, Classifications) { - this.theta = [] + computeThetas (Examples, Classifications) { + this.theta = [] - // each class will have it's own theta. - const zero = function () { return 0 } - for (let i = 1; i <= this.classifications.length; i++) { - const theta = Examples.row(1).map(zero) - this.theta.push(descendGradient(theta, Examples, Classifications.column(i))) + // each class will have it's own theta. + const zero = function () { return 0 } + for (let i = 1; i <= this.classifications.length; i++) { + const theta = Examples.row(1).map(zero) + this.theta.push(descendGradient(theta, Examples, Classifications.column(i))) + } } -} -function train () { - const examples = [] - const classifications = this.createClassifications() - let d = 0; let c = 0 + train () { + const examples = [] + const classifications = this.createClassifications() + let d = 0; let c = 0 - for (const classification in this.examples) { - for (let i = 0; i < this.examples[classification].length; i++) { - const doc = this.examples[classification][i] - const example = doc + for (const classification in this.examples) { + for (let i = 0; i < this.examples[classification].length; i++) { + const doc = this.examples[classification][i] + const example = doc + + examples.push(example) + classifications[d][c] = 1 + d++ + } - examples.push(example) - classifications[d][c] = 1 - d++ + c++ } - c++ + this.computeThetas(Matrix.create(examples), Matrix.create(classifications)) } - this.computeThetas(Matrix.create(examples), Matrix.create(classifications)) -} + addExample (data, classification) { + if (!this.examples[classification]) { + this.examples[classification] = [] + this.classifications.push(classification) + } -function addExample (data, classification) { - if (!this.examples[classification]) { - this.examples[classification] = [] - this.classifications.push(classification) + this.examples[classification].push(data) + this.exampleCount++ } - this.examples[classification].push(data) - this.exampleCount++ -} + getClassifications (observation) { + observation = Vector.create(observation) + const classifications = [] -function getClassifications (observation) { - observation = Vector.create(observation) - const classifications = [] + for (let i = 0; i < this.theta.length; i++) { + classifications.push({ label: this.classifications[i], value: sigmoid(observation.dot(this.theta[i])) }) + } - for (let i = 0; i < this.theta.length; i++) { - classifications.push({ label: this.classifications[i], value: sigmoid(observation.dot(this.theta[i])) }) + return classifications.sort(function (x, y) { + return y.value - x.value + }) } - return classifications.sort(function (x, y) { - return y.value - x.value - }) -} - -function restore (classifier) { - classifier = Classifier.restore(classifier) - Object.setPrototypeOf(classifier, LogisticRegressionClassifier.prototype) + static restore (classifier) { + classifier = Classifier.restore(classifier) + Object.setPrototypeOf(classifier, LogisticRegressionClassifier.prototype) - return classifier + return classifier + } } -LogisticRegressionClassifier.prototype.addExample = addExample -LogisticRegressionClassifier.prototype.restore = restore -LogisticRegressionClassifier.prototype.train = train -LogisticRegressionClassifier.prototype.createClassifications = createClassifications -LogisticRegressionClassifier.prototype.computeThetas = computeThetas -LogisticRegressionClassifier.prototype.getClassifications = getClassifications - -LogisticRegressionClassifier.restore = restore - module.exports = LogisticRegressionClassifier diff --git a/lib/apparatus/classifier/randomforest_classifier.js b/lib/apparatus/classifier/randomforest_classifier.js index 5ac0e6f..714a8a1 100644 --- a/lib/apparatus/classifier/randomforest_classifier.js +++ b/lib/apparatus/classifier/randomforest_classifier.js @@ -21,62 +21,123 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -const util = require('util') const Classifier = require('./classifier') -const RandomForestClassifier = function (options) { - Classifier.call(this) - this.options = options - this.examples = [] - this.example_size = 0 - this.labels = [] -} +class RandomForestClassifier extends Classifier { + constructor (options) { + super() + this.options = options + this.examples = [] + this.example_size = 0 + this.labels = [] + } -/* - data is 2D array of size N x D of examples - labels is a 1D array of labels (only -1 or 1 for now). In future will support multiclass or maybe even regression - options.numTrees can be used to customize number of trees to train (default = 100) - options.maxDepth is the maximum depth of each tree in the forest (default = 4) - options.numTries is the number of random hypotheses generated at each node during training (default = 10) - options.trainFun is a function with signature "function myWeakTrain(data, labels, ix, options)". Here, ix is a list of - indexes into data of the instances that should be payed attention to. Everything not in the list - should be ignored. This is done for efficiency. The function should return a model where you store - variables. (i.e. model = {}; model.myvar = 5;) This will be passed to testFun. - options.testFun is a function with signature "funtion myWeakTest(inst, model)" where inst is 1D array specifying an example, - and model will be the same model that you return in options.trainFun. For example, model.myvar will be 5. - see decisionStumpTrain() and decisionStumpTest() below for example. -*/ -function trainRandomForest (data, labels, options) { - options = options || {} - this.numTrees = options.numTrees || 100 + /* + data is 2D array of size N x D of examples + labels is a 1D array of labels (only -1 or 1 for now). In future will support multiclass or maybe even regression + options.numTrees can be used to customize number of trees to train (default = 100) + options.maxDepth is the maximum depth of each tree in the forest (default = 4) + options.numTries is the number of random hypotheses generated at each node during training (default = 10) + options.trainFun is a function with signature "function myWeakTrain(data, labels, ix, options)". Here, ix is a list of + indexes into data of the instances that should be payed attention to. Everything not in the list + should be ignored. This is done for efficiency. The function should return a model where you store + variables. (i.e. model = {}; model.myvar = 5;) This will be passed to testFun. + options.testFun is a function with signature "funtion myWeakTest(inst, model)" where inst is 1D array specifying an example, + and model will be the same model that you return in options.trainFun. For example, model.myvar will be 5. + see decisionStumpTrain() and decisionStumpTest() below for example. + */ + trainRandomForest (data, labels, options) { + options = options || {} + this.numTrees = options.numTrees || 100 + + // initialize many trees and train them all independently + this.trees = new Array(this.numTrees) + for (let i = 0; i < this.numTrees; i++) { + this.trees[i] = new DecisionTree() + this.trees[i].train(data, labels, options) + } + } - // initialize many trees and train them all independently - this.trees = new Array(this.numTrees) - for (let i = 0; i < this.numTrees; i++) { - this.trees[i] = new DecisionTree() - this.trees[i].train(data, labels, options) + /* + inst is a 1D array of length D of an example. + returns the probability of label 1, i.e. a number in range [0, 1] + */ + predictOne (inst) { + // have each tree predict and average out all votes + let dec = 0 + for (let i = 0; i < this.numTrees; i++) { + dec += this.trees[i].predictOne(inst) + } + dec /= this.numTrees + return dec } -} -/* - inst is a 1D array of length D of an example. - returns the probability of label 1, i.e. a number in range [0, 1] -*/ -function predictOne (inst) { - // have each tree predict and average out all votes - let dec = 0 - for (let i = 0; i < this.numTrees; i++) { - dec += this.trees[i].predictOne(inst) + addExample (observation, classification) { + if (classification !== -1 && classification !== 1) { + throw new Error('Only classes 1 and -1 are currently supported') + } + + if (observation.length > this.example_size) { + const newSize = observation.length + this.example_size = newSize + for (let i = 0; i < this.examples.length; i++) { + const e = this.examples[i] + for (let j = e.length; j < newSize; j++) { + e.push(0.0) + } + } + } + this.examples.push(observation) + this.labels.push(classification) + } + + train () { + this.trainRandomForest(this.examples, this.labels, this.options) + } + + getClassifications (observation) { + if (observation.length !== this.example_size) { + throw new Error('Observation should be of length ' + this.example_size) + } + const prob = this.predictOne(observation) + if (prob > 0.5) { + return [{ + value: prob, + label: 1 + }, + { + value: (1 - prob), + label: -1 + }] + } else { + return [{ + value: (1 - prob), + label: -1 + }, + { + value: prob, + label: 1 + }] + } + } + + static restore (classifier) { + classifier = Classifier.restore(classifier) + Object.setPrototypeOf(classifier, RandomForestClassifier.prototype) + + // change prototypes recursively for the trees? + + return classifier } - dec /= this.numTrees - return dec } // represents a single decision tree -const DecisionTree = function (options) { -} +class DecisionTree { + constructor (options) { + this.options = options + } -function trainDT (data, labels, options) { + train (data, labels, options) { options = options || {} const maxDepth = options.maxDepth || 4 const weakType = options.type || 0 @@ -141,24 +202,25 @@ function trainDT (data, labels, options) { } // back up important prediction variables for predicting later - this.models = models - this.leafPositives = leafPositives - this.leafNegatives = leafNegatives - this.maxDepth = maxDepth - this.trainFun = trainFun - this.testFun = testFun -} - -// returns probability that example inst is 1. -function predictOneDT (inst) { - let n = 0 - for (let i = 0; i < this.maxDepth; i++) { - const dir = this.testFun(inst, this.models[n]) - if (dir === 1) n = n * 2 + 1 // descend left - else n = n * 2 + 2 // descend right + this.models = models + this.leafPositives = leafPositives + this.leafNegatives = leafNegatives + this.maxDepth = maxDepth + this.trainFun = trainFun + this.testFun = testFun } - return (this.leafPositives[n] + 0.5) / (this.leafNegatives[n] + 1.0) // bayesian smoothing! + // returns probability that example inst is 1. + predictOne (inst) { + let n = 0 + for (let i = 0; i < this.maxDepth; i++) { + const dir = this.testFun(inst, this.models[n]) + if (dir === 1) n = n * 2 + 1 // descend left + else n = n * 2 + 2 // descend right + } + + return (this.leafPositives[n] + 0.5) / (this.leafNegatives[n] + 1.0) // bayesian smoothing! + } } // returns model @@ -341,78 +403,7 @@ function randi (a, b) { return Math.floor(Math.random() * (b - a) + a) } -// apparatus adapter - -util.inherits(RandomForestClassifier, Classifier) - -function restore (classifier) { - classifier = Classifier.restore(classifier) - Object.setPrototypeOf(classifier, RandomForestClassifier.prototype) - - // change prototypes recursively for the trees? - - return classifier -} - -function addExample (observation, classification) { - if (classification !== -1 && classification !== 1) { - throw new Error('Only classes 1 and -1 are currently supported') - } - - if (observation.length > this.example_size) { - const newSize = observation.length - this.example_size = newSize - for (let i = 0; i < this.examples.length; i++) { - const e = this.examples[i] - for (let j = e.length; j < newSize; j++) { - e.push(0.0) - } - } - } - this.examples.push(observation) - this.labels.push(classification) -} - -function train () { - this.trainRandomForest(this.examples, this.labels, this.options) -} - -function getClassifications (observation) { - if (observation.length !== this.example_size) { - throw new Error('Observation should be of length ' + this.example_size) - } - const prob = this.predictOne(observation) - if (prob > 0.5) { - return [{ - value: prob, - label: 1 - }, - { - value: (1 - prob), - label: -1 - }] - } else { - return [{ - value: (1 - prob), - label: -1 - }, - { - value: prob, - label: 1 - }] - } -} - -RandomForestClassifier.prototype.train = train -RandomForestClassifier.prototype.trainRandomForest = trainRandomForest -RandomForestClassifier.prototype.predictOne = predictOne -RandomForestClassifier.prototype.restore = restore -RandomForestClassifier.prototype.addExample = addExample -RandomForestClassifier.prototype.getClassifications = getClassifications - -DecisionTree.prototype.train = trainDT -DecisionTree.prototype.predictOne = predictOneDT - +// Static properties for compatibility RandomForestClassifier.DecisionTree = DecisionTree RandomForestClassifier.decisionStumpTrain = decisionStumpTrain RandomForestClassifier.decisionStumpTest = decisionStumpTest From 9de4293644d4c0636a6de6090f17504ac649cfd0 Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Tue, 3 Mar 2026 20:27:11 +0100 Subject: [PATCH 18/19] lint correction --- .../classifier/randomforest_classifier.js | 110 +++++++++--------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/lib/apparatus/classifier/randomforest_classifier.js b/lib/apparatus/classifier/randomforest_classifier.js index 714a8a1..7a67a76 100644 --- a/lib/apparatus/classifier/randomforest_classifier.js +++ b/lib/apparatus/classifier/randomforest_classifier.js @@ -138,70 +138,70 @@ class DecisionTree { } train (data, labels, options) { - options = options || {} - const maxDepth = options.maxDepth || 4 - const weakType = options.type || 0 + options = options || {} + const maxDepth = options.maxDepth || 4 + const weakType = options.type || 0 - let trainFun = decision2DStumpTrain - let testFun = decision2DStumpTest + let trainFun = decision2DStumpTrain + let testFun = decision2DStumpTest - if (options.trainFun) trainFun = options.trainFun - if (options.testFun) testFun = options.testFun + if (options.trainFun) trainFun = options.trainFun + if (options.testFun) testFun = options.testFun - if (weakType === 0) { - trainFun = decisionStumpTrain - testFun = decisionStumpTest - } - if (weakType === 1) { - trainFun = decision2DStumpTrain - testFun = decision2DStumpTest - } + if (weakType === 0) { + trainFun = decisionStumpTrain + testFun = decisionStumpTest + } + if (weakType === 1) { + trainFun = decision2DStumpTrain + testFun = decision2DStumpTest + } - // initialize various helper variables - const numInternals = Math.pow(2, maxDepth) - 1 - const numNodes = Math.pow(2, maxDepth + 1) - 1 - const ixs = new Array(numNodes) - for (let i = 1; i < ixs.length; i++) ixs[i] = [] - ixs[0] = new Array(labels.length) - for (let i = 0; i < labels.length; i++) ixs[0][i] = i // root node starts out with all nodes as relevant - const models = new Array(numInternals) - - // train - for (let n = 0; n < numInternals; n++) { + // initialize various helper variables + const numInternals = Math.pow(2, maxDepth) - 1 + const numNodes = Math.pow(2, maxDepth + 1) - 1 + const ixs = new Array(numNodes) + for (let i = 1; i < ixs.length; i++) ixs[i] = [] + ixs[0] = new Array(labels.length) + for (let i = 0; i < labels.length; i++) ixs[0][i] = i // root node starts out with all nodes as relevant + const models = new Array(numInternals) + + // train + for (let n = 0; n < numInternals; n++) { // few base cases - const ixhere = ixs[n] - if (ixhere.length === 0) { continue } - if (ixhere.length === 1) { ixs[n * 2 + 1] = [ixhere[0]]; continue } // arbitrary send it down left - - // learn a weak model on relevant data for this node - const model = trainFun(data, labels, ixhere) - models[n] = model // back it up model - - // split the data according to the learned model - const ixleft = [] - const ixright = [] - for (let i = 0; i < ixhere.length; i++) { - const label = testFun(data[ixhere[i]], model) - if (label === 1) ixleft.push(ixhere[i]) - else ixright.push(ixhere[i]) + const ixhere = ixs[n] + if (ixhere.length === 0) { continue } + if (ixhere.length === 1) { ixs[n * 2 + 1] = [ixhere[0]]; continue } // arbitrary send it down left + + // learn a weak model on relevant data for this node + const model = trainFun(data, labels, ixhere) + models[n] = model // back it up model + + // split the data according to the learned model + const ixleft = [] + const ixright = [] + for (let i = 0; i < ixhere.length; i++) { + const label = testFun(data[ixhere[i]], model) + if (label === 1) ixleft.push(ixhere[i]) + else ixright.push(ixhere[i]) + } + ixs[n * 2 + 1] = ixleft + ixs[n * 2 + 2] = ixright } - ixs[n * 2 + 1] = ixleft - ixs[n * 2 + 2] = ixright - } - // compute data distributions at the leafs - const leafPositives = new Array(numNodes) - const leafNegatives = new Array(numNodes) - for (let n = numInternals; n < numNodes; n++) { - let numones = 0 - for (let i = 0; i < ixs[n].length; i++) { - if (labels[ixs[n][i]] === 1) numones += 1 + // compute data distributions at the leafs + const leafPositives = new Array(numNodes) + const leafNegatives = new Array(numNodes) + for (let n = numInternals; n < numNodes; n++) { + let numones = 0 + for (let i = 0; i < ixs[n].length; i++) { + if (labels[ixs[n][i]] === 1) numones += 1 + } + leafPositives[n] = numones + leafNegatives[n] = ixs[n].length - numones } - leafPositives[n] = numones - leafNegatives[n] = ixs[n].length - numones - } - // back up important prediction variables for predicting later + // back up important prediction variables for predicting later this.models = models this.leafPositives = leafPositives this.leafNegatives = leafNegatives From 25308a5abb6dff9f5cfaa22b3b8175e11fd6c771 Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Wed, 4 Mar 2026 15:58:00 +0100 Subject: [PATCH 19/19] Added Typescript main index --- index.d.ts | 7 +++++++ package.json | 1 + 2 files changed, 8 insertions(+) create mode 100644 index.d.ts diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..e60cf03 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,7 @@ +export { BayesClassifier } from './lib/apparatus/classifier/bayes_classifier' +export { LogisticRegressionModernized as LogisticRegressionClassifier } from './lib/apparatus/classifier/logistic_regression_classifier' +export { RandomForestClassifier } from './lib/apparatus/classifier/randomforest_classifier' +export { KMeans } from './lib/apparatus/clusterer/kmeans' + +export type { Classification } from './lib/apparatus/classifier/classifier' +export type { RandomForestOptions } from './lib/apparatus/classifier/randomforest_classifier' diff --git a/package.json b/package.json index 9f3df01..b783f27 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "regression" ], "main": "./lib/apparatus/index.js", + "types": "./index.d.ts", "maintainers": [ { "name": "Chris Umbel",