Fix various issues with pharmacokinetics, improved parameters, distinction between adult/child
This commit is contained in:
299
docs/pharmacokinetics.test.ts.example
Normal file
299
docs/pharmacokinetics.test.ts.example
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Pharmacokinetic Model Tests
|
||||
*
|
||||
* Validates LDX/d-amphetamine concentration calculations against clinical data
|
||||
* from research literature. Tests cover:
|
||||
* - Clinical validation targets (Research Section 5.1)
|
||||
* - Age-specific elimination kinetics (Research Section 5.2)
|
||||
* - Renal function effects (Research Section 8.2)
|
||||
* - Edge cases and boundary conditions
|
||||
*
|
||||
* REFERENCES:
|
||||
* - AI Research Document (2026-01-17): Sections 3.2, 5.1, 5.2, 8.2
|
||||
* - PMC4823324: Ermer et al. meta-analysis of LDX pharmacokinetics
|
||||
* - FDA NDA 021-977: Clinical pharmacology label
|
||||
*
|
||||
* @author Andreas Weyer
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { calculateSingleDoseConcentration } from '../pharmacokinetics';
|
||||
import { getDefaultState } from '../../constants/defaults';
|
||||
import type { PkParams } from '../../constants/defaults';
|
||||
|
||||
// Helper: Get default PK parameters
|
||||
const getDefaultPkParams = (): PkParams => {
|
||||
return getDefaultState().pkParams;
|
||||
};
|
||||
|
||||
describe('Pharmacokinetic Model - Clinical Validation', () => {
|
||||
describe('70mg Reference Case (Research Section 5.1)', () => {
|
||||
test('LDX peak concentration should be ~55-65 ng/mL at 1h', () => {
|
||||
const pkParams = getDefaultPkParams();
|
||||
const result = calculateSingleDoseConcentration('70', 1.0, pkParams);
|
||||
|
||||
// Research target: ~58 ng/mL (±10%)
|
||||
expect(result.ldx).toBeGreaterThan(55);
|
||||
expect(result.ldx).toBeLessThan(65);
|
||||
});
|
||||
|
||||
test('d-Amphetamine peak concentration should be ~75-85 ng/mL at 4h', () => {
|
||||
const pkParams = getDefaultPkParams();
|
||||
const result = calculateSingleDoseConcentration('70', 4.0, pkParams);
|
||||
|
||||
// Research target: ~80 ng/mL (±10%)
|
||||
expect(result.damph).toBeGreaterThan(75);
|
||||
expect(result.damph).toBeLessThan(85);
|
||||
});
|
||||
|
||||
test('Crossover phenomenon: LDX peak < d-Amph peak', () => {
|
||||
const pkParams = getDefaultPkParams();
|
||||
const ldxPeak = calculateSingleDoseConcentration('70', 1.0, pkParams);
|
||||
const damphPeak = calculateSingleDoseConcentration('70', 4.0, pkParams);
|
||||
|
||||
// Characteristic prodrug behavior: prodrug peaks early but lower
|
||||
expect(ldxPeak.ldx).toBeLessThan(damphPeak.damph);
|
||||
});
|
||||
|
||||
test('LDX near-zero by 12h (rapid conversion)', () => {
|
||||
const pkParams = getDefaultPkParams();
|
||||
const result = calculateSingleDoseConcentration('70', 12.0, pkParams);
|
||||
|
||||
// LDX should be essentially eliminated (< 1 ng/mL)
|
||||
expect(result.ldx).toBeLessThan(1.0);
|
||||
});
|
||||
|
||||
test('d-Amphetamine persists at 12h (~25-35 ng/mL)', () => {
|
||||
const pkParams = getDefaultPkParams();
|
||||
const result = calculateSingleDoseConcentration('70', 12.0, pkParams);
|
||||
|
||||
// d-amph has 11h half-life, should still be measurable
|
||||
// ~80 ng/mL at 4h → ~30 ng/mL at 12h (roughly 1 half-life)
|
||||
expect(result.damph).toBeGreaterThan(25);
|
||||
expect(result.damph).toBeLessThan(35);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Age-Specific Elimination (Research Section 5.2)', () => {
|
||||
test('Child elimination: faster than adult (9h vs 11h half-life)', () => {
|
||||
const adultParams = getDefaultPkParams();
|
||||
const childParams = {
|
||||
...adultParams,
|
||||
advanced: {
|
||||
...adultParams.advanced,
|
||||
ageGroup: { preset: 'child' as const }
|
||||
}
|
||||
};
|
||||
|
||||
const adultResult = calculateSingleDoseConcentration('70', 12.0, adultParams);
|
||||
const childResult = calculateSingleDoseConcentration('70', 12.0, childParams);
|
||||
|
||||
// At 12h, child should have ~68% of adult concentration
|
||||
// exp(-ln(2)*12/9) / exp(-ln(2)*12/11) ≈ 0.68
|
||||
const ratio = childResult.damph / adultResult.damph;
|
||||
expect(ratio).toBeGreaterThan(0.60);
|
||||
expect(ratio).toBeLessThan(0.75);
|
||||
});
|
||||
|
||||
test('Adult preset uses 11h half-life', () => {
|
||||
const pkParams = {
|
||||
...getDefaultPkParams(),
|
||||
advanced: {
|
||||
...getDefaultPkParams().advanced,
|
||||
ageGroup: { preset: 'adult' as const }
|
||||
}
|
||||
};
|
||||
|
||||
const result4h = calculateSingleDoseConcentration('70', 4.0, pkParams);
|
||||
const result15h = calculateSingleDoseConcentration('70', 15.0, pkParams);
|
||||
|
||||
// At 15h (4h + 11h), concentration should be ~half of 4h peak
|
||||
// Allows some tolerance for absorption/distribution phase
|
||||
const ratio = result15h.damph / result4h.damph;
|
||||
expect(ratio).toBeGreaterThan(0.40);
|
||||
expect(ratio).toBeLessThan(0.60);
|
||||
});
|
||||
|
||||
test('Custom preset uses base half-life from config', () => {
|
||||
const customParams = {
|
||||
...getDefaultPkParams(),
|
||||
damph: { halfLife: '13' }, // Custom 13h half-life
|
||||
advanced: {
|
||||
...getDefaultPkParams().advanced,
|
||||
ageGroup: { preset: 'custom' as const }
|
||||
}
|
||||
};
|
||||
|
||||
const result4h = calculateSingleDoseConcentration('70', 4.0, customParams);
|
||||
const result17h = calculateSingleDoseConcentration('70', 17.0, customParams);
|
||||
|
||||
// At 17h (4h + 13h), should be ~half of 4h peak
|
||||
const ratio = result17h.damph / result4h.damph;
|
||||
expect(ratio).toBeGreaterThan(0.40);
|
||||
expect(ratio).toBeLessThan(0.60);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Renal Function Effects (Research Section 8.2)', () => {
|
||||
test('Severe renal impairment: ~50% slower elimination', () => {
|
||||
const normalParams = getDefaultPkParams();
|
||||
const renalParams = {
|
||||
...normalParams,
|
||||
advanced: {
|
||||
...normalParams.advanced,
|
||||
renalFunction: {
|
||||
enabled: true,
|
||||
severity: 'severe' as const
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const normalResult = calculateSingleDoseConcentration('70', 18.0, normalParams);
|
||||
const renalResult = calculateSingleDoseConcentration('70', 18.0, renalParams);
|
||||
|
||||
// Severe renal: half-life 11h → 16.5h (1.5x factor)
|
||||
// At 18h, renal patient should have ~1.5x concentration vs normal
|
||||
const ratio = renalResult.damph / normalResult.damph;
|
||||
expect(ratio).toBeGreaterThan(1.3);
|
||||
expect(ratio).toBeLessThan(1.7);
|
||||
});
|
||||
|
||||
test('Normal/mild severity: no adjustment', () => {
|
||||
const baseParams = getDefaultPkParams();
|
||||
|
||||
const normalResult = calculateSingleDoseConcentration('70', 8.0, baseParams);
|
||||
|
||||
const mildParams = {
|
||||
...baseParams,
|
||||
advanced: {
|
||||
...baseParams.advanced,
|
||||
renalFunction: {
|
||||
enabled: true,
|
||||
severity: 'mild' as const
|
||||
}
|
||||
}
|
||||
};
|
||||
const mildResult = calculateSingleDoseConcentration('70', 8.0, mildParams);
|
||||
|
||||
// Mild impairment should not affect elimination in this model
|
||||
expect(mildResult.damph).toBeCloseTo(normalResult.damph, 1);
|
||||
});
|
||||
|
||||
test('Renal function disabled: no effect', () => {
|
||||
const baseParams = getDefaultPkParams();
|
||||
const disabledParams = {
|
||||
...baseParams,
|
||||
advanced: {
|
||||
...baseParams.advanced,
|
||||
renalFunction: {
|
||||
enabled: false,
|
||||
severity: 'severe' as const // Should be ignored when disabled
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const baseResult = calculateSingleDoseConcentration('70', 12.0, baseParams);
|
||||
const disabledResult = calculateSingleDoseConcentration('70', 12.0, disabledParams);
|
||||
|
||||
expect(disabledResult.damph).toBeCloseTo(baseResult.damph, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases and Boundary Conditions', () => {
|
||||
test('Zero dose returns zero concentrations', () => {
|
||||
const pkParams = getDefaultPkParams();
|
||||
const result = calculateSingleDoseConcentration('0', 4.0, pkParams);
|
||||
|
||||
expect(result.ldx).toBe(0);
|
||||
expect(result.damph).toBe(0);
|
||||
});
|
||||
|
||||
test('Negative time returns zero concentrations', () => {
|
||||
const pkParams = getDefaultPkParams();
|
||||
const result = calculateSingleDoseConcentration('70', -1.0, pkParams);
|
||||
|
||||
expect(result.ldx).toBe(0);
|
||||
expect(result.damph).toBe(0);
|
||||
});
|
||||
|
||||
test('Very high dose scales proportionally', () => {
|
||||
const pkParams = getDefaultPkParams();
|
||||
const result70 = calculateSingleDoseConcentration('70', 4.0, pkParams);
|
||||
const result140 = calculateSingleDoseConcentration('140', 4.0, pkParams);
|
||||
|
||||
// Linear pharmacokinetics: 2x dose → 2x concentration
|
||||
expect(result140.damph).toBeCloseTo(result70.damph * 2, 0);
|
||||
});
|
||||
|
||||
test('Food effect delays absorption without changing AUC', () => {
|
||||
const pkParams = getDefaultPkParams();
|
||||
const fedParams = {
|
||||
...pkParams,
|
||||
advanced: {
|
||||
...pkParams.advanced,
|
||||
foodEffect: {
|
||||
enabled: true,
|
||||
tmaxDelay: '1.0' // 1h delay
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Peak should be later for fed state
|
||||
const fastedPeak1h = calculateSingleDoseConcentration('70', 1.0, pkParams);
|
||||
const fedPeak1h = calculateSingleDoseConcentration('70', 1.0, fedParams, true);
|
||||
const fedPeak2h = calculateSingleDoseConcentration('70', 2.0, fedParams, true);
|
||||
|
||||
// Fed state at 1h should be lower than fasted (absorption delayed)
|
||||
expect(fedPeak1h.ldx).toBeLessThan(fastedPeak1h.ldx);
|
||||
|
||||
// Fed peak should occur later (around 2h instead of 1h)
|
||||
expect(fedPeak2h.ldx).toBeGreaterThan(fedPeak1h.ldx);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Weight-Based Volume of Distribution', () => {
|
||||
test('Lower body weight increases concentrations', () => {
|
||||
const standardParams = getDefaultPkParams();
|
||||
const lightweightParams = {
|
||||
...standardParams,
|
||||
advanced: {
|
||||
...standardParams.advanced,
|
||||
weightBasedVd: {
|
||||
enabled: true,
|
||||
bodyWeight: '50' // 50kg vs default ~70kg
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const standardResult = calculateSingleDoseConcentration('70', 4.0, standardParams);
|
||||
const lightweightResult = calculateSingleDoseConcentration('70', 4.0, lightweightParams);
|
||||
|
||||
// Smaller Vd → higher concentration
|
||||
// 50kg: Vd ~270L, 70kg: Vd ~377L, ratio ~1.4x
|
||||
const ratio = lightweightResult.damph / standardResult.damph;
|
||||
expect(ratio).toBeGreaterThan(1.2);
|
||||
expect(ratio).toBeLessThan(1.6);
|
||||
});
|
||||
|
||||
test('Higher body weight decreases concentrations', () => {
|
||||
const standardParams = getDefaultPkParams();
|
||||
const heavyweightParams = {
|
||||
...standardParams,
|
||||
advanced: {
|
||||
...standardParams.advanced,
|
||||
weightBasedVd: {
|
||||
enabled: true,
|
||||
bodyWeight: '100' // 100kg
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const standardResult = calculateSingleDoseConcentration('70', 4.0, standardParams);
|
||||
const heavyweightResult = calculateSingleDoseConcentration('70', 4.0, heavyweightParams);
|
||||
|
||||
// Larger Vd → lower concentration
|
||||
const ratio = heavyweightResult.damph / standardResult.damph;
|
||||
expect(ratio).toBeLessThan(0.8);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user