From 20bcc29c4bcabcb4f428fd0c1312842fc2d02873 Mon Sep 17 00:00:00 2001 From: jobo322 Date: Thu, 28 May 2026 15:40:58 -0500 Subject: [PATCH 1/3] feat(pseudovoigt): add TCH style Pseudo-Voigt peak shape with independent Gaussian and Lorentzian broadening --- src/shapes/1d/Shape1D.ts | 8 +- src/shapes/1d/Shape1DClass.ts | 7 +- src/shapes/1d/Shape1DInstance.ts | 2 + src/shapes/1d/getShape1D.ts | 3 + .../1d/pseudoVoigtTCH/PseudoVoigtTCH.ts | 238 ++++++++++++++++++ 5 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 src/shapes/1d/pseudoVoigtTCH/PseudoVoigtTCH.ts diff --git a/src/shapes/1d/Shape1D.ts b/src/shapes/1d/Shape1D.ts index 4bd73f1..165c51c 100644 --- a/src/shapes/1d/Shape1D.ts +++ b/src/shapes/1d/Shape1D.ts @@ -2,6 +2,7 @@ import type { GaussianClassOptions } from './gaussian/Gaussian'; import { GeneralizedLorentzianClassOptions } from './generalizedLorentzian/GeneralizedLorentzian'; import type { LorentzianClassOptions } from './lorentzian/Lorentzian'; import type { PseudoVoigtClassOptions } from './pseudoVoigt/PseudoVoigt'; +import type { PseudoVoigtTCHClassOptions } from './pseudoVoigtTCH/PseudoVoigtTCH'; /** * kind of shape @@ -27,9 +28,14 @@ export interface GeneralizedLorentzianShape1D kind: 'generalizedLorentzian'; } +export interface PseudoVoigtTCHShape1D extends PseudoVoigtTCHClassOptions { + kind: 'pseudoVoigtTCH'; +} + export type Shape1D = | GaussianShape1D | LorentzianShape1D | PseudoVoigtShape1D | LorentzianDispersiveShape1D - | GeneralizedLorentzianShape1D; + | GeneralizedLorentzianShape1D + | PseudoVoigtTCHShape1D; diff --git a/src/shapes/1d/Shape1DClass.ts b/src/shapes/1d/Shape1DClass.ts index 724988e..d8983ff 100644 --- a/src/shapes/1d/Shape1DClass.ts +++ b/src/shapes/1d/Shape1DClass.ts @@ -2,7 +2,12 @@ import { DoubleArray } from 'cheminfo-types'; import { GetData1DOptions } from './GetData1DOptions'; -export type Parameter = 'fwhm' | 'mu' | 'gamma'; +export type Parameter = + | 'fwhm' + | 'mu' + | 'gamma' + | 'fwhmG' + | 'fwhmL'; export interface Shape1DClass { fwhm: number; diff --git a/src/shapes/1d/Shape1DInstance.ts b/src/shapes/1d/Shape1DInstance.ts index 9f6cf54..44ddff5 100644 --- a/src/shapes/1d/Shape1DInstance.ts +++ b/src/shapes/1d/Shape1DInstance.ts @@ -3,10 +3,12 @@ import { GeneralizedLorentzian } from './generalizedLorentzian/GeneralizedLorent import { Lorentzian } from './lorentzian/Lorentzian'; import { LorentzianDispersive } from './lorentzianDispersive/LorentzianDispersive'; import { PseudoVoigt } from './pseudoVoigt/PseudoVoigt'; +import { PseudoVoigtTCH } from './pseudoVoigtTCH/PseudoVoigtTCH'; export type Shape1DInstance = | Gaussian | Lorentzian | PseudoVoigt + | PseudoVoigtTCH | LorentzianDispersive | GeneralizedLorentzian; diff --git a/src/shapes/1d/getShape1D.ts b/src/shapes/1d/getShape1D.ts index 433490d..f5e9fb3 100644 --- a/src/shapes/1d/getShape1D.ts +++ b/src/shapes/1d/getShape1D.ts @@ -5,6 +5,7 @@ import { GeneralizedLorentzian } from './generalizedLorentzian/GeneralizedLorent import { Lorentzian } from './lorentzian/Lorentzian'; import { LorentzianDispersive } from './lorentzianDispersive/LorentzianDispersive'; import { PseudoVoigt } from './pseudoVoigt/PseudoVoigt'; +import { PseudoVoigtTCH } from './pseudoVoigtTCH/PseudoVoigtTCH'; /** * Generate a instance of a specific kind of shape. @@ -19,6 +20,8 @@ export function getShape1D(shape: Shape1D): Shape1DInstance { return new Lorentzian(shape); case 'pseudoVoigt': return new PseudoVoigt(shape); + case 'pseudoVoigtTCH': + return new PseudoVoigtTCH(shape); case 'lorentzianDispersive': return new LorentzianDispersive(shape); case 'generalizedLorentzian': diff --git a/src/shapes/1d/pseudoVoigtTCH/PseudoVoigtTCH.ts b/src/shapes/1d/pseudoVoigtTCH/PseudoVoigtTCH.ts new file mode 100644 index 0000000..3b1a24c --- /dev/null +++ b/src/shapes/1d/pseudoVoigtTCH/PseudoVoigtTCH.ts @@ -0,0 +1,238 @@ +import { + GAUSSIAN_EXP_FACTOR, + ROOT_2LN2_MINUS_ONE, + ROOT_PI_OVER_LN2, +} from '../../../util/constants'; +import { GetData1DOptions } from '../GetData1DOptions'; +import { Parameter, Shape1DClass } from '../Shape1DClass'; +import { gaussianFct } from '../gaussian/Gaussian'; +import { lorentzianFct } from '../lorentzian/Lorentzian'; +import { pseudoVoigtFindFactor } from '../pseudoVoigt/computeFactor'; + +export interface PseudoVoigtTCHClassOptions { + /** + * Full width at half maximum of gaussian. + * @default 500 + */ + fwhmG?: number; + /** + * Full width at half maximum of lorentzian. + * @default 500 + */ + fwhmL?: number; + /** + * Full width at half maximum. + * @default 500 + */ + fwhm?: number; + /** + * Ratio of gaussian contribution in the shape + * @default 0.5 + */ + mu?: number; +} + +interface GetPseudoVoigtAreaOptions { + /** + * The maximum intensity value of the shape + * @default 1 + */ + height?: number; + /** + * Full width at half maximum. + * @default 500 + */ + fwhm?: number; + /** + * Ratio of gaussian contribution in the shape + * @default 0.5 + */ + mu?: number; +} + +interface CalculatePseudoVoightHeightOptions { + /** + * @default 1 + */ + fwhm?: number; + /** + * @default 0.5 + */ + mu?: number; + /** + * @default 1 + */ + area?: number; +} + +export class PseudoVoigtTCH implements Shape1DClass { + private _fwhmG: number; + private _fwhmL: number; + public fwhm: number; + /** + * Ratio of gaussian contribution in the shape + * @default 0.5 + */ + public mu: number; + + public constructor(options: PseudoVoigtTCHClassOptions = {}) { + const { fwhm = 500, mu = 0.5, fwhmG = 500, fwhmL = 500 } = options; + + this.mu = mu; + this.fwhm = fwhm; + this._fwhmG = fwhmG; + this._fwhmL = fwhmL; + this.fwhmG = fwhmG; + this.fwhmL = fwhmL; + } + + public set fwhmG(value: number) { + const H = computeEffectiveWidth(value, this._fwhmL); + const r = this._fwhmL / H; + const mu = 1 - (1.36603 * r - 0.47719 * r * r + 0.11116 * r * r * r); + this.fwhm = H; + this.mu = mu; + this._fwhmG = value; + } + + public get fwhmG() { + return this._fwhmG; + } + + public set fwhmL(value: number) { + const H = computeEffectiveWidth(this._fwhmG, value); + const r = value / H; + const mu = 1 - (1.36603 * r - 0.47719 * r * r + 0.11116 * r * r * r); + this.fwhm = H; + this.mu = mu; + this._fwhmL = value; + } + + public get fwhmL() { + return this._fwhmL; + } + + public fwhmToWidth(fwhm = this.fwhm, mu = this.mu) { + return pseudoVoigtFwhmToWidth(fwhm, mu); + } + + public widthToFWHM(width: number, mu: number = this.mu) { + return pseudoVoigtWidthToFWHM(width, mu); + } + + public fct(x: number) { + return pseudoVoigtFct(x, this.fwhm, this.mu); + } + + public getArea(height = 1) { + return getPseudoVoigtArea({ fwhm: this.fwhm, height, mu: this.mu }); + } + + public getFactor(area?: number) { + return getPseudoVoigtFactor(area, this.mu); + } + + public getData(options: GetData1DOptions = {}) { + const { + length, + factor, + height = calculatePseudoVoigtHeight({ + fwhm: this.fwhm, + mu: this.mu, + area: 1, + }), + } = options; + return getPseudoVoigtData(this, { factor, length, height }); + } + + public calculateHeight(area = 1) { + return calculatePseudoVoigtHeight({ fwhm: this.fwhm, mu: this.mu, area }); + } + + public getParameters(): Parameter[] { + return ['fwhmG', 'fwhmL']; + } +} + +export const calculatePseudoVoigtHeight = ( + options: CalculatePseudoVoightHeightOptions = {}, +) => { + let { fwhm = 1, mu = 0.5, area = 1 } = options; + return (2 * area) / (fwhm * (mu * ROOT_PI_OVER_LN2 + (1 - mu) * Math.PI)); +}; + +export const pseudoVoigtFct = (x: number, fwhm: number, mu: number) => { + return (1 - mu) * lorentzianFct(x, fwhm) + mu * gaussianFct(x, fwhm); +}; + +export const pseudoVoigtWidthToFWHM = (width: number, mu = 0.5) => { + return width * (mu * ROOT_2LN2_MINUS_ONE + 1); +}; + +export const pseudoVoigtFwhmToWidth = (fwhm: number, mu = 0.5) => { + return fwhm / (mu * ROOT_2LN2_MINUS_ONE + 1); +}; + +export const getPseudoVoigtArea = (options: GetPseudoVoigtAreaOptions) => { + const { fwhm = 500, height = 1, mu = 0.5 } = options; + return (fwhm * height * (mu * ROOT_PI_OVER_LN2 + (1 - mu) * Math.PI)) / 2; +}; + +export const getPseudoVoigtFactor = (area = 0.9999, mu = 0.5) => { + return pseudoVoigtFindFactor(area, mu); +}; + +export const getPseudoVoigtData = ( + shape: PseudoVoigtTCHClassOptions = {}, + options: GetData1DOptions = {}, +) => { + let { fwhm = 500, mu = 0.5 } = shape; + let { + length, + factor = getPseudoVoigtFactor(0.999, mu), + height = calculatePseudoVoigtHeight({ fwhm, mu, area: 1 }), + } = options; + + if (!height) { + height = + 1 / + ((mu / Math.sqrt(-GAUSSIAN_EXP_FACTOR / Math.PI)) * fwhm + + ((1 - mu) * fwhm * Math.PI) / 2); + } + + if (!length) { + length = Math.min(Math.ceil(fwhm * factor), Math.pow(2, 25) - 1); + if (length % 2 === 0) length++; + } + + const center = (length - 1) / 2; + const data = new Float64Array(length); + for (let i = 0; i <= center; i++) { + data[i] = pseudoVoigtFct(i - center, fwhm, mu) * height; + data[length - 1 - i] = data[i]; + } + + return data; +}; + +function computeEffectiveWidth(Hg: number, Hl: number): number { + const Hg2 = Hg * Hg; + const Hg3 = Hg2 * Hg; + const Hg4 = Hg3 * Hg; + const Hg5 = Hg4 * Hg; + + const Hl2 = Hl * Hl; + const Hl3 = Hl2 * Hl; + const Hl4 = Hl3 * Hl; + const Hl5 = Hl4 * Hl; + + return Math.pow( + Hg5 + + 2.69269 * Hg4 * Hl + + 2.42843 * Hg3 * Hl2 + + 4.47163 * Hg2 * Hl3 + + 0.07842 * Hg * Hl4 + + Hl5, + 1 / 5, + ); +} From 848a45377c2e6c99dcebd1a1c8f483d376e3b613 Mon Sep 17 00:00:00 2001 From: jobo322 Date: Fri, 29 May 2026 10:01:31 -0500 Subject: [PATCH 2/3] refactor(pseudovoigt): update PseudoVoigtTCH class to use private properties for fwhm and mu --- src/shapes/1d/Shape1DClass.ts | 7 +- .../1d/pseudoVoigtTCH/PseudoVoigtTCH.ts | 77 +++++++++++++++---- 2 files changed, 61 insertions(+), 23 deletions(-) diff --git a/src/shapes/1d/Shape1DClass.ts b/src/shapes/1d/Shape1DClass.ts index d8983ff..b55d4f1 100644 --- a/src/shapes/1d/Shape1DClass.ts +++ b/src/shapes/1d/Shape1DClass.ts @@ -2,12 +2,7 @@ import { DoubleArray } from 'cheminfo-types'; import { GetData1DOptions } from './GetData1DOptions'; -export type Parameter = - | 'fwhm' - | 'mu' - | 'gamma' - | 'fwhmG' - | 'fwhmL'; +export type Parameter = 'fwhm' | 'mu' | 'gamma' | 'fwhmG' | 'fwhmL'; export interface Shape1DClass { fwhm: number; diff --git a/src/shapes/1d/pseudoVoigtTCH/PseudoVoigtTCH.ts b/src/shapes/1d/pseudoVoigtTCH/PseudoVoigtTCH.ts index 3b1a24c..eb39934 100644 --- a/src/shapes/1d/pseudoVoigtTCH/PseudoVoigtTCH.ts +++ b/src/shapes/1d/pseudoVoigtTCH/PseudoVoigtTCH.ts @@ -68,21 +68,22 @@ interface CalculatePseudoVoightHeightOptions { export class PseudoVoigtTCH implements Shape1DClass { private _fwhmG: number; private _fwhmL: number; - public fwhm: number; + public _fwhm: number; /** * Ratio of gaussian contribution in the shape * @default 0.5 */ - public mu: number; + public _mu: number; + private lorentzianWidthFraction: number; public constructor(options: PseudoVoigtTCHClassOptions = {}) { - const { fwhm = 500, mu = 0.5, fwhmG = 500, fwhmL = 500 } = options; + const { fwhmG = 500, fwhmL = 500 } = options; - this.mu = mu; - this.fwhm = fwhm; + this._mu = 0; + this.lorentzianWidthFraction = 1; + this._fwhm = 500; this._fwhmG = fwhmG; this._fwhmL = fwhmL; - this.fwhmG = fwhmG; this.fwhmL = fwhmL; } @@ -90,8 +91,8 @@ export class PseudoVoigtTCH implements Shape1DClass { const H = computeEffectiveWidth(value, this._fwhmL); const r = this._fwhmL / H; const mu = 1 - (1.36603 * r - 0.47719 * r * r + 0.11116 * r * r * r); - this.fwhm = H; - this.mu = mu; + this._fwhm = H; + this._mu = mu; this._fwhmG = value; } @@ -103,8 +104,8 @@ export class PseudoVoigtTCH implements Shape1DClass { const H = computeEffectiveWidth(this._fwhmG, value); const r = value / H; const mu = 1 - (1.36603 * r - 0.47719 * r * r + 0.11116 * r * r * r); - this.fwhm = H; - this.mu = mu; + this._fwhm = H; + this._mu = mu; this._fwhmL = value; } @@ -112,24 +113,48 @@ export class PseudoVoigtTCH implements Shape1DClass { return this._fwhmL; } - public fwhmToWidth(fwhm = this.fwhm, mu = this.mu) { + public set mu(value: number) { + const r = lorentzianWidthFraction(1 - value); + this.lorentzianWidthFraction = r; + this._fwhmL = this._fwhm * r; + this._fwhmG = this._fwhm * (1 - r); + this._mu = value; + } + + public get mu() { + return this._mu; + } + + public set fwhm(value: number) { + const r = + this.lorentzianWidthFraction || lorentzianWidthFraction(1 - this._mu); + this._fwhmL = value * r; + this._fwhmG = value * (1 - r); + this._fwhm = value; + } + + public get fwhm() { + return this._fwhm; + } + + public fwhmToWidth(fwhm = this._fwhm, mu = this._mu) { return pseudoVoigtFwhmToWidth(fwhm, mu); } - public widthToFWHM(width: number, mu: number = this.mu) { + public widthToFWHM(width: number, mu: number = this._mu) { return pseudoVoigtWidthToFWHM(width, mu); } public fct(x: number) { - return pseudoVoigtFct(x, this.fwhm, this.mu); + return pseudoVoigtFct(x, this._fwhm, this._mu); } public getArea(height = 1) { - return getPseudoVoigtArea({ fwhm: this.fwhm, height, mu: this.mu }); + return getPseudoVoigtArea({ fwhm: this._fwhm, height, mu: this._mu }); } public getFactor(area?: number) { - return getPseudoVoigtFactor(area, this.mu); + return getPseudoVoigtFactor(area, this._mu); } public getData(options: GetData1DOptions = {}) { @@ -137,8 +162,8 @@ export class PseudoVoigtTCH implements Shape1DClass { length, factor, height = calculatePseudoVoigtHeight({ - fwhm: this.fwhm, - mu: this.mu, + fwhm: this._fwhm, + mu: this._mu, area: 1, }), } = options; @@ -236,3 +261,21 @@ function computeEffectiveWidth(Hg: number, Hl: number): number { 1 / 5, ); } + +function lorentzianWidthFraction(eta: number): number { + // solve: + // mu = 1.36603*x - 0.47719*x² + 0.11116*x³ + // where x = Hl / H + + let x = eta; // initial guess + + for (let i = 0; i < 6; i++) { + const f = 1.36603 * x - 0.47719 * x * x + 0.11116 * x * x * x - eta; + + const df = 1.36603 - 2 * 0.47719 * x + 3 * 0.11116 * x * x; + + x -= f / df; + } + + return x; +} From 0080f6b090c31b5f3153171ceadc59dd45f80d46 Mon Sep 17 00:00:00 2001 From: jobo322 Date: Fri, 29 May 2026 12:15:57 -0500 Subject: [PATCH 3/3] feat: initialize also with fwhm --- .../1d/pseudoVoigtTCH/PseudoVoigtTCH.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/shapes/1d/pseudoVoigtTCH/PseudoVoigtTCH.ts b/src/shapes/1d/pseudoVoigtTCH/PseudoVoigtTCH.ts index eb39934..7ee21c9 100644 --- a/src/shapes/1d/pseudoVoigtTCH/PseudoVoigtTCH.ts +++ b/src/shapes/1d/pseudoVoigtTCH/PseudoVoigtTCH.ts @@ -77,14 +77,20 @@ export class PseudoVoigtTCH implements Shape1DClass { private lorentzianWidthFraction: number; public constructor(options: PseudoVoigtTCHClassOptions = {}) { - const { fwhmG = 500, fwhmL = 500 } = options; - - this._mu = 0; - this.lorentzianWidthFraction = 1; - this._fwhm = 500; - this._fwhmG = fwhmG; - this._fwhmL = fwhmL; - this.fwhmL = fwhmL; + const { fwhmG, fwhmL, fwhm, mu = 0.5 } = options; + + this._mu = mu; + this._fwhm = 0; + this._fwhmG = 0; + this._fwhmL = 0; + this.lorentzianWidthFraction = lorentzianWidthFraction(1 - mu); + + if (fwhmG && fwhmL) { + this._fwhmG = fwhmG; + this.fwhmL = fwhmL; + } else if (fwhm) { + this.fwhm = fwhm; + } } public set fwhmG(value: number) {