Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { PathResult } from './correction-searchable.js';
import { SearchNode } from './distance-modeler.js';
import { SearchQuotientNode, PathInputProperties } from './search-quotient-node.js';
import { SearchQuotientSpur } from './search-quotient-spur.js';
import { SearchQuotientRoot } from './search-quotient-root.js';
import { LegacyQuotientRoot } from './legacy-quotient-root.js';
import { TokenResultMapping } from './token-result-mapping.js';

import Distribution = LexicalModelTypes.Distribution;
Expand Down Expand Up @@ -51,6 +53,10 @@ export class LegacyQuotientSpur extends SearchQuotientSpur {
return new LegacyQuotientSpur(parentNode, inputs, inputSource) as this;
}

constructRoot(): SearchQuotientRoot {
return new LegacyQuotientRoot(this.model);
}

protected buildEdgesFromResults(priorResults: ReadonlyArray<TokenResultMapping>): SearchNode[] {
// With a newly-available input, we can extend new input-dependent paths from
// our previously-reached 'extractedResults' nodes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { PriorityQueue } from '@keymanapp/web-utils';
import { LexicalModelTypes } from '@keymanapp/common-types';

import { CORRECTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js';
import { LegacyQuotientRoot } from './legacy-quotient-root.js';
import { generateSpaceSeed, InputSegment, SearchQuotientNode } from './search-quotient-node.js';
import { SearchQuotientRoot } from './search-quotient-root.js';
import { SearchQuotientSpur } from './search-quotient-spur.js';
import { TokenResultMapping } from './token-result-mapping.js';

Expand Down Expand Up @@ -70,6 +70,10 @@ export class SearchQuotientCluster extends SearchQuotientNode {
throw new Error(`SearchQuotientNode does not share the same source identifiers as others in the cluster`);
}

if(path instanceof SearchQuotientRoot) {
throw new Error(`SearchQuotientRoot instances may not be part of clusters`);
}

lowestPossibleSingleCost = Math.min(lowestPossibleSingleCost, path.lowestPossibleSingleCost);
}

Expand Down Expand Up @@ -217,7 +221,9 @@ export class SearchQuotientCluster extends SearchQuotientNode {
split(charIndex: number): [SearchQuotientNode, SearchQuotientNode][] {
// Don't rebuild if this is already a perfect split point!
if(this.codepointLength <= charIndex) {
return [[this, new LegacyQuotientRoot(this.model)]];
// We'll assume that the search path is either using legacy nodes or is not;
// we shouldn't see mixed-use cases.
return [[this, (this.parents[0] as SearchQuotientSpur).constructRoot()]];
}

const results = this.parents.flatMap((p) => p.split(charIndex));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { EDIT_DISTANCE_COST_SCALE, SearchNode } from './distance-modeler.js';
import { generateSpaceSeed, InputSegment, PathInputProperties, SearchQuotientNode } from './search-quotient-node.js';
import { generateSubsetId } from './tokenization-subsets.js';
import { SearchQuotientRoot } from './search-quotient-root.js';
import { LegacyQuotientRoot } from './legacy-quotient-root.js';
import { TokenResultMapping } from './token-result-mapping.js';

import Distribution = LexicalModelTypes.Distribution;
Expand Down Expand Up @@ -45,7 +44,7 @@ export abstract class SearchQuotientSpur extends SearchQuotientNode {
readonly inputs?: Distribution<Transform>;
readonly inputSource?: PathInputProperties;

private parentNode: SearchQuotientNode;
protected readonly parentNode: SearchQuotientNode;
readonly spaceId: number;

readonly inputCount: number;
Expand Down Expand Up @@ -177,6 +176,11 @@ export abstract class SearchQuotientSpur extends SearchQuotientNode {
inputSource: PathInputProperties
): this;

// TODO: Remove once LegacyQuotientRoot + LegacyQuotientSpur are removed!
constructRoot(): SearchQuotientRoot {
return new SearchQuotientRoot(this.model);
}

// spaces are in sequence here.
// `this` = head 'space'.
public merge(space: SearchQuotientNode): SearchQuotientNode {
Expand Down Expand Up @@ -267,7 +271,7 @@ export abstract class SearchQuotientSpur extends SearchQuotientNode {
//
// stopgap: maybe go ahead and check each input for any that are longer?
// won't matter shortly, though.
return [[this, new LegacyQuotientRoot(this.model)]];
return [[this, this.constructRoot()]];
} else {
const firstSet: Distribution<Transform> = this.inputs.map((input) => ({
// keep insert head
Expand Down Expand Up @@ -304,7 +308,7 @@ export abstract class SearchQuotientSpur extends SearchQuotientNode {
// construct two SearchPath instances based on the two sets!
return [[
parent,
this.construct(new LegacyQuotientRoot(this.model), secondSet, {
this.construct(this.constructRoot(), secondSet, {
...this.inputSource,
segment: {
...this.inputSource.segment,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by jahorton on 2026-02-03
*
* This file adds a SearchQuotientSpur variant modeling match & substitute edit
* operations in regard to the corresponding keystroke.
*/

import { LexicalModelTypes } from "@keymanapp/common-types";
import { KMWString } from "@keymanapp/web-utils";

import { SearchNode } from "./distance-modeler.js";
import { PathInputProperties, SearchQuotientNode } from "./search-quotient-node.js";
import { SearchQuotientSpur } from "./search-quotient-spur.js";
import { TokenResultMapping } from "./token-result-mapping.js";

import Distribution = LexicalModelTypes.Distribution;
import ProbabilityMass = LexicalModelTypes.ProbabilityMass;
import Transform = LexicalModelTypes.Transform;

export class SubstitutionQuotientSpur extends SearchQuotientSpur {
public readonly insertLength: number;
public readonly leftDeleteLength: number;

constructor(
parentNode: SearchQuotientNode,
inputs: Distribution<Readonly<Transform>>,
inputSource: PathInputProperties | ProbabilityMass<Transform>
) {
// Compute this SearchPath's codepoint length & edge length.
const inputSample = inputs?.[0].sample ?? { insert: '', deleteLeft: 0 };
const insertLength = KMWString.length(inputSample.insert);
super(parentNode, inputs, inputSource, parentNode.codepointLength + insertLength - inputSample.deleteLeft);

// Compute this SearchPath's codepoint length & edge length.
this.insertLength = insertLength;
this.leftDeleteLength = inputSample.deleteLeft;
}

construct(parentNode: SearchQuotientNode, inputs: ProbabilityMass<Readonly<Transform>>[], inputSource: PathInputProperties): this {
return new SubstitutionQuotientSpur(parentNode, inputs, inputSource) as this;
}

protected buildEdgesFromResults(baseResults: ReadonlyArray<TokenResultMapping>): SearchNode[] {
return baseResults.flatMap((n) => n.buildSubstitutionEdges(this.inputs, this.spaceId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { ContextTransition } from './correction/context-transition.js';
export * from './correction/correction-searchable.js';
export * from './correction/correction-result-mapping.js';
export * from './correction/distance-modeler.js';
export * from './correction/substitution-quotient-spur.js';
export * from './correction/search-quotient-cluster.js';
export * from './correction/search-quotient-spur.js';
export * from './correction/search-quotient-node.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'

import {
models,
LegacyQuotientSpur,
SearchQuotientCluster,
LegacyQuotientRoot
SearchQuotientRoot,
SubstitutionQuotientSpur
} from '@keymanapp/lm-worker/test-index';

import Distribution = LexicalModelTypes.Distribution;
Expand All @@ -31,7 +31,7 @@ const testModel = new TrieModel(jsonFixture('models/tries/english-1000'));
* @returns
*/
export const buildAlphabeticClusterFixtures = () => {
const rootPath = new LegacyQuotientRoot(testModel);
const rootPath = new SearchQuotientRoot(testModel);

// consonant-cluster 1, insert 1, delete 0
const distrib_c1_i1d0: Distribution<Transform> = [
Expand All @@ -48,9 +48,9 @@ export const buildAlphabeticClusterFixtures = () => {
];

// keystrokes 1, codepoints 1, total inserts 1, delete 0
const path_k1c1_i1d0 = new LegacyQuotientSpur(rootPath, distrib_c1_i1d0, distrib_c1_i1d0[0]);
const path_k1c1_i1d0 = new SubstitutionQuotientSpur(rootPath, distrib_c1_i1d0, distrib_c1_i1d0[0]);
// keystrokes 1, codepoints 2, total inserts 2, delete 0
const path_k1c2_i2d0 = new LegacyQuotientSpur(rootPath, distrib_c1_i2d0, distrib_c1_i1d0[0]);
const path_k1c2_i2d0 = new SubstitutionQuotientSpur(rootPath, distrib_c1_i2d0, distrib_c1_i1d0[0]);

// Second input

Expand All @@ -62,8 +62,8 @@ export const buildAlphabeticClusterFixtures = () => {
{ sample: { insert: 'u', deleteLeft: 0, deleteRight: 0, id: 12 }, p: 0.1 },
];

const path_k2c2_i2d0 = new LegacyQuotientSpur(path_k1c1_i1d0, distrib_v1_i1d0, distrib_v1_i1d0[0]);
const path_k2c3_i3d0 = new LegacyQuotientSpur(path_k1c2_i2d0, distrib_v1_i1d0, distrib_v1_i1d0[0]);
const path_k2c2_i2d0 = new SubstitutionQuotientSpur(path_k1c1_i1d0, distrib_v1_i1d0, distrib_v1_i1d0[0]);
const path_k2c3_i3d0 = new SubstitutionQuotientSpur(path_k1c2_i2d0, distrib_v1_i1d0, distrib_v1_i1d0[0]);

// Third input
const distrib_v2_i1d0: Distribution<Transform> = [
Expand All @@ -90,15 +90,15 @@ export const buildAlphabeticClusterFixtures = () => {
{ sample: { insert: 'úú', deleteLeft: 1, deleteRight: 0, id: 13 }, p: 0.02 },
]; // 0.2 total

const path_k3c2_i3d1 = new LegacyQuotientSpur(path_k2c2_i2d0, distrib_v2_i1d1, distrib_v2_i1d0[0]);
const path_k3c2_i3d1 = new SubstitutionQuotientSpur(path_k2c2_i2d0, distrib_v2_i1d1, distrib_v2_i1d0[0]);

const path_k3c3_i3d0 = new LegacyQuotientSpur(path_k2c2_i2d0, distrib_v2_i1d0, distrib_v2_i1d0[0]);
const path_k3c3_i4d1a = new LegacyQuotientSpur(path_k2c2_i2d0, distrib_v2_i2d1, distrib_v2_i1d0[0]);
const path_k3c3_i4d1b = new LegacyQuotientSpur(path_k2c3_i3d0, distrib_v2_i1d1, distrib_v2_i1d0[0]);
const path_k3c3_i3d0 = new SubstitutionQuotientSpur(path_k2c2_i2d0, distrib_v2_i1d0, distrib_v2_i1d0[0]);
const path_k3c3_i4d1a = new SubstitutionQuotientSpur(path_k2c2_i2d0, distrib_v2_i2d1, distrib_v2_i1d0[0]);
const path_k3c3_i4d1b = new SubstitutionQuotientSpur(path_k2c3_i3d0, distrib_v2_i1d1, distrib_v2_i1d0[0]);

// both are built on path k1c2 (splits at index 1)
const path_k3c4_i4d0 = new LegacyQuotientSpur(path_k2c3_i3d0, distrib_v2_i1d0, distrib_v2_i1d0[0]);
const path_k3c4_i5d1 = new LegacyQuotientSpur(path_k2c3_i3d0, distrib_v2_i2d1, distrib_v2_i1d0[0]);
const path_k3c4_i4d0 = new SubstitutionQuotientSpur(path_k2c3_i3d0, distrib_v2_i1d0, distrib_v2_i1d0[0]);
const path_k3c4_i5d1 = new SubstitutionQuotientSpur(path_k2c3_i3d0, distrib_v2_i2d1, distrib_v2_i1d0[0]);

const cluster_k3c3 = new SearchQuotientCluster([path_k3c3_i3d0, path_k3c3_i4d1a, path_k3c3_i4d1b]);
// both are built on path k1c2.
Expand All @@ -116,13 +116,13 @@ export const buildAlphabeticClusterFixtures = () => {
{ sample: { insert: 'vw', deleteLeft: 0, deleteRight: 0, id: 14 }, p: 0.1 }
];

const path_k4c4_i2 = new LegacyQuotientSpur(path_k3c2_i3d1, distrib_c2_i2d0, distrib_c2_i2d0[0]);
const path_k4c4_i1 = new LegacyQuotientSpur(cluster_k3c3, distrib_c2_i1d0, distrib_c2_i2d0[0]);
const path_k4c4_i2 = new SubstitutionQuotientSpur(path_k3c2_i3d1, distrib_c2_i2d0, distrib_c2_i2d0[0]);
const path_k4c4_i1 = new SubstitutionQuotientSpur(cluster_k3c3, distrib_c2_i1d0, distrib_c2_i2d0[0]);

const path_k4c5_i2 = new LegacyQuotientSpur(cluster_k3c3, distrib_c2_i2d0, distrib_c2_i2d0[0]);
const path_k4c5_i1 = new LegacyQuotientSpur(cluster_k3c4, distrib_c2_i1d0, distrib_c2_i2d0[0]);
const path_k4c5_i2 = new SubstitutionQuotientSpur(cluster_k3c3, distrib_c2_i2d0, distrib_c2_i2d0[0]);
const path_k4c5_i1 = new SubstitutionQuotientSpur(cluster_k3c4, distrib_c2_i1d0, distrib_c2_i2d0[0]);

const path_k4c6 = new LegacyQuotientSpur(cluster_k3c4, distrib_c2_i2d0, distrib_c2_i2d0[0]);
const path_k4c6 = new SubstitutionQuotientSpur(cluster_k3c4, distrib_c2_i2d0, distrib_c2_i2d0[0]);

const cluster_k4c4 = new SearchQuotientCluster([path_k4c4_i2, path_k4c4_i1]);
const cluster_k4c5 = new SearchQuotientCluster([path_k4c5_i2, path_k4c5_i1]);
Expand All @@ -135,8 +135,8 @@ export const buildAlphabeticClusterFixtures = () => {
{ sample: { insert: 'z', deleteLeft: 0, deleteRight: 0, id: 15 }, p: 0.4 }
];

const path_k5c6_a = new LegacyQuotientSpur(cluster_k4c4, distrib_c3_i2d0, distrib_c3_i2d0[0]);
const path_k5c6_b = new LegacyQuotientSpur(cluster_k4c5, distrib_c3_i1d0, distrib_c3_i2d0[0]);
const path_k5c6_a = new SubstitutionQuotientSpur(cluster_k4c4, distrib_c3_i2d0, distrib_c3_i2d0[0]);
const path_k5c6_b = new SubstitutionQuotientSpur(cluster_k4c5, distrib_c3_i1d0, distrib_c3_i2d0[0]);

const cluster_k5c6 = new SearchQuotientCluster([path_k5c6_a, path_k5c6_b]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* divergence occurring within the fixture.
*/

import { LegacyQuotientRoot, LegacyQuotientSpur, models } from "@keymanapp/lm-worker/test-index";
import { SearchQuotientRoot, SubstitutionQuotientSpur, models } from "@keymanapp/lm-worker/test-index";
import { jsonFixture } from "@keymanapp/common-test-resources/model-helpers.mjs";

import TrieModel = models.TrieModel;
Expand All @@ -19,31 +19,31 @@ const testModel = new TrieModel(jsonFixture('models/tries/english-1000'));
* Build a linear fixture that models the word 'cant' and words close to that.
*/
export function buildCantLinearFixture() {
const rootPath = new LegacyQuotientRoot(testModel);
const rootPath = new SearchQuotientRoot(testModel);

const distrib1 = [
{ sample: {insert: 'c', deleteLeft: 0, id: 11}, p: 0.5 },
{ sample: {insert: 'r', deleteLeft: 0, id: 11}, p: 0.4 },
{ sample: {insert: 't', deleteLeft: 0, id: 11}, p: 0.1 }
];
const path1 = new LegacyQuotientSpur(rootPath, distrib1, distrib1[0]);
const path1 = new SubstitutionQuotientSpur(rootPath, distrib1, distrib1[0]);

const distrib2 = [
{ sample: {insert: 'a', deleteLeft: 0, id: 12}, p: 0.7 },
{ sample: {insert: 'e', deleteLeft: 0, id: 12}, p: 0.3 }
];
const path2 = new LegacyQuotientSpur(path1, distrib2, distrib2[0]);
const path2 = new SubstitutionQuotientSpur(path1, distrib2, distrib2[0]);

const distrib3 = [
{ sample: {insert: 'n', deleteLeft: 0, id: 13}, p: 0.8 },
{ sample: {insert: 'r', deleteLeft: 0, id: 13}, p: 0.2 }
];
const path3 = new LegacyQuotientSpur(path2, distrib3, distrib3[0]);
const path3 = new SubstitutionQuotientSpur(path2, distrib3, distrib3[0]);

const distrib4 = [
{ sample: {insert: 't', deleteLeft: 0, id: 14}, p: 1 }
];
const path4 = new LegacyQuotientSpur(path3, distrib4, distrib4[0]);
const path4 = new SubstitutionQuotientSpur(path3, distrib4, distrib4[0]);

return {
paths: [null, path1, path2, path3, path4],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { assert } from "chai";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Header missing:

Suggested change
import { assert } from "chai";
/*
* Keyman is copyright (C) SIL Global. MIT License.
* /
import { assert } from "chai";


import { buildQuotientDocFixture } from "./buildQuotientDocFixture.js";

describe('buildQuotientDocFixture() fixture', () => {
it('constructs paths properly', () => {
const {searchRoot, nodes} = buildQuotientDocFixture();

[searchRoot /*, nodes.sc1, nodes.sc2*/].forEach((n) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have the commented nodes in these tests?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #15714 and #15720, which implement these nodes.

I first implemented the fixture and tests in #15720, then commented out the pieces not yet readied in the earlier PRs. They'll become un-commented in the other specialized-node PRs.

assert.equal(n.inputCount, 0);
});
[/*nodes.k1c0,*/ nodes.k1c1, nodes.k1c2 /*, nodes.k1c3*/].forEach((n) => {
assert.equal(n.inputCount, 1);
});
[/*nodes.k2c0, nodes.k2c1,*/ nodes.k2c2, nodes.k2c3].forEach((n) => {
assert.equal(n.inputCount, 2);
});

[searchRoot/*, nodes.k1c0, nodes.k2c0*/].forEach((n) => {
assert.equal(n.codepointLength, 0);
});
[/*nodes.sc1, */nodes.k1c1/*, nodes.k2c1*/].forEach((n) => {
assert.equal(n.codepointLength, 1);
});
[/*nodes.sc2,*/ nodes.k1c2, nodes.k2c2].forEach((n) => {
assert.equal(n.codepointLength, 2);
});
[/*nodes.k1c3,*/ nodes.k2c3].forEach((n) => {
assert.equal(n.codepointLength, 3);
});
});
});
Loading
Loading