Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 8 additions & 25 deletions .github/workflows/typedoc.yml
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
name: Deploy TypeDoc on GitHub pages
name: TypeDoc

on:
workflow_dispatch:
release:
types: [published]

env:
NODE_VERSION: 20.x
ENTRY_FILE: 'src/index.ts'

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- 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@releases/v4
with:
token: ${{ secrets.BOT_TOKEN }}
branch: gh-pages
folder: docs
clean: true
typedoc:
# Documentation: https://github.com/zakodium/workflows#typedoc
uses: zakodium/workflows/.github/workflows/typedoc.yml@typedoc-v1
with:
entry: 'src/index.ts'
secrets:
github-token: ${{ secrets.BOT_TOKEN }}
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,5 @@ jspm_packages
.node_repl_history

lib

lib-esm
docs
.claude
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineConfig, globalIgnores } from 'eslint/config';
import ts from 'eslint-config-cheminfo-typescript/base';

export default defineConfig(globalIgnores(['coverage', 'lib', 'lib-esm']), ts);
export default defineConfig(globalIgnores(['coverage', 'lib']), ts);
21 changes: 11 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,27 @@
"name": "ml-regression-base",
"version": "4.0.1",
"description": "Base class for regression modules",
"main": "./lib/index.js",
"module": "./lib-esm/index.js",
"types": "./lib/index.d.ts",
"type": "module",
"exports": {
".": "./lib/index.js"
},
"sideEffects": false,
"files": [
"src",
"lib",
"lib-esm"
"src"
],
"scripts": {
"check-types": "tsc --noEmit",
"clean": "rimraf lib lib-esm",
"clean": "rimraf lib",
"eslint": "eslint src",
"eslint-fix": "npm run eslint -- --fix",
"prepack": "npm run tsc",
"prettier": "prettier --check src",
"prettier-write": "prettier --write src",
"test": "npm run test-only && npm run eslint && npm run prettier && npm run check-types",
"test": "npm run test-only && npm run check-types && npm run eslint && npm run prettier",
"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"
"tsc": "npm run clean && npm run tsc-build",
"tsc-build": "tsc --project tsconfig.build.json"
},
"repository": {
"type": "git",
Expand All @@ -41,7 +40,9 @@
"is-any-array": "^3.0.0"
},
"devDependencies": {
"@types/node": "^25.6.0",
"@vitest/coverage-v8": "^4.1.4",
"@zakodium/tsconfig": "^1.0.5",
"eslint": "^9.39.4",
"eslint-config-cheminfo-typescript": "^22.0.0",
"prettier": "^3.8.3",
Expand Down
25 changes: 14 additions & 11 deletions src/BaseRegression.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type { NumberArray } from 'cheminfo-types';
import { isAnyArray } from 'is-any-array';

import { checkArrayLength } from './checkArrayLength';
import { checkArrayLength } from './checkArrayLength.ts';

export interface RegressionScore {
r: number;
r2: number;
chi2: number;
rmsd: number;
}

export class BaseRegression {
constructor() {
if (new.target === BaseRegression) {
Expand Down Expand Up @@ -55,15 +56,15 @@ export class BaseRegression {
* Return the correlation coefficient of determination (r) and chi-square.
* @param x - explanatory variable
* @param y - response variable
* @return - Object with further statistics.
* @returns Object with further statistics.
*/
score(x: NumberArray, y: NumberArray): RegressionScore {
checkArrayLength(x, y);

const n = x.length;
const y2: number[] = new Array(n);
for (let i = 0; i < n; i++) {
y2[i] = this._predict(x[i]);
y2[i] = this._predict(x[i] as number);
}

let xSum = 0;
Expand All @@ -74,15 +75,17 @@ export class BaseRegression {
let ySquared = 0;
let xY = 0;
for (let i = 0; i < n; i++) {
xSum += y2[i];
ySum += y[i];
xSquared += y2[i] * y2[i];
ySquared += y[i] * y[i];
xY += y2[i] * y[i];
if (y[i] !== 0) {
chi2 += ((y[i] - y2[i]) * (y[i] - y2[i])) / y[i];
const yi = y[i] as number;
const y2i = y2[i] as number;
xSum += y2i;
ySum += yi;
xSquared += y2i * y2i;
ySquared += yi * yi;
xY += y2i * yi;
if (yi !== 0) {
chi2 += ((yi - y2i) * (yi - y2i)) / yi;
}
rmsd += (y[i] - y2[i]) * (y[i] - y2[i]);
rmsd += (yi - y2i) * (yi - y2i);
}

const r =
Expand Down
136 changes: 70 additions & 66 deletions src/__tests__/BaseRegression.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { expect, test } from 'vitest';

import { BaseRegression } from '..';
import { BaseRegression } from '../index.ts';

class NoPredict extends BaseRegression {}
class Basic extends BaseRegression {
Expand All @@ -9,82 +9,86 @@ class Basic extends BaseRegression {
super();
this.factor = factor;
}
_predict(x: number) {
override _predict(x: number) {
return x * this.factor;
}
}

describe('base regression', () => {
it('should not be directly constructable', () => {
expect(() => {
// eslint-disable-next-line no-new
new BaseRegression();
}).toThrow(/BaseRegression must be subclassed/);
});
test('should not be directly constructable', () => {
expect(() => {
// eslint-disable-next-line no-new
new BaseRegression();
}).toThrow(/BaseRegression must be subclassed/);
});

it('should throw if _predict is not implemented', () => {
const reg = new NoPredict();
expect(() => {
reg.predict(0);
}).toThrow(/_predict must be implemented/);
});
test('should throw if _predict is not implemented', () => {
const reg = new NoPredict();

it('should do a basic prediction', () => {
const basic = new Basic(2);
expect(basic.predict(1)).toBe(2);
expect(basic.predict(2)).toBe(4);
expect(basic.predict([2, 3])).toStrictEqual([4, 6]);
});
expect(() => {
reg.predict(0);
}).toThrow(/_predict must be implemented/);
});

test('should do a basic prediction', () => {
const basic = new Basic(2);

expect(basic.predict(1)).toBe(2);
expect(basic.predict(2)).toBe(4);
expect(basic.predict([2, 3])).toStrictEqual([4, 6]);
});

test('should throw on invalid value', () => {
const basic = new Basic(2);

it('should throw on invalid value', () => {
const basic = new Basic(2);
expect(() => {
// @ts-expect-error testing invalid input
basic.predict();
}).toThrow(/must be a number or array/);
expect(() => {
// @ts-expect-error testing invalid input
basic.predict();
}).toThrow(/must be a number or array/);
});

test('should implement dummy predictor functions', () => {
const basic = new Basic(2);
basic.train(); // should not throw

expect(basic.toString()).toBe('');
expect(basic.toLaTeX()).toBe('');
});

test('should implement a scoring function', () => {
const basic = new Basic(2);

expect(basic.score([1, 2], [2, 4])).toStrictEqual({
r: 1,
r2: 1,
chi2: 0,
rmsd: 0,
});
expect(basic.score([1, 2], [2, 4.1]).rmsd).toBe(0.0707106781186545);

it('should implement dummy predictor functions', () => {
const basic = new Basic(2);
basic.train(); // should not throw
expect(basic.toString()).toBe('');
expect(basic.toLaTeX()).toBe('');
expect(basic.score([1, 2], [0.5, 2])).toStrictEqual({
r: 1,
r2: 1,
chi2: 6.5,
rmsd: 1.7677669529663689,
});

it('should implement a scoring function', () => {
const basic = new Basic(2);
expect(basic.score([1, 2], [2, 4])).toStrictEqual({
r: 1,
r2: 1,
chi2: 0,
rmsd: 0,
});
expect(basic.score([1, 2], [2, 4.1]).rmsd).toBe(0.0707106781186545);

expect(basic.score([1, 2], [0.5, 2])).toStrictEqual({
r: 1,
r2: 1,
chi2: 6.5,
rmsd: 1.7677669529663689,
});

expect(basic.score([1, 2], [0, 1])).toStrictEqual({
r: 1,
r2: 1,
chi2: 9,
rmsd: 2.5495097567963922,
});
expect(basic.score([1, 2], [0, 1])).toStrictEqual({
r: 1,
r2: 1,
chi2: 9,
rmsd: 2.5495097567963922,
});
});

it('should throw error if inputs are not arrays or has different length', () => {
const basic = new Basic(2);
expect(() => {
// @ts-expect-error testing invalid input
basic.score(1, 2);
}).toThrow('x and y must be arrays');
test('should throw error if inputs are not arrays or has different length', () => {
const basic = new Basic(2);

expect(() => {
basic.score([1, 2], [2]);
}).toThrow('x and y arrays must have the same length');
});
expect(() => {
// @ts-expect-error testing invalid input
basic.score(1, 2);
}).toThrow('x and y must be arrays');

expect(() => {
basic.score([1, 2], [2]);
}).toThrow('x and y arrays must have the same length');
});
58 changes: 29 additions & 29 deletions src/__tests__/checkArrayLength.test.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
import { describe, expect, it } from 'vitest';
import { expect, test } from 'vitest';

import { checkArrayLength } from '..';
import { checkArrayLength } from '../index.ts';

describe('checkArrayLength', () => {
it('throws on different Length', () => {
const expected = /x and y arrays must have the same length/;
expect(() => checkArrayLength([], [1])).toThrow(expected);
expect(() => checkArrayLength([1], [])).toThrow(expected);
expect(() => checkArrayLength([1], [1, 2])).toThrow(expected);
});
test('throws on different Length', () => {
const expected = /x and y arrays must have the same length/;

it('throws if not arrays', () => {
const expected = /x and y must be arrays/;
// @ts-expect-error testing invalid input
expect(() => checkArrayLength(null, [1])).toThrow(expected);
// @ts-expect-error testing invalid input
expect(() => checkArrayLength([], null)).toThrow(expected);
// @ts-expect-error testing invalid input
expect(() => checkArrayLength()).toThrow(expected);
// @ts-expect-error testing invalid input
expect(() => checkArrayLength(42, [])).toThrow(expected);
// @ts-expect-error testing invalid input
expect(() => checkArrayLength([1, 2, 3, 4, 5], 'hello')).toThrow(expected);
});
expect(() => checkArrayLength([], [1])).toThrow(expected);
expect(() => checkArrayLength([1], [])).toThrow(expected);
expect(() => checkArrayLength([1], [1, 2])).toThrow(expected);
});

test('throws if not arrays', () => {
const expected = /x and y must be arrays/;

// @ts-expect-error testing invalid input
expect(() => checkArrayLength(null, [1])).toThrow(expected);
// @ts-expect-error testing invalid input
expect(() => checkArrayLength([], null)).toThrow(expected);
// @ts-expect-error testing invalid input
expect(() => checkArrayLength()).toThrow(expected);
// @ts-expect-error testing invalid input
expect(() => checkArrayLength(42, [])).toThrow(expected);
// @ts-expect-error testing invalid input
expect(() => checkArrayLength([1, 2, 3, 4, 5], 'hello')).toThrow(expected);
});

it('correct result', () => {
expect(checkArrayLength([1, 2], [2, 3])).toBeUndefined();
expect(
checkArrayLength(new Float64Array([1, 2]), new Float64Array([2, 3])),
).toBeUndefined();
expect(checkArrayLength([1, 2], new Float64Array([2, 3]))).toBeUndefined();
});
test('correct result', () => {
expect(checkArrayLength([1, 2], [2, 3])).toBeUndefined();
expect(
checkArrayLength(new Float64Array([1, 2]), new Float64Array([2, 3])),
).toBeUndefined();
expect(checkArrayLength([1, 2], new Float64Array([2, 3]))).toBeUndefined();
});
Loading