diff --git a/.changeset/stupid-oranges-march.md b/.changeset/stupid-oranges-march.md new file mode 100644 index 00000000000..db3ea9ca947 --- /dev/null +++ b/.changeset/stupid-oranges-march.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/kmath": minor +--- + +Add the logarithm math utilities to kmath for supporting Logarithm graph in Interactive Graph diff --git a/packages/kmath/src/coefficients.test.ts b/packages/kmath/src/coefficients.test.ts index 69c63e73173..c3686d4c27a 100644 --- a/packages/kmath/src/coefficients.test.ts +++ b/packages/kmath/src/coefficients.test.ts @@ -1,5 +1,6 @@ import { getExponentialCoefficients, + getLogarithmCoefficients, getTangentCoefficients, } from "./coefficients"; @@ -89,6 +90,94 @@ describe("getExponentialCoefficients", () => { }); }); +describe("getLogarithmCoefficients", () => { + it("returns correct coefficients for Grapher test data (y = 4·log₂(x+6) − 7)", () => { + // coords [-4, -3] and [-5, -7] with asymptote at x = -6 + const result = getLogarithmCoefficients( + [ + [-4, -3], + [-5, -7], + ], + -6, + ); + + // Verify the coefficients reproduce the correct y-values + expect(result).toBeDefined(); + expect(result!.a * Math.log(result!.b * -4 + result!.c)).toBeCloseTo( + -3, + 10, + ); + expect(result!.a * Math.log(result!.b * -5 + result!.c)).toBeCloseTo( + -7, + 10, + ); + }); + + it("returns correct coefficients for the natural log (y = ln(x))", () => { + const result = getLogarithmCoefficients( + [ + [1, 0], + [Math.E, 1], + ], + 0, + ); + + expect(result?.a).toBeCloseTo(1, 10); + expect(result?.b).toBeCloseTo(1, 10); + expect(result?.c).toBeCloseTo(0, 10); + }); + + it("returns correct coefficients when points are left of the asymptote (y = ln(−x))", () => { + const result = getLogarithmCoefficients( + [ + [-1, 0], + [-Math.E, 1], + ], + 0, + ); + + expect(result?.a).toBeCloseTo(1, 10); + expect(result?.b).toBeCloseTo(-1, 10); + expect(result?.c).toBeCloseTo(0, 10); + }); + + it("returns undefined when both points share the same y-coordinate", () => { + expect( + getLogarithmCoefficients( + [ + [1, 3], + [2, 3], + ], + 0, + ), + ).toBeUndefined(); + }); + + it("returns undefined when a point lies on the asymptote", () => { + expect( + getLogarithmCoefficients( + [ + [0, 1], // x === asymptote x + [2, 3], + ], + 0, + ), + ).toBeUndefined(); + }); + + it("returns undefined when points are on opposite sides of the asymptote", () => { + expect( + getLogarithmCoefficients( + [ + [-1, 1], + [1, 2], + ], + 0, + ), + ).toBeUndefined(); + }); +}); + describe("getTangentCoefficients", () => { it("returns correct coefficients for basic tangent", () => { // Simplest case: inflection at origin, p2 one quarter-period away. diff --git a/packages/kmath/src/coefficients.ts b/packages/kmath/src/coefficients.ts index ef339ae2f00..33b9cf809ea 100644 --- a/packages/kmath/src/coefficients.ts +++ b/packages/kmath/src/coefficients.ts @@ -33,6 +33,16 @@ export function getSinusoidCoefficients( return [amplitude, angularFrequency, phase, verticalOffset]; } +/** Coefficients for f(x) = a·ln(b·x + c). */ +export type LogarithmCoefficient = { + /** Vertical scale factor. */ + a: number; + /** Horizontal scale factor. */ + b: number; + /** Horizontal shift (determines vertical asymptote at x = −c/b). */ + c: number; +}; + /** Coefficients for f(x) = a·eᵇˣ + c. */ export type ExponentialCoefficient = { /** Vertical scale factor (amplitude). */ @@ -81,6 +91,59 @@ export function getExponentialCoefficients( return {a, b, c}; } +/** + * Returns the coefficients {a, b, c} for f(x) = a·ln(b·x + c) given two + * points on the curve and the x-value of the vertical asymptote. + * + * Uses the inverse exponential approach: flip each coordinate (x, y) → (y, x), + * compute exponential coefficients from the flipped points, then invert. + * + * Returns undefined if the inputs are geometrically invalid (same y, a point + * on the asymptote, or points on opposite sides of the asymptote). + */ +export function getLogarithmCoefficients( + coords: ReadonlyArray, + asymptote: number, +): LogarithmCoefficient | undefined { + const p1 = coords[0]; + const p2 = coords[1]; + + if (p1[1] === p2[1]) { + return; + } // same y makes bExp undefined + if (p1[0] === asymptote || p2[0] === asymptote) { + return; + } // point on asymptote + + // Flip coordinates: treat logarithm as inverse exponential + const p1Flipped: Coord = [p1[1], p1[0]]; + const p2Flipped: Coord = [p2[1], p2[0]]; + const cExp = asymptote; + + const ratio = (p1Flipped[1] - cExp) / (p2Flipped[1] - cExp); + if (ratio <= 0) { + return; + } // points on opposite sides of asymptote + + const bExp = Math.log(ratio) / (p1Flipped[0] - p2Flipped[0]); + const aExp = (p1Flipped[1] - cExp) / Math.exp(bExp * p1Flipped[0]); + + if (!isFinite(bExp) || !isFinite(aExp) || aExp === 0 || bExp === 0) { + return; + } + + // Invert exponential coefficients to get logarithm coefficients + const a = 1 / bExp; + const b = 1 / aExp; + const c = -cExp / aExp; + + if (!isFinite(a) || !isFinite(b) || !isFinite(c)) { + return; + } + + return {a, b, c}; +} + // p1 is the inflection point (where tan = 0, i.e. the curve crosses // through its vertical offset). p2 is a quarter-period away and // determines the amplitude and period of the tangent function. diff --git a/packages/kmath/src/index.ts b/packages/kmath/src/index.ts index 6eb8811c3b8..44184999972 100644 --- a/packages/kmath/src/index.ts +++ b/packages/kmath/src/index.ts @@ -14,6 +14,7 @@ export {default as KhanMath, sum} from "./math"; export type {Range, SineCoefficient, TangentCoefficient} from "./geometry"; export type { ExponentialCoefficient, + LogarithmCoefficient, AbsoluteValueCoefficient, NamedSineCoefficient, NamedTangentCoefficient,