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 @@ -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;
Expand All @@ -141,7 +141,7 @@ export class ContextToken {
*/
addInput(inputSource: TokenInputSource, distribution: Distribution<Transform>) {
this._inputRange.push(inputSource);
this._searchModule = this._searchModule.addInput(distribution, inputSource.bestProbFromSet);
this._searchModule = new SearchQuotientSpur(this._searchModule, distribution, inputSource.bestProbFromSet);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Transform>[];

/**
* Determines the best example text representable by this batcher's portion of
* the correction-search graph and its paths.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ export const QUEUE_NODE_COMPARATOR: Comparator<SearchNode> = function(arg1, arg2
// Whenever a wordbreak boundary is crossed, a new instance should be made.
export class SearchQuotientSpur implements SearchQuotientNode {
private selectionQueue: PriorityQueue<SearchNode> = new PriorityQueue(QUEUE_NODE_COMPARATOR);
private inputs: Distribution<Transform>;

readonly rootPath: SearchQuotientSpur;
readonly inputs?: Distribution<Readonly<Transform>>;

private parentPath: SearchQuotientSpur;
readonly spaceId: number;
Expand Down Expand Up @@ -58,45 +56,38 @@ 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.
* @param baseSpaceId
* @param model
*/
constructor(model: LexicalModel);
constructor(arg1: SearchQuotientSpur|LexicalModel) {
constructor(space: SearchQuotientSpur, inputs: Distribution<Transform>, bestProbFromSet: number);
constructor(arg1: LexicalModel | SearchQuotientSpur, inputs?: Distribution<Transform>, 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<Transform>[] {
if(this.parentPath) {
Expand Down Expand Up @@ -141,76 +132,33 @@ 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<Transform>, 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;

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<SearchNode>) {
// 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);
}

/**
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down