diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/context-token.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/context-token.ts index bf637b15495..2ac222f32b0 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/context-token.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/context-token.ts @@ -128,7 +128,7 @@ export class ContextToken { inputStartIndex: 0, bestProbFromSet: BASE_PROBABILITY }); - searchSpace = searchSpace.addInput([{sample: transform, p: BASE_PROBABILITY}], 1); + searchSpace = new SearchQuotientSpur(searchSpace, [{sample: transform, p: BASE_PROBABILITY}], 1); }); this._searchModule = searchSpace; @@ -141,7 +141,7 @@ export class ContextToken { */ addInput(inputSource: TokenInputSource, distribution: Distribution) { this._inputRange.push(inputSource); - this._searchModule = this._searchModule.addInput(distribution, inputSource.bestProbFromSet); + this._searchModule = new SearchQuotientSpur(this._searchModule, distribution, inputSource.bestProbFromSet); } /** diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts index c563eb2f922..e5a7802dc81 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts @@ -7,8 +7,13 @@ * manage the search-space(s) for text corrections within the engine. */ +import { LexicalModelTypes } from "@keymanapp/common-types"; + import { SearchNode, SearchResult } from "./distance-modeler.js"; +import Distribution = LexicalModelTypes.Distribution; +import Transform = LexicalModelTypes.Transform; + let SPACE_ID_SEED = 0; export function generateSpaceSeed(): number { @@ -80,6 +85,14 @@ export interface SearchQuotientNode { */ readonly inputCount: number; + /** + * Retrieves the sequence of inputs that led to this SearchSpace. + * + * THIS WILL BE REMOVED SHORTLY. (Once SearchQuotientNode takes on merging & + * splitting) + */ + readonly inputSequence: Distribution[]; + /** * Determines the best example text representable by this batcher's portion of * the correction-search graph and its paths. diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts index 7c19d840154..fdaa77d8eb2 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts @@ -28,9 +28,7 @@ export const QUEUE_NODE_COMPARATOR: Comparator = function(arg1, arg2 // Whenever a wordbreak boundary is crossed, a new instance should be made. export class SearchQuotientSpur implements SearchQuotientNode { private selectionQueue: PriorityQueue = new PriorityQueue(QUEUE_NODE_COMPARATOR); - private inputs: Distribution; - - readonly rootPath: SearchQuotientSpur; + readonly inputs?: Distribution>; private parentPath: SearchQuotientSpur; readonly spaceId: number; @@ -58,11 +56,6 @@ export class SearchQuotientSpur implements SearchQuotientNode { */ private lowestCostAtDepth: number[]; - /** - * Clone constructor. Deep-copies its internal queues, but not search nodes. - * @param instance - */ - constructor(instance: SearchQuotientSpur); /** * Constructs a fresh SearchSpace instance for used in predictive-text correction * and suggestion searches. @@ -70,33 +63,31 @@ export class SearchQuotientSpur implements SearchQuotientNode { * @param model */ constructor(model: LexicalModel); - constructor(arg1: SearchQuotientSpur|LexicalModel) { + constructor(space: SearchQuotientSpur, inputs: Distribution, bestProbFromSet: number); + constructor(arg1: LexicalModel | SearchQuotientSpur, inputs?: Distribution, bestProbFromSet?: number) { this.spaceId = generateSpaceSeed(); if(arg1 instanceof SearchQuotientSpur) { - const parentSpace = arg1; - this.lowestCostAtDepth = parentSpace.lowestCostAtDepth.slice(); - this.rootPath = parentSpace.rootPath; - this.parentPath = parentSpace; + const parentNode = arg1 as SearchQuotientSpur; + const logTierCost = -Math.log(bestProbFromSet); - return; - } + this.inputs = inputs; + this.lowestCostAtDepth = parentNode.lowestCostAtDepth.concat(logTierCost); + this.parentPath = parentNode; + + this.addEdgesForNodes(parentNode.completedPaths); - const model = arg1; - if(!model.traverseFromRoot) { - throw new Error("The provided model does not implement the `traverseFromRoot` function, which is needed to support robust correction searching."); + return; } - const rootNode = new SearchNode(model.traverseFromRoot(), this.spaceId, model.toKey ? model.toKey.bind(model) : null); - this.selectionQueue.enqueue(rootNode); + const model = arg1 as LexicalModel; + this.selectionQueue.enqueue(new SearchNode(model.traverseFromRoot(), this.spaceId, t => model.toKey(t))); this.lowestCostAtDepth = []; - this.rootPath = this; - this.completedPaths = []; } /** - * Retrieves the sequence of inputs + * Retrieves the sequences of inputs that led to this SearchPath. */ public get inputSequence(): Distribution[] { if(this.parentPath) { @@ -141,46 +132,6 @@ export class SearchQuotientSpur implements SearchQuotientNode { return this.parentPath?.correctionsEnabled || this.inputs?.length > 1; } - /** - * Extends the correction-search process embodied by this SearchPath by an extra - * input character, according to the characters' likelihood in the distribution. - * @param inputDistribution The fat-finger distribution for the incoming keystroke (or - * just the raw keystroke if corrections are disabled) - */ - addInput(inputDistribution: Distribution, bestProbFromSet: number): SearchQuotientSpur { - const input = inputDistribution; - - const childSpace = new SearchQuotientSpur(this); - - childSpace.inputs = inputDistribution; - const lastDepthCost = this.lowestCostAtDepth[this.lowestCostAtDepth.length - 1] ?? 0; - const logTierCost = -Math.log(bestProbFromSet); - childSpace.lowestCostAtDepth.push(lastDepthCost + logTierCost); - - // With a newly-available input, we can extend new input-dependent paths from - // our previously-reached 'extractedResults' nodes. - let newlyAvailableEdges: SearchNode[] = []; - let batches = this.completedPaths?.map(function(node) { - let deletions = node.buildDeletionEdges(input, childSpace.spaceId); - let substitutions = node.buildSubstitutionEdges(input, childSpace.spaceId); - - // Skip the queue for the first pass; there will ALWAYS be at least one pass, - // and queue-enqueing does come with a cost. Avoid the unnecessary overhead. - return substitutions.flatMap(e => e.processSubsetEdge()).concat(deletions); - }); - - childSpace.completedPaths = []; - childSpace.returnedValues = {}; - - batches?.forEach(function(batch) { - newlyAvailableEdges = newlyAvailableEdges.concat(batch); - }); - - childSpace.selectionQueue.enqueueAll(newlyAvailableEdges); - - return childSpace; - } - public get currentCost(): number { const parentCost = this.parentPath?.currentCost ?? Number.POSITIVE_INFINITY; const localCost = this.selectionQueue.peek()?.currentCost ?? Number.POSITIVE_INFINITY; @@ -188,29 +139,26 @@ export class SearchQuotientSpur implements SearchQuotientNode { return Math.min(localCost, parentCost); } - /** - * Given an incoming SearchNode, this method will build all outgoing edges - * from the node that correspond to processing this SearchPath instance's - * input distribution. - * @param currentNode - */ - private addEdgesForNodes(currentNode: SearchNode) { - // Hard restriction: no further edits will be supported. This helps keep the search - // more narrowly focused. - const substitutionsOnly = currentNode.editCount == 2; - - let deletionEdges: SearchNode[] = []; - if(!substitutionsOnly) { - deletionEdges = currentNode.buildDeletionEdges(this.inputs, this.spaceId); - } - const substitutionEdges = currentNode.buildSubstitutionEdges(this.inputs, this.spaceId); + private addEdgesForNodes(baseNodes: ReadonlyArray) { + // With a newly-available input, we can extend new input-dependent paths from + // our previously-reached 'extractedResults' nodes. + let outboundNodes = baseNodes.map((node) => { + // Hard restriction: no further edits will be supported. This helps keep the search + // more narrowly focused. + const substitutionsOnly = node.editCount == 2; + + let deletionEdges: SearchNode[] = []; + if(!substitutionsOnly) { + deletionEdges = node.buildDeletionEdges(this.inputs, this.spaceId); + } + const substitutionEdges = node.buildSubstitutionEdges(this.inputs, this.spaceId); - // Skip the queue for the first pass; there will ALWAYS be at least one pass, - // and queue-enqueing does come with a cost - avoid unnecessary overhead here. - let batch = substitutionEdges.flatMap(e => e.processSubsetEdge()).concat(deletionEdges); + // Skip the queue for the first pass; there will ALWAYS be at least one pass, + // and queue-enqueing does come with a cost - avoid unnecessary overhead here. + return substitutionEdges.flatMap(e => e.processSubsetEdge()).concat(deletionEdges); + }).flat(); - this.selectionQueue.enqueueAll(batch); - // We didn't reach an end-node, so we just end the iteration and continue the search. + this.selectionQueue.enqueueAll(outboundNodes); } /** @@ -233,7 +181,7 @@ export class SearchQuotientSpur implements SearchQuotientNode { const result = this.parentPath.handleNextNode(); if(result.type == 'complete') { - this.addEdgesForNodes(result.finalNode); + this.addEdgesForNodes([result.finalNode]); } return { diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts index 75582eee33c..146d8bb8218 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts @@ -126,9 +126,9 @@ describe('getBestMatches', () => { {sample: {insert: 'n', deleteLeft: 0}, p: 0.25} ]; - const searchPath1 = searchPath.addInput(synthInput1, 1); - const searchPath2 = searchPath1.addInput(synthInput2, .75); - const searchPath3 = searchPath2.addInput(synthInput3, .75); + const searchPath1 = new SearchQuotientSpur(searchPath, synthInput1, 1); + const searchPath2 = new SearchQuotientSpur(searchPath1, synthInput2, .75); + const searchPath3 = new SearchQuotientSpur(searchPath2, synthInput3, .75); assert.notEqual(searchPath1.spaceId, searchPath.spaceId); assert.notEqual(searchPath2.spaceId, searchPath1.spaceId); @@ -160,9 +160,9 @@ describe('getBestMatches', () => { {sample: {insert: 'n', deleteLeft: 0}, p: 0.25} ]; - const searchPath1 = searchPath.addInput(synthInput1, 1); - const searchPath2 = searchPath1.addInput(synthInput2, .75); - const searchPath3 = searchPath2.addInput(synthInput3, .75); + const searchPath1 = new SearchQuotientSpur(searchPath, synthInput1, 1); + const searchPath2 = new SearchQuotientSpur(searchPath1, synthInput2, .75); + const searchPath3 = new SearchQuotientSpur(searchPath2, synthInput3, .75); assert.notEqual(searchPath1.spaceId, searchPath.spaceId); assert.notEqual(searchPath2.spaceId, searchPath1.spaceId);