diff --git a/.babelrc.json b/.babelrc.json deleted file mode 100644 index 34bc6d2..0000000 --- a/.babelrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "plugins": ["@babel/plugin-transform-modules-commonjs"] -} diff --git a/.eslintrc.yml b/.eslintrc.yml index f40ac5c..ff9f6e0 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,3 +1,3 @@ -extends: cheminfo +extends: cheminfo-typescript parserOptions: sourceType: module diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs-ts.yml similarity index 85% rename from .github/workflows/nodejs.yml rename to .github/workflows/nodejs-ts.yml index 2e2007c..71e0af2 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs-ts.yml @@ -10,3 +10,5 @@ jobs: nodejs: # Documentation: https://github.com/zakodium/workflows#nodejs-ci uses: zakodium/workflows/.github/workflows/nodejs.yml@nodejs-v1 + with: + lint-check-types: true diff --git a/.gitignore b/.gitignore index fec7c0b..0c53f7f 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ jspm_packages # Optional REPL history .node_repl_history -lib \ No newline at end of file +lib +lib-esm diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..c49b80d --- /dev/null +++ b/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + "presets": ["@babel/preset-typescript"], + "plugins": ["@babel/plugin-transform-modules-commonjs"] +} diff --git a/package.json b/package.json index d5cbb57..9e0d2fb 100644 --- a/package.json +++ b/package.json @@ -2,23 +2,27 @@ "name": "ml-tree-similarity", "version": "2.1.0", "description": "Compares two spectra using a tree similarity", - "main": "lib/index.js", - "module": "src/index.js", - "types": "tree-similarity.d.ts", + "main": "./lib/index.js", + "module": "./lib-esm/index.js", + "types": "./lib/index.d.ts", "files": [ "lib", "src", - "tree-similarity.d.ts" + "lib-esm" ], "scripts": { - "compile": "rollup -c", + "check-types": "tsc --noEmit", + "clean": "rimraf lib lib-esm", "eslint": "eslint src", "eslint-fix": "npm run eslint -- --fix", - "prepack": "npm run compile", + "prepack": "npm run tsc", "prettier": "prettier --check src", "prettier-write": "prettier --write src", - "test": "npm run test-only && npm run eslint && npm run prettier", - "test-only": "vitest run --coverage" + "test": "npm run test-only && npm run eslint && npm run prettier && npm run check-types", + "test-only": "vitest run --coverage", + "tsc": "npm run clean && npm run tsc-cjs && npm run tsc-esm", + "tsc-cjs": "tsc --project tsconfig.cjs.json", + "tsc-esm": "tsc --project tsconfig.esm.json" }, "repository": { "type": "git", @@ -33,11 +37,13 @@ "homepage": "https://github.com/mljs/tree-similarity#readme", "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/preset-typescript": "^7.23.3", "@vitest/coverage-v8": "^1.2.2", "eslint": "^8.56.0", - "eslint-config-cheminfo": "^9.1.1", + "eslint-config-cheminfo-typescript": "^12.2.0", "prettier": "^3.2.5", - "rollup": "^4.9.6", + "rimraf": "^5.0.5", + "typescript": "^5.3.3", "vitest": "^1.2.2" }, "dependencies": { diff --git a/rollup.config.mjs b/rollup.config.mjs deleted file mode 100644 index 7b0ccfe..0000000 --- a/rollup.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -export default { - input: 'src/index.js', - output: { - format: 'cjs', - file: 'lib/index.js' - }, - external: ['binary-search'] -}; diff --git a/src/__tests__/compressTree.test.js b/src/__tests__/compressTree.test.ts similarity index 68% rename from src/__tests__/compressTree.test.js rename to src/__tests__/compressTree.test.ts index 3dce62f..9923e90 100644 --- a/src/__tests__/compressTree.test.js +++ b/src/__tests__/compressTree.test.ts @@ -1,12 +1,12 @@ import { test, expect } from 'vitest'; import { compressTree } from '../compressTree'; -import { createTree } from '../createTree'; +import { Tree, createTree } from '../createTree'; test('compressTree', () => { - let x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - let y = [0, 1, 2, 3, 0, 0, 0, 1, 0, 0, 0]; - let tree = createTree({ x, y }); + const x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const y = [0, 1, 2, 3, 0, 0, 0, 1, 0, 0, 0]; + const tree = createTree({ x, y }) as Tree; expect(compressTree(tree, { fixed: 3 })).toStrictEqual({ sum: 7, diff --git a/src/__tests__/createTree.test.js b/src/__tests__/createTree.test.ts similarity index 63% rename from src/__tests__/createTree.test.js rename to src/__tests__/createTree.test.ts index 98e1a4c..93f641a 100644 --- a/src/__tests__/createTree.test.js +++ b/src/__tests__/createTree.test.ts @@ -1,23 +1,25 @@ import { describe, it, expect } from 'vitest'; -import { createTree } from '../createTree'; +import { Tree, createTree } from '../createTree'; describe('simple trees', () => { it('two peaks, same height', () => { - let x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - let y = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]; - let tree = createTree({ x, y }); + const x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const y = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]; + const tree = createTree({ x, y }) as Tree; expect(tree.center).toBe(5); expect(tree.sum).toBe(2); - let left = tree.left; + const left = tree.left as Tree; + expect(left).not.toBe(null); expect(left.center).toBe(3); expect(left.sum).toBe(1); expect(left.left).toStrictEqual(null); expect(left.right).toStrictEqual(null); - let right = tree.right; + const right = tree.right as Tree; + expect(right).not.toBe(null); expect(right.center).toBe(7); expect(right.sum).toBe(1); expect(right.left).toStrictEqual(null); @@ -25,8 +27,8 @@ describe('simple trees', () => { }); it('two peaks, same height (higher)', () => { - let x = new Array(101); - let y = new Array(101); + const x = new Array(101); + const y = new Array(101); for (let i = 0; i < 101; i++) { x[i] = i; y[i] = 0; @@ -34,18 +36,20 @@ describe('simple trees', () => { y[20] = 20; y[80] = 20; - let tree = createTree({ x, y }); + const tree = createTree({ x, y }) as Tree; expect(tree.center).toBe(50); expect(tree.sum).toBe(40); - let left = tree.left; + const left = tree.left as Tree; + expect(left).not.toBe(null); expect(left.center).toBe(20); expect(left.sum).toBe(20); expect(left.left).toStrictEqual(null); expect(left.right).toStrictEqual(null); - let right = tree.right; + const right = tree.right as Tree; + expect(right).not.toBe(null); expect(right.center).toBe(80); expect(right.sum).toBe(20); expect(right.left).toStrictEqual(null); diff --git a/src/__tests__/tree.test.js b/src/__tests__/tree.test.ts similarity index 63% rename from src/__tests__/tree.test.js rename to src/__tests__/tree.test.ts index b11337f..1b0b404 100644 --- a/src/__tests__/tree.test.js +++ b/src/__tests__/tree.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { createTree, treeSimilarity } from '../index'; +import { Tree, createTree, treeSimilarity } from '../index'; const a = { x: [1, 2, 3, 4, 5, 6, 7], @@ -18,4 +18,9 @@ describe('Tree similarity', () => { 4, ); }); + it('should throw with wrong input', () => { + expect(() => treeSimilarity(createTree(a), {} as Tree)).toThrow( + 'tree similarity expects tree as inputs', + ); + }); }); diff --git a/src/__tests__/treeSimilarity.test.js b/src/__tests__/treeSimilarity.test.ts similarity index 60% rename from src/__tests__/treeSimilarity.test.js rename to src/__tests__/treeSimilarity.test.ts index 620bc4c..65f0295 100644 --- a/src/__tests__/treeSimilarity.test.js +++ b/src/__tests__/treeSimilarity.test.ts @@ -5,10 +5,10 @@ import { treeSimilarity } from '../treeSimilarity'; describe('simple trees', () => { it('same tree', () => { - let x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - let y = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]; - let tree1 = createTree({ x, y }); - let tree2 = createTree({ x, y }); + const x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const y = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]; + const tree1 = createTree({ x, y }); + const tree2 = createTree({ x, y }); expect(treeSimilarity(tree1, tree2, { beta: 1 })).toBe(1); }); diff --git a/src/compressTree.js b/src/compressTree.ts similarity index 68% rename from src/compressTree.js rename to src/compressTree.ts index 076732f..115a7ba 100644 --- a/src/compressTree.js +++ b/src/compressTree.ts @@ -1,12 +1,15 @@ +import { Tree } from './createTree'; + /** * Destructive compression in which we reduce the number of decimals - * @param {object} tree - * @param {object} [options={}] - * @param {number} [options.fixed=undefined] - number of decimal ot keep - * @returns */ - -export function compressTree(tree, options = {}) { +export function compressTree( + tree: Tree, + options: { + // number of decimal ot keep + fixed?: number; + } = {}, +): Tree { const { fixed } = options; return JSON.parse( JSON.stringify(tree, (key, value) => { diff --git a/src/createTree.js b/src/createTree.ts similarity index 59% rename from src/createTree.js rename to src/createTree.ts index b0905cd..8a5fe2c 100644 --- a/src/createTree.js +++ b/src/createTree.ts @@ -1,19 +1,49 @@ import binarySearch from 'binary-search'; +import { DataXY } from 'cheminfo-types'; -/** - * @typedef {import("../tree-similarity").Tree} Tree - * @typedef {import("../tree-similarity").CreateTreeOptions} CreateTreeOptions - * @typedef {import("../tree-similarity").Spectrum} Spectrum - */ +export interface Tree { + sum: number; + center: number; + /** + * left and right have the same structure than the parent, + * or are null if they are leaves + */ + left: Tree | null; + right: Tree | null; +} + +export interface CreateTreeOptions { + /** + * low limit of the tree + * @default x[0] + */ + from?: number; + /** + * high limit of the tree + * @default x.at(-1) + */ + to?: number; + /** + * minimal sum value to accept a node + * @default 0.01 + */ + threshold?: number; + /** + * minimal window width to create a node + * @default 0.16 + */ + minWindow?: number; +} /** * Function that creates the tree - * @param {Spectrum} spectrum - * @param {CreateTreeOptions} [options] - * @return { Tree | null } */ -export function createTree(spectrum, options = {}) { - const { x, y } = spectrum; + +export function createTree( + dataXY: DataXY, + options: CreateTreeOptions = {}, +): Tree | null { + const { x, y } = dataXY; const { minWindow = 0.16, threshold = 0.01, @@ -30,7 +60,7 @@ function mainCreateTree(x, y, from, to, minWindow, threshold) { } // search first point - let start = binarySearch(x, from, (a, b) => a - b); + let start = binarySearch(x, from, (a: number, b: number) => a - b); if (start < 0) { start = ~start; } diff --git a/src/index.js b/src/index.js deleted file mode 100644 index bfde6a8..0000000 --- a/src/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export { treeSimilarity } from './treeSimilarity'; -export { createTree } from './createTree'; -export { compressTree } from './compressTree'; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..0fe3afa --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export * from './treeSimilarity'; +export * from './createTree'; +export { compressTree } from './compressTree'; diff --git a/src/treeSimilarity.js b/src/treeSimilarity.js deleted file mode 100644 index 6deeb5c..0000000 --- a/src/treeSimilarity.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @typedef {import("../tree-similarity").Tree} Tree - * @typedef {import("../tree-similarity").TreeSimilarityOptions} TreeSimilarityOptions - */ -/** - * Similarity between two nodes - * @param {Tree | null} a - tree A node - * @param {Tree | null} b - tree B node - * @param {TreeSimilarityOptions} [options] - * @return {number} similarity measure between tree nodes - */ -export function treeSimilarity(a, b, options = {}) { - const { alpha = 0.1, beta = 0.33, gamma = 0.001 } = options; - - if (a === null || b === null) { - return 0; - } - - if (!isTree(a) || !isTree(b)) { - throw new Error('tree similarity expects tree as inputs'); - } - - if (a.sum === 0 && b.sum === 0) { - return 1; - } - - const C = - (alpha * Math.min(a.sum, b.sum)) / Math.max(a.sum, b.sum) + - (1 - alpha) * Math.exp(-gamma * Math.abs(a.center - b.center)); - - return ( - beta * C + - ((1 - beta) * - (treeSimilarity(a.left, b.left, options) + - treeSimilarity(a.right, b.right, options))) / - 2 - ); -} - -function isTree(a) { - return ['sum', 'center', 'left', 'right'].every((key) => key in a); -} diff --git a/src/treeSimilarity.ts b/src/treeSimilarity.ts new file mode 100644 index 0000000..547ed2c --- /dev/null +++ b/src/treeSimilarity.ts @@ -0,0 +1,47 @@ +import { Tree } from './createTree'; + +export interface TreeSimilarityOptions { + alpha?: number; + beta?: number; + gamma?: number; +} + +/** + * Similarity between two nodes + * @return similarity measure between tree nodes + */ +export function treeSimilarity( + treeA: Tree | null, + treeB: Tree | null, + options: TreeSimilarityOptions = {}, +): number { + const { alpha = 0.1, beta = 0.33, gamma = 0.001 } = options; + + if (treeA === null || treeB === null) { + return 0; + } + + if (!isTree(treeA) || !isTree(treeB)) { + throw new Error('tree similarity expects tree as inputs'); + } + + if (treeA.sum === 0 && treeB.sum === 0) { + return 1; + } + + const C = + (alpha * Math.min(treeA.sum, treeB.sum)) / Math.max(treeA.sum, treeB.sum) + + (1 - alpha) * Math.exp(-gamma * Math.abs(treeA.center - treeB.center)); + + return ( + beta * C + + ((1 - beta) * + (treeSimilarity(treeA.left, treeB.left, options) + + treeSimilarity(treeA.right, treeB.right, options))) / + 2 + ); +} + +function isTree(tree: object): tree is Tree { + return ['sum', 'center', 'left', 'right'].every((key) => key in tree); +} diff --git a/tree-similarity.d.ts b/tree-similarity.d.ts deleted file mode 100644 index 12c537e..0000000 --- a/tree-similarity.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NumberArray } from "cheminfo-types"; - -export interface Tree { - sum: number; - center: number; - /** - * left and right have the same structure than the parent, - * or are null if they are leaves - */ - left: Tree | null; - right: Tree | null; -} - -export interface CreateTreeOptions { - /** - * low limit of the tree - * @default x[0] - */ - from?: number - /** - * high limit of the tree - * @default x.at(-1) - */ - to?: number - /** - * minimal sum value to accept a node - * @default 0.01 - */ - threshold?: number; - /** - * minimal window width to create a node - * @default 0.16 - */ - minWindow?: number -} - -export interface TreeSimilarityOptions { - alpha?: number; - beta?: number; - gamma?: number; -} -export interface Spectrum { - x: NumberArray; - y: NumberArray; -} \ No newline at end of file diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 0000000..56bd566 --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "declarationMap": true + }, + "exclude": ["./src/**/__tests__"] +} \ No newline at end of file diff --git a/tsconfig.esm.json b/tsconfig.esm.json new file mode 100644 index 0000000..c0485b0 --- /dev/null +++ b/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.cjs.json", + "compilerOptions": { + "module": "es2020", + "outDir": "lib-esm" + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7eed2d9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "allowJs": false, + "esModuleInterop": true, + "moduleResolution": "node", + "skipLibCheck": true, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2020", + "noImplicitAny": false, + }, + "include": ["./src/**/*"] +} \ No newline at end of file