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..bc57d11 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,3 +1 @@ -extends: cheminfo -parserOptions: - sourceType: module +extends: cheminfo-typescript diff --git a/.github/workflows/documentationjs.yml b/.github/workflows/documentationjs.yml deleted file mode 100644 index 0ae90e1..0000000 --- a/.github/workflows/documentationjs.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Deploy documentation.js on GitHub pages - -on: - workflow_dispatch: - release: - types: [published] - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Build documentation - uses: zakodium/documentationjs-action@v1 - - name: Deploy to GitHub pages - uses: JamesIves/github-pages-deploy-action@releases/v4 - with: - token: ${{ secrets.BOT_TOKEN }} - branch: gh-pages - folder: docs - clean: true diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs-ts.yml similarity index 80% rename from .github/workflows/nodejs.yml rename to .github/workflows/nodejs-ts.yml index c8bcc74..71e0af2 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs-ts.yml @@ -3,7 +3,7 @@ name: Node.js CI on: push: branches: - - master + - main pull_request: jobs: @@ -11,4 +11,4 @@ jobs: # Documentation: https://github.com/zakodium/workflows#nodejs-ci uses: zakodium/workflows/.github/workflows/nodejs.yml@nodejs-v1 with: - node-version-matrix: '[14, 16]' + lint-check-types: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 23bb3c4..7f5db58 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Release on: push: branches: - - master + - main jobs: release: @@ -14,4 +14,3 @@ jobs: secrets: github-token: ${{ secrets.BOT_TOKEN }} npm-token: ${{ secrets.NPM_BOT_TOKEN }} - diff --git a/.github/workflows/typedoc.yml b/.github/workflows/typedoc.yml new file mode 100644 index 0000000..7f4a28c --- /dev/null +++ b/.github/workflows/typedoc.yml @@ -0,0 +1,32 @@ +name: Deploy TypeDoc on GitHub pages + +on: + workflow_dispatch: + release: + types: [published] + +env: + NODE_VERSION: 18.x + ENTRY_FILE: 'src/index.ts' + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Install dependencies + run: npm install + - name: Build documentation + uses: zakodium/typedoc-action@v2 + with: + entry: ${{ env.ENTRY_FILE }} + - name: Deploy to GitHub pages + uses: JamesIves/github-pages-deploy-action@v4 + with: + token: ${{ secrets.BOT_TOKEN }} + branch: gh-pages + folder: docs + clean: true diff --git a/.gitignore b/.gitignore index fbd9508..52f2560 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ jspm_packages .node_repl_history lib + +lib-esm diff --git a/README.md b/README.md index 0bc8b1b..5c347d1 100644 --- a/README.md +++ b/README.md @@ -64,5 +64,5 @@ For intercepting at zero using an array, skip the zero in the array (the option [download-url]: https://npmjs.org/package/ml-regression-polynomial [codecov-image]: https://img.shields.io/codecov/c/github/mljs/regression-polynomial.svg [codecov-url]: https://codecov.io/gh/mljs/regression-polynomial -[ci-image]: https://github.com/mljs/regression-polynomial/workflows/Node.js%20CI/badge.svg?branch=master +[ci-image]: https://github.com/mljs/regression-polynomial/workflows/Node.js%20CI/badge.svg?branch=main [ci-url]: https://github.com/mljs/regression-polynomial/actions?query=workflow%3A%22Node.js+CI%22 diff --git a/package.json b/package.json index ee43f75..b5f41bf 100644 --- a/package.json +++ b/package.json @@ -2,21 +2,27 @@ "name": "ml-regression-polynomial", "version": "2.2.0", "description": "Polynomial Regression", - "main": "lib/index.js", - "module": "src/index.js", + "types": "./lib/index.d.ts", + "main": "./lib/index.js", + "module": "./lib-esm/index.js", "files": [ + "src", "lib", - "src" + "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": "jest --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", @@ -30,16 +36,17 @@ }, "homepage": "https://github.com/mljs/regression-polynomial#readme", "devDependencies": { - "@babel/plugin-transform-modules-commonjs": "^7.23.0", + "@vitest/coverage-v8": "^0.34.5", "eslint": "^8.50.0", - "eslint-config-cheminfo": "^9.0.2", - "jest": "^29.7.0", + "eslint-config-cheminfo-typescript": "^12.0.4", "prettier": "^3.0.3", - "rollup": "^3.29.3" + "rimraf": "^5.0.5", + "typescript": "^5.2.2", + "vitest": "^0.34.5" }, "dependencies": { - "@jest/globals": "^29.7.0", + "cheminfo-types": "^1.7.2", "ml-matrix": "^6.10.5", "ml-regression-base": "^3.0.0" } -} +} \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index 07ed9cb..0000000 --- a/rollup.config.js +++ /dev/null @@ -1,8 +0,0 @@ -export default { - input: 'src/index.js', - output: { - file: 'lib/index.js', - format: 'cjs', - }, - external: ['ml-regression-base', 'ml-matrix'], -}; diff --git a/src/__tests__/test.js b/src/__tests__/test.test.ts similarity index 76% rename from src/__tests__/test.js rename to src/__tests__/test.test.ts index 61fbbf1..133c7f5 100644 --- a/src/__tests__/test.js +++ b/src/__tests__/test.test.ts @@ -1,10 +1,14 @@ -import { expect, describe, it } from '@jest/globals'; +import { NumberArray } from 'cheminfo-types'; +import { expect, it, describe } from 'vitest'; import { PolynomialRegression } from '..'; -function assertCoefficientsAndPowers(result, expectedCs, expectedPowers) { - let i = 0; - for (i; i < expectedCs.length; ++i) { +function assertCoefficientsAndPowers( + result: PolynomialRegression, + expectedCs: NumberArray, + expectedPowers: NumberArray, +) { + for (let i = 0; i < expectedCs.length; ++i) { expect(result.coefficients[i]).toBeCloseTo(expectedCs[i], 10e-6); expect(result.powers).toStrictEqual(expectedPowers); } @@ -16,9 +20,10 @@ describe('Polynomial regression', () => { const x = [-3, 0, 2, 4]; const y = [3, 1, 1, 3]; const result = new PolynomialRegression(x, y, 2); - const expected = [0.850519, -0.192495, 0.178462]; + const expectedCoefficients = [0.850519, -0.192495, 0.178462]; + const expectedPowers = [0, 1, 2]; - assertCoefficientsAndPowers(result, expected, [0, 1, 2]); + assertCoefficientsAndPowers(result, expectedCoefficients, expectedPowers); const score = result.score(x, y); expect(score.r2).toBeGreaterThan(0.8); @@ -35,9 +40,10 @@ describe('Polynomial regression', () => { const y = new Float64Array([3, 1, 1, 3]); const result = new PolynomialRegression(x, y, 2); - const expected = [0.850519, -0.192495, 0.178462]; + const expectedCoefficients = [0.850519, -0.192495, 0.178462]; + const expectedPowers = [0, 1, 2]; - assertCoefficientsAndPowers(result, expected, [0, 1, 2]); + assertCoefficientsAndPowers(result, expectedCoefficients, expectedPowers); const score = result.score(x, y); expect(score.r2).toBeGreaterThan(0.8); @@ -98,11 +104,16 @@ describe('Polynomial regression', () => { const solution = [0.018041553971009705, 1.0095279075485593]; assertCoefficientsAndPowers(result, solution, [1, 2]); }); - it('Fit a parabola inverting the degree array terms', () => { + + it('We should get the same result using numeric degree', () => { const x = new Float64Array([-4, 4, 2, 3, 1, 8, 5, 7]); + // the .5 is to prove that we can force the origin on 0. + // remove .5 and it tends to y=x^2 as expected. const y = new Float64Array([16.5, 16.5, 4.5, 9.5, 1.5, 64.5, 25.5, 49.5]); - const result = new PolynomialRegression(x, y, [2, 1]); - const solution = [1.0095279075485593, 0.018041553971009705]; - assertCoefficientsAndPowers(result, solution, [2, 1]); + const result = new PolynomialRegression(x, y, 2, { + interceptAtZero: true, + }); + const solution = [0.018041553971009705, 1.0095279075485593]; + assertCoefficientsAndPowers(result, solution, [1, 2]); }); }); diff --git a/src/index.js b/src/index.ts similarity index 56% rename from src/index.js rename to src/index.ts index 7b544fe..6af58cf 100644 --- a/src/index.js +++ b/src/index.ts @@ -1,15 +1,42 @@ +import { type NumberArray } from 'cheminfo-types'; import { Matrix, MatrixTransposeView, solve } from 'ml-matrix'; import BaseRegression, { checkArrayLength, maybeToPrecision, } from 'ml-regression-base'; +interface PolynomialRegressionOptions { + interceptAtZero?: boolean; +} export class PolynomialRegression extends BaseRegression { - constructor(x, y, degree, options = {}) { + degree: number; + powers: number[]; + coefficients: number[]; + /** + * @param x - independent or explanatory variable + * @param y - dependent or response variable + * @param degree - degree of the polynomial regression, or array of powers to be used. When degree is an array, intercept at zero is forced to false/ignored. + * @example `new PolynomialRegression(x, y, 2)`, in this case, you can pass the option `interceptAtZero`, if you need it. + * @example `new PolynomialRegression(x, y, [1, 3, 5])` + * Each of the degrees corresponds to a column, so if you have them switched, just do: + * @example `new PolynomialRegression(x, y, [3, 1, 5])` + * + * @param options.interceptAtZero - force the polynomial regression so that f(0) = 0 + */ + constructor( + x: NumberArray, + y: NumberArray, + degree: number | NumberArray, + options: PolynomialRegressionOptions = {}, + ) { super(); + // @ts-expect-error internal use only if (x === true) { + // @ts-expect-error internal use only this.degree = y.degree; + // @ts-expect-error internal use only this.powers = y.powers; + // @ts-expect-error internal use only this.coefficients = y.coefficients; } else { checkArrayLength(x, y); @@ -20,7 +47,7 @@ export class PolynomialRegression extends BaseRegression { } } - _predict(x) { + _predict(x: number) { let y = 0; for (let k = 0; k < this.powers.length; k++) { y += this.coefficients[k] * Math.pow(x, this.powers[k]); @@ -37,15 +64,15 @@ export class PolynomialRegression extends BaseRegression { }; } - toString(precision) { + toString(precision: number) { return this._toFormula(precision, false); } - toLaTeX(precision) { + toLaTeX(precision: number) { return this._toFormula(precision, true); } - _toFormula(precision, isLaTeX) { + _toFormula(precision: number, isLaTeX: boolean) { let sup = '^'; let closeSup = ''; let times = ' * '; @@ -78,37 +105,53 @@ export class PolynomialRegression extends BaseRegression { } fn = str + fn; } - if (fn.charAt(0) === '+') { + if (fn.startsWith('+')) { fn = fn.slice(1); } return `f(x) = ${fn}`; } - static load(json) { + static load(json: ReturnType) { if (json.name !== 'polynomialRegression') { throw new TypeError('not a polynomial regression model'); } + // @ts-expect-error internal use only return new PolynomialRegression(true, json); } } -function regress(x, y, degree, options) { +/** + * Perform a polynomial regression on the given data set. + * This is an internal function. + * @param x - independent or explanatory variable + * @param y - dependent or response variable + * @param degree - degree of the polynomial regression + * @param options.interceptAtZero - force the polynomial regression so that $f(0) = 0$ + */ +function regress( + x: NumberArray, + y: NumberArray, + degree: number | NumberArray, + options: PolynomialRegressionOptions = {}, +) { const n = x.length; let { interceptAtZero = false } = options; - let powers; + let powers: number[] = []; if (Array.isArray(degree)) { powers = degree; interceptAtZero = false; //must be false in this case - } else if (interceptAtZero) { - powers = new Array(degree); - for (let k = 0; k < degree; k++) { - powers[k] = k + 1; - } - } else { - powers = new Array(degree + 1); - for (let k = 0; k <= degree; k++) { - powers[k] = k; + } else if (typeof degree === 'number') { + if (interceptAtZero) { + powers = new Array(degree); + for (let k = 0; k < degree; k++) { + powers[k] = k + 1; + } + } else { + powers = new Array(degree + 1); + for (let k = 0; k <= degree; k++) { + powers[k] = k; + } } } const nCoefficients = powers.length; //1 per power, in any case. diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 0000000..3442d16 --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "declarationMap": true + }, + "exclude": [ + "./src/**/__tests__" + ] +} diff --git a/tsconfig.esm.json b/tsconfig.esm.json new file mode 100644 index 0000000..050b45d --- /dev/null +++ b/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.cjs.json", + "compilerOptions": { + "module": "es2020", + "outDir": "lib-esm" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..66e32a3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "allowJs": true, + "esModuleInterop": true, + "moduleResolution": "node", + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2020" + }, + "include": [ + "./src/**/*" + ] +}