mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-20 04:47:42 +00:00
Merge pull request #15680 from keymanapp/refactor/web/root-and-legacy-spur-tests
refactor(web): improve clarity and organization of search-graph root + legacy-spur unit tests 🚂
This commit is contained in:
commit
347f7edec3
6 changed files with 513 additions and 234 deletions
|
|
@ -29,6 +29,10 @@ export class SearchQuotientRoot implements SearchQuotientNode {
|
|||
* @param model
|
||||
*/
|
||||
constructor(model: LexicalModel) {
|
||||
if(!model.traverseFromRoot) {
|
||||
throw new Error("The active lexical model does not support traversal-based searching.");
|
||||
}
|
||||
|
||||
this.rootNode = new SearchNode(model.traverseFromRoot(), generateSpaceSeed(), t => model.toKey(t));
|
||||
this.model = model;
|
||||
this.rootResult = new SearchResult(this.rootNode);
|
||||
|
|
@ -64,7 +68,7 @@ export class SearchQuotientRoot implements SearchQuotientNode {
|
|||
*/
|
||||
public handleNextNode(): PathResult {
|
||||
if(this.hasBeenProcessed) {
|
||||
return null;
|
||||
return { type: 'none' };
|
||||
}
|
||||
|
||||
this.hasBeenProcessed = true;
|
||||
|
|
@ -122,6 +126,10 @@ export class SearchQuotientRoot implements SearchQuotientNode {
|
|||
}
|
||||
|
||||
merge(space: SearchQuotientNode): SearchQuotientNode {
|
||||
if(this.model != space.model) {
|
||||
throw new Error("Cannot merge search graphs based on different LexicalModels");
|
||||
}
|
||||
|
||||
// Head node for the incoming path is empty, so skip it.
|
||||
if(space.parents.length == 0 || space instanceof SearchQuotientRoot) {
|
||||
return this;
|
||||
|
|
|
|||
|
|
@ -171,6 +171,10 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode {
|
|||
// spaces are in sequence here.
|
||||
// `this` = head 'space'.
|
||||
public merge(space: SearchQuotientNode): SearchQuotientNode {
|
||||
if(this.model != space.model) {
|
||||
throw new Error("Cannot merge search graphs based on different LexicalModels");
|
||||
}
|
||||
|
||||
// Head node for the incoming path is empty, so skip it.
|
||||
if(space.parents.length == 0 || space instanceof SearchQuotientRoot) {
|
||||
return this;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*
|
||||
* Created by jahorton on 2026-03-11
|
||||
*
|
||||
* This file defines a unit-text helper designed to test what search-graph
|
||||
* results for corrections may result from paths traversing through a given
|
||||
* SearchQuotientNode.
|
||||
*/
|
||||
|
||||
import { PathResult, SearchQuotientNode } from "@keymanapp/lm-worker/test-index";
|
||||
|
||||
/**
|
||||
* Represents the results of a call to `analyzeQuotientNodeResults` during unit
|
||||
* testing.
|
||||
*
|
||||
* All specified `expectedResults` entries will be found once - either in
|
||||
* `found` or `missing` for each. If `found`, the result _may_ also appear in
|
||||
* `foundWithDuplicates` if it is found more than once during the search.
|
||||
*/
|
||||
export interface SearchResultAnalysis {
|
||||
/**
|
||||
* Indicates specified strings (expected results) that were found via search
|
||||
* on the provided node one or more times.
|
||||
*/
|
||||
found: string[];
|
||||
|
||||
/**
|
||||
* Indicates specified strings (expected results) that were not found via
|
||||
* search on the provided node.
|
||||
*/
|
||||
missing: string[];
|
||||
|
||||
/**
|
||||
* Indicates specified strings (expected results) that were found via search
|
||||
* on the provided node more than once.
|
||||
*/
|
||||
foundWithDuplicates: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes all correction-search search targets that have a path passing
|
||||
* through the specified SearchQuotientNode in order to determine whether or not
|
||||
* valid paths exist for specified ("expected") correction strings.
|
||||
*
|
||||
* @param node The SearchQuotientNode to utilize for the search.
|
||||
* @param expectedResults A set of strings used for the test scenario. Strings
|
||||
* not found here will be ignored.
|
||||
* @returns
|
||||
*/
|
||||
export function analyzeQuotientNodeResults(
|
||||
node: SearchQuotientNode,
|
||||
expectedResults: string[]
|
||||
) {
|
||||
const matchMap: Map<string, number> = new Map();
|
||||
|
||||
let result: PathResult = node.handleNextNode();
|
||||
while(result.type != 'none') {
|
||||
if(result.type == 'complete') {
|
||||
const resultKey = result.finalNode.resultKey;
|
||||
if(expectedResults.find((entry) => entry == resultKey)) {
|
||||
matchMap.set(resultKey, (matchMap.get(resultKey) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
result = node.handleNextNode();
|
||||
}
|
||||
|
||||
const analysis: SearchResultAnalysis = {
|
||||
found: [],
|
||||
missing: [],
|
||||
foundWithDuplicates: []
|
||||
};
|
||||
|
||||
for(let key of expectedResults) {
|
||||
const matchCount = matchMap.get(key) ?? 0;
|
||||
|
||||
if(matchCount == 0) {
|
||||
analysis.missing.push(key);
|
||||
} else {
|
||||
analysis.found.push(key);
|
||||
if(matchCount > 1) {
|
||||
analysis.foundWithDuplicates.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return analysis;
|
||||
}
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*
|
||||
* Created by jahorton on 2025-10-29
|
||||
*
|
||||
* This file defines tests for the SearchQuotientSpur classes of the
|
||||
* predictive-text correction-search engine.
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs';
|
||||
import {
|
||||
generateSubsetId,
|
||||
LegacyQuotientRoot,
|
||||
LegacyQuotientSpur,
|
||||
models
|
||||
} from '@keymanapp/lm-worker/test-index';
|
||||
|
||||
import { buildCantLinearFixture } from '../../helpers/buildCantLinearFixture.js';
|
||||
import { analyzeQuotientNodeResults } from '../../helpers/analyzeQuotientNodeResults.js';
|
||||
|
||||
import TrieModel = models.TrieModel;
|
||||
|
||||
const testModel = new TrieModel(jsonFixture('models/tries/english-1000'));
|
||||
|
||||
describe('LegacyQuotientSpur', () => {
|
||||
describe('constructor', () => {
|
||||
it('initializes from a lexical model', () => {
|
||||
const path = new LegacyQuotientRoot(testModel);
|
||||
assert.equal(path.inputCount, 0);
|
||||
assert.equal(path.codepointLength, 0);
|
||||
assert.isNumber(path.spaceId);
|
||||
assert.deepEqual(path.bestExample, {text: '', p: 1});
|
||||
assert.deepEqual(path.parents, []);
|
||||
});
|
||||
|
||||
it('may be extended from root path', () => {
|
||||
const rootPath = new LegacyQuotientRoot(testModel);
|
||||
|
||||
const leadEdgeDistribution = [
|
||||
{sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5},
|
||||
{sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3},
|
||||
{sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2}
|
||||
];
|
||||
|
||||
const extendedPath = new LegacyQuotientSpur(rootPath, leadEdgeDistribution, leadEdgeDistribution[0]);
|
||||
|
||||
assert.equal(extendedPath.inputCount, 1);
|
||||
assert.equal(extendedPath.codepointLength, 1);
|
||||
assert.isNumber(extendedPath.spaceId);
|
||||
assert.notEqual(extendedPath.spaceId, rootPath.spaceId);
|
||||
assert.deepEqual(extendedPath.bestExample, {text: 't', p: 0.5});
|
||||
assert.deepEqual(extendedPath.parents, [rootPath]);
|
||||
assert.deepEqual(extendedPath.inputs, leadEdgeDistribution);
|
||||
assert.deepEqual(extendedPath.inputSegments, [
|
||||
{
|
||||
transitionId: leadEdgeDistribution[0].sample.id,
|
||||
start: 0
|
||||
}
|
||||
]);
|
||||
|
||||
// Assert the root is unchanged.
|
||||
assert.equal(rootPath.inputCount, 0);
|
||||
// Should (still) have codepointLength == 0 once it's defined.
|
||||
assert.deepEqual(rootPath.bestExample, {text: '', p: 1});
|
||||
assert.deepEqual(rootPath.parents, []);
|
||||
});
|
||||
|
||||
it('may be built from arbitrary prior SearchQuotientSpur', () => {
|
||||
const rootPath = new LegacyQuotientRoot(testModel);
|
||||
|
||||
const leadEdgeDistribution = [
|
||||
{sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5},
|
||||
{sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3},
|
||||
{sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2}
|
||||
];
|
||||
const inputClone = leadEdgeDistribution.map(e => ({...e}));
|
||||
|
||||
const length1Path = new LegacyQuotientSpur(
|
||||
rootPath,
|
||||
leadEdgeDistribution,
|
||||
leadEdgeDistribution[0]
|
||||
);
|
||||
|
||||
const tailEdgeDistribution = [
|
||||
{sample: {insert: 'r', deleteLeft: 0, id: 17 }, p: 0.6},
|
||||
{sample: {insert: 'e', deleteLeft: 0, id: 17 }, p: 0.25},
|
||||
{sample: {insert: 'h', deleteLeft: 0, id: 17 }, p: 0.15}
|
||||
];
|
||||
|
||||
const length2Path = new LegacyQuotientSpur(
|
||||
length1Path,
|
||||
tailEdgeDistribution,
|
||||
tailEdgeDistribution[0]
|
||||
);
|
||||
|
||||
// Verify that the prior distribution remains fully unaltered.
|
||||
assert.deepEqual(leadEdgeDistribution, inputClone);
|
||||
|
||||
assert.equal(length2Path.inputCount, 2);
|
||||
assert.equal(length2Path.codepointLength, 2);
|
||||
assert.isNumber(length2Path.spaceId);
|
||||
assert.notEqual(length2Path.spaceId, length1Path.spaceId);
|
||||
assert.deepEqual(length2Path.bestExample, {text: 'tr', p: leadEdgeDistribution[0].p * tailEdgeDistribution[0].p});
|
||||
assert.deepEqual(length2Path.parents, [length1Path]);
|
||||
assert.deepEqual(length2Path.inputs, tailEdgeDistribution);
|
||||
assert.deepEqual(length2Path.inputSegments, [
|
||||
{
|
||||
transitionId: leadEdgeDistribution[0].sample.id,
|
||||
start: 0
|
||||
}, {
|
||||
transitionId: tailEdgeDistribution[0].sample.id,
|
||||
start: 0
|
||||
}
|
||||
]);
|
||||
|
||||
assert.equal(length1Path.inputCount, 1);
|
||||
assert.equal(length1Path.codepointLength, 1);
|
||||
assert.isNumber(length1Path.spaceId);
|
||||
assert.notEqual(length1Path.spaceId, rootPath.spaceId);
|
||||
assert.deepEqual(length1Path.bestExample, {text: 't', p: 0.5});
|
||||
assert.deepEqual(length1Path.parents, [rootPath]);
|
||||
assert.deepEqual(length1Path.inputs, leadEdgeDistribution);
|
||||
});
|
||||
|
||||
it('throws if input and input-source transition IDs mismatch', () => {
|
||||
const rootPath = new LegacyQuotientRoot(testModel);
|
||||
|
||||
const leadEdgeDistribution = [
|
||||
{sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5},
|
||||
{sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3},
|
||||
{sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2}
|
||||
];
|
||||
|
||||
assert.throws(() => new LegacyQuotientSpur(rootPath, leadEdgeDistribution, {
|
||||
...leadEdgeDistribution[0],
|
||||
sample: {...leadEdgeDistribution[0].sample, id: 15}
|
||||
}));
|
||||
});
|
||||
|
||||
it('may extend with a Transform inserting multiple codepoints', () => {
|
||||
const rootPath = new LegacyQuotientRoot(testModel);
|
||||
|
||||
const leadEdgeDistribution = [
|
||||
{sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5},
|
||||
{sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3},
|
||||
{sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2}
|
||||
];
|
||||
const inputClone = leadEdgeDistribution.map(e => ({...e}));
|
||||
|
||||
const length1Path = new LegacyQuotientSpur(
|
||||
rootPath,
|
||||
leadEdgeDistribution,
|
||||
leadEdgeDistribution[0]
|
||||
);
|
||||
|
||||
const tailEdgeDistribution = [
|
||||
{sample: {insert: 'ri', deleteLeft: 0, id: 17 }, p: 0.6},
|
||||
{sample: {insert: 'er', deleteLeft: 0, id: 17 }, p: 0.25},
|
||||
{sample: {insert: 'hi', deleteLeft: 0, id: 17 }, p: 0.15}
|
||||
];
|
||||
|
||||
const length2Path = new LegacyQuotientSpur(
|
||||
length1Path,
|
||||
tailEdgeDistribution,
|
||||
tailEdgeDistribution[0]
|
||||
);
|
||||
|
||||
// Verify that the prior distribution remains fully unaltered.
|
||||
assert.deepEqual(leadEdgeDistribution, inputClone);
|
||||
|
||||
assert.equal(length2Path.inputCount, 2);
|
||||
assert.equal(length2Path.codepointLength, 3);
|
||||
assert.isNumber(length2Path.spaceId);
|
||||
assert.notEqual(length2Path.spaceId, length1Path.spaceId);
|
||||
assert.deepEqual(length2Path.bestExample, {text: 'tri', p: leadEdgeDistribution[0].p * tailEdgeDistribution[0].p});
|
||||
assert.deepEqual(length2Path.parents, [length1Path]);
|
||||
assert.deepEqual(length2Path.inputs, tailEdgeDistribution);
|
||||
assert.deepEqual(length2Path.inputSegments, [
|
||||
{
|
||||
transitionId: leadEdgeDistribution[0].sample.id,
|
||||
start: 0
|
||||
}, {
|
||||
transitionId: tailEdgeDistribution[0].sample.id,
|
||||
start: 0
|
||||
}
|
||||
]);
|
||||
|
||||
assert.equal(length1Path.inputCount, 1);
|
||||
assert.equal(length1Path.codepointLength, 1);
|
||||
assert.isNumber(length1Path.spaceId);
|
||||
assert.notEqual(length1Path.spaceId, rootPath.spaceId);
|
||||
assert.deepEqual(length1Path.bestExample, {text: 't', p: 0.5});
|
||||
assert.deepEqual(length1Path.parents, [rootPath]);
|
||||
assert.deepEqual(length1Path.inputs, leadEdgeDistribution);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.edgeKey', () => {
|
||||
it('changes when input source subset IDs differ', () => {
|
||||
const root = new LegacyQuotientRoot(testModel);
|
||||
|
||||
const {distributions} = buildCantLinearFixture();
|
||||
const inputSrc = {
|
||||
segment: {
|
||||
transitionId: distributions[0][0].sample.id,
|
||||
start: 0
|
||||
},
|
||||
subsetId: generateSubsetId(),
|
||||
bestProbFromSet: distributions[0][0].p
|
||||
};
|
||||
|
||||
const spur1 = new LegacyQuotientSpur(root, distributions[0], {
|
||||
...inputSrc,
|
||||
subsetId: generateSubsetId()
|
||||
});
|
||||
const spur2 = new LegacyQuotientSpur(root, distributions[0], {
|
||||
...inputSrc,
|
||||
subsetId: generateSubsetId()
|
||||
});
|
||||
|
||||
assert.notEqual(spur1.edgeKey, spur2.edgeKey);
|
||||
});
|
||||
|
||||
it('changes when different parts of the same input source are used', () => {
|
||||
const root = new LegacyQuotientRoot(testModel);
|
||||
|
||||
const {distributions} = buildCantLinearFixture();
|
||||
const inputSrc = {
|
||||
segment: {
|
||||
transitionId: distributions[0][0].sample.id,
|
||||
start: 0
|
||||
},
|
||||
subsetId: generateSubsetId(),
|
||||
bestProbFromSet: distributions[0][0].p
|
||||
};
|
||||
|
||||
const spur1 = new LegacyQuotientSpur(root, distributions[0], inputSrc);
|
||||
const spur2 = new LegacyQuotientSpur(root, distributions[0], {
|
||||
...inputSrc,
|
||||
segment: {
|
||||
...inputSrc.segment,
|
||||
end: 1
|
||||
}
|
||||
});
|
||||
const spur3 = new LegacyQuotientSpur(root, distributions[0], {
|
||||
...inputSrc,
|
||||
segment: {
|
||||
...inputSrc.segment,
|
||||
start: inputSrc.segment.start + 1
|
||||
}
|
||||
});
|
||||
|
||||
assert.notEqual(spur1.edgeKey, spur2.edgeKey);
|
||||
assert.notEqual(spur2.edgeKey, spur3.edgeKey);
|
||||
assert.notEqual(spur3.edgeKey, spur1.edgeKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleNextNode()', () => {
|
||||
it('outputs results that directly match inputs', () => {
|
||||
const canPath = buildCantLinearFixture().paths[3];
|
||||
|
||||
const matchTargets = [
|
||||
'can',
|
||||
'car',
|
||||
'cen', // 'cent' and 'center' are supported in this test model.
|
||||
];
|
||||
const analysis = analyzeQuotientNodeResults(canPath, matchTargets);
|
||||
|
||||
assert.sameMembers(analysis.found, matchTargets);
|
||||
assert.isEmpty(analysis.foundWithDuplicates);
|
||||
});
|
||||
|
||||
it('outputs results that substitute inputs', () => {
|
||||
const canPath = buildCantLinearFixture().paths[3];
|
||||
|
||||
const matchTargets = [
|
||||
// Replacement of first char
|
||||
'man',
|
||||
'far',
|
||||
// Replacement of second char
|
||||
'con', // 'consider' and variants thereof are also supported.
|
||||
'cor', // 'corner'
|
||||
// Replacement of third char
|
||||
'cal', // 'call'
|
||||
];
|
||||
const analysis = analyzeQuotientNodeResults(canPath, matchTargets);
|
||||
|
||||
assert.sameMembers(analysis.found, matchTargets);
|
||||
assert.isEmpty(analysis.foundWithDuplicates);
|
||||
});
|
||||
|
||||
it('outputs results that insert characters as needed', () => {
|
||||
const canPath = buildCantLinearFixture().paths[3];
|
||||
|
||||
const matchTargets = [
|
||||
'char', // 'c' + (insert) 'h' + 'ar'
|
||||
'than', // 't' + (insert) 'h' + 'an'
|
||||
];
|
||||
const analysis = analyzeQuotientNodeResults(canPath, matchTargets);
|
||||
|
||||
assert.sameMembers(analysis.found, matchTargets);
|
||||
assert.isEmpty(analysis.foundWithDuplicates);
|
||||
});
|
||||
|
||||
it('outputs results that delete incoming keystrokes as needed', () => {
|
||||
const canPath = buildCantLinearFixture().paths[3];
|
||||
|
||||
const matchTargets = [
|
||||
// Delete only first
|
||||
'an', // (delete) 'c'/'r'/'t' + 'an', for 'and' and 'any'
|
||||
'en', // (delete) 'c'/'r'/'t' + 'en', for 'end',
|
||||
// Even delete second
|
||||
'n', // model possesses words starting with just 'n'
|
||||
't' // ... and 't'.
|
||||
];
|
||||
const analysis = analyzeQuotientNodeResults(canPath, matchTargets);
|
||||
|
||||
assert.sameMembers(analysis.found, matchTargets);
|
||||
assert.isEmpty(analysis.foundWithDuplicates);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*
|
||||
* Created by jahorton on 2025-03-04
|
||||
*
|
||||
* This file defines tests for the SearchQuotientRoot class of the
|
||||
* predictive-text correction-search engine.
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs';
|
||||
import {
|
||||
models,
|
||||
SearchQuotientRoot,
|
||||
} from '@keymanapp/lm-worker/test-index';
|
||||
|
||||
import { constituentPaths } from '../../helpers/constituentPaths.js';
|
||||
|
||||
import DummyModel = models.DummyModel;
|
||||
import TrieModel = models.TrieModel;
|
||||
|
||||
const testModel = new TrieModel(jsonFixture('models/tries/english-1000'));
|
||||
const altModel = new TrieModel(jsonFixture('models/tries/accented'));
|
||||
|
||||
describe('SearchQuotientRoot', () => {
|
||||
describe('constructor', () => {
|
||||
it('initializes from a lexical model', () => {
|
||||
const path = new SearchQuotientRoot(testModel);
|
||||
assert.equal(path.inputCount, 0);
|
||||
assert.equal(path.codepointLength, 0);
|
||||
assert.isNumber(path.spaceId);
|
||||
assert.deepEqual(path.bestExample, {text: '', p: 1});
|
||||
assert.deepEqual(path.parents, []);
|
||||
});
|
||||
|
||||
it('throws an error for lexical models that do not support traversal-based searching', () => {
|
||||
assert.throws(() => new SearchQuotientRoot(new DummyModel({})));
|
||||
});
|
||||
});
|
||||
|
||||
it('constituentPaths returns an empty array', () => {
|
||||
const path = new SearchQuotientRoot(testModel);
|
||||
assert.deepEqual(constituentPaths(path), []);
|
||||
});
|
||||
|
||||
it('.isSameNode returns true for separate instance on same model', () => {
|
||||
const root1 = new SearchQuotientRoot(testModel);
|
||||
const root2 = new SearchQuotientRoot(testModel);
|
||||
|
||||
assert.isTrue(root1.isSameNode(root2));
|
||||
});
|
||||
|
||||
it('split() results in two separate root instances', () => {
|
||||
const root = new SearchQuotientRoot(testModel);
|
||||
|
||||
const splitResults = root.split(0);
|
||||
|
||||
// Only one way to split it...
|
||||
assert.equal(splitResults.length, 1);
|
||||
|
||||
const [head, tail] = splitResults[0];
|
||||
assert.notStrictEqual(head, tail);
|
||||
assert.isTrue(head.isSameNode(tail));
|
||||
});
|
||||
|
||||
describe('merge()', () => {
|
||||
it('treats two copies of same-model root as same node', () => {
|
||||
const head = new SearchQuotientRoot(testModel);
|
||||
const tail = new SearchQuotientRoot(testModel);
|
||||
|
||||
const merged = head.merge(tail);
|
||||
|
||||
assert.isTrue(head.isSameNode(tail));
|
||||
assert.isTrue(merged.isSameNode(head));
|
||||
assert.isTrue(merged.isSameNode(tail));
|
||||
});
|
||||
|
||||
it('rejects roots from different models', () => {
|
||||
const head = new SearchQuotientRoot(testModel);
|
||||
const tail = new SearchQuotientRoot(altModel);
|
||||
|
||||
assert.throws(() => head.merge(tail));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -63,239 +63,6 @@ function toMathematicalSMP(text: string) {
|
|||
}
|
||||
|
||||
describe('SearchQuotientSpur', () => {
|
||||
describe('constructor', () => {
|
||||
it('initializes from a lexical model', () => {
|
||||
const path = new LegacyQuotientRoot(testModel);
|
||||
assert.equal(path.inputCount, 0);
|
||||
assert.equal(path.codepointLength, 0);
|
||||
assert.isNumber(path.spaceId);
|
||||
assert.deepEqual(path.bestExample, {text: '', p: 1});
|
||||
assert.deepEqual(path.parents, []);
|
||||
});
|
||||
|
||||
it('may be extended from root path', () => {
|
||||
const rootPath = new LegacyQuotientRoot(testModel);
|
||||
|
||||
const leadEdgeDistribution = [
|
||||
{sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5},
|
||||
{sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3},
|
||||
{sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2}
|
||||
];
|
||||
|
||||
const extendedPath = new LegacyQuotientSpur(rootPath, leadEdgeDistribution, leadEdgeDistribution[0]);
|
||||
|
||||
assert.equal(extendedPath.inputCount, 1);
|
||||
assert.equal(extendedPath.codepointLength, 1);
|
||||
assert.isNumber(extendedPath.spaceId);
|
||||
assert.notEqual(extendedPath.spaceId, rootPath.spaceId);
|
||||
assert.deepEqual(extendedPath.bestExample, {text: 't', p: 0.5});
|
||||
assert.deepEqual(extendedPath.parents, [rootPath]);
|
||||
assert.deepEqual(extendedPath.inputs, leadEdgeDistribution);
|
||||
assert.deepEqual(extendedPath.inputSegments, [
|
||||
{
|
||||
transitionId: leadEdgeDistribution[0].sample.id,
|
||||
start: 0
|
||||
}
|
||||
]);
|
||||
|
||||
// Assert the root is unchanged.
|
||||
assert.equal(rootPath.inputCount, 0);
|
||||
// Should (still) have codepointLength == 0 once it's defined.
|
||||
assert.deepEqual(rootPath.bestExample, {text: '', p: 1});
|
||||
assert.deepEqual(rootPath.parents, []);
|
||||
});
|
||||
|
||||
it('may be built from arbitrary prior SearchQuotientSpur', () => {
|
||||
const rootPath = new LegacyQuotientRoot(testModel);
|
||||
|
||||
const leadEdgeDistribution = [
|
||||
{sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5},
|
||||
{sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3},
|
||||
{sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2}
|
||||
];
|
||||
const inputClone = leadEdgeDistribution.map(e => ({...e}));
|
||||
|
||||
const length1Path = new LegacyQuotientSpur(
|
||||
rootPath,
|
||||
leadEdgeDistribution,
|
||||
leadEdgeDistribution[0]
|
||||
);
|
||||
|
||||
const tailEdgeDistribution = [
|
||||
{sample: {insert: 'r', deleteLeft: 0, id: 17 }, p: 0.6},
|
||||
{sample: {insert: 'e', deleteLeft: 0, id: 17 }, p: 0.25},
|
||||
{sample: {insert: 'h', deleteLeft: 0, id: 17 }, p: 0.15}
|
||||
];
|
||||
|
||||
const length2Path = new LegacyQuotientSpur(
|
||||
length1Path,
|
||||
tailEdgeDistribution,
|
||||
tailEdgeDistribution[0]
|
||||
);
|
||||
|
||||
// Verify that the prior distribution remains fully unaltered.
|
||||
assert.deepEqual(leadEdgeDistribution, inputClone);
|
||||
|
||||
assert.equal(length2Path.inputCount, 2);
|
||||
assert.equal(length2Path.codepointLength, 2);
|
||||
assert.isNumber(length2Path.spaceId);
|
||||
assert.notEqual(length2Path.spaceId, length1Path.spaceId);
|
||||
assert.deepEqual(length2Path.bestExample, {text: 'tr', p: leadEdgeDistribution[0].p * tailEdgeDistribution[0].p});
|
||||
assert.deepEqual(length2Path.parents, [length1Path]);
|
||||
assert.deepEqual(length2Path.inputs, tailEdgeDistribution);
|
||||
assert.deepEqual(length2Path.inputSegments, [
|
||||
{
|
||||
transitionId: leadEdgeDistribution[0].sample.id,
|
||||
start: 0
|
||||
}, {
|
||||
transitionId: tailEdgeDistribution[0].sample.id,
|
||||
start: 0
|
||||
}
|
||||
]);
|
||||
|
||||
assert.equal(length1Path.inputCount, 1);
|
||||
assert.equal(length1Path.codepointLength, 1);
|
||||
assert.isNumber(length1Path.spaceId);
|
||||
assert.notEqual(length1Path.spaceId, rootPath.spaceId);
|
||||
assert.deepEqual(length1Path.bestExample, {text: 't', p: 0.5});
|
||||
assert.deepEqual(length1Path.parents, [rootPath]);
|
||||
assert.deepEqual(length1Path.inputs, leadEdgeDistribution);
|
||||
});
|
||||
|
||||
it('throws if input and input-source transition IDs mismatch', () => {
|
||||
const rootPath = new LegacyQuotientRoot(testModel);
|
||||
|
||||
const leadEdgeDistribution = [
|
||||
{sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5},
|
||||
{sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3},
|
||||
{sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2}
|
||||
];
|
||||
|
||||
assert.throws(() => new LegacyQuotientSpur(rootPath, leadEdgeDistribution, {
|
||||
...leadEdgeDistribution[0],
|
||||
sample: {...leadEdgeDistribution[0].sample, id: 15}
|
||||
}));
|
||||
});
|
||||
|
||||
it('may extend with a Transform inserting multiple codepoints', () => {
|
||||
const rootPath = new LegacyQuotientRoot(testModel);
|
||||
|
||||
const leadEdgeDistribution = [
|
||||
{sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5},
|
||||
{sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3},
|
||||
{sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2}
|
||||
];
|
||||
const inputClone = leadEdgeDistribution.map(e => ({...e}));
|
||||
|
||||
const length1Path = new LegacyQuotientSpur(
|
||||
rootPath,
|
||||
leadEdgeDistribution,
|
||||
leadEdgeDistribution[0]
|
||||
);
|
||||
|
||||
const tailEdgeDistribution = [
|
||||
{sample: {insert: 'ri', deleteLeft: 0, id: 17 }, p: 0.6},
|
||||
{sample: {insert: 'er', deleteLeft: 0, id: 17 }, p: 0.25},
|
||||
{sample: {insert: 'hi', deleteLeft: 0, id: 17 }, p: 0.15}
|
||||
];
|
||||
|
||||
const length2Path = new LegacyQuotientSpur(
|
||||
length1Path,
|
||||
tailEdgeDistribution,
|
||||
tailEdgeDistribution[0]
|
||||
);
|
||||
|
||||
// Verify that the prior distribution remains fully unaltered.
|
||||
assert.deepEqual(leadEdgeDistribution, inputClone);
|
||||
|
||||
assert.equal(length2Path.inputCount, 2);
|
||||
assert.equal(length2Path.codepointLength, 3);
|
||||
assert.isNumber(length2Path.spaceId);
|
||||
assert.notEqual(length2Path.spaceId, length1Path.spaceId);
|
||||
assert.deepEqual(length2Path.bestExample, {text: 'tri', p: leadEdgeDistribution[0].p * tailEdgeDistribution[0].p});
|
||||
assert.deepEqual(length2Path.parents, [length1Path]);
|
||||
assert.deepEqual(length2Path.inputs, tailEdgeDistribution);
|
||||
assert.deepEqual(length2Path.inputSegments, [
|
||||
{
|
||||
transitionId: leadEdgeDistribution[0].sample.id,
|
||||
start: 0
|
||||
}, {
|
||||
transitionId: tailEdgeDistribution[0].sample.id,
|
||||
start: 0
|
||||
}
|
||||
]);
|
||||
|
||||
assert.equal(length1Path.inputCount, 1);
|
||||
assert.equal(length1Path.codepointLength, 1);
|
||||
assert.isNumber(length1Path.spaceId);
|
||||
assert.notEqual(length1Path.spaceId, rootPath.spaceId);
|
||||
assert.deepEqual(length1Path.bestExample, {text: 't', p: 0.5});
|
||||
assert.deepEqual(length1Path.parents, [rootPath]);
|
||||
assert.deepEqual(length1Path.inputs, leadEdgeDistribution);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.edgeKey', () => {
|
||||
it('changes when input source subset IDs differ', () => {
|
||||
const root = new LegacyQuotientRoot(testModel);
|
||||
|
||||
const {distributions} = buildCantLinearFixture();
|
||||
const inputSrc = {
|
||||
segment: {
|
||||
transitionId: distributions[0][0].sample.id,
|
||||
start: 0
|
||||
},
|
||||
subsetId: generateSubsetId(),
|
||||
bestProbFromSet: distributions[0][0].p
|
||||
};
|
||||
|
||||
const spur1 = new LegacyQuotientSpur(root, distributions[0], {
|
||||
...inputSrc,
|
||||
subsetId: generateSubsetId()
|
||||
});
|
||||
const spur2 = new LegacyQuotientSpur(root, distributions[0], {
|
||||
...inputSrc,
|
||||
subsetId: generateSubsetId()
|
||||
});
|
||||
|
||||
assert.notEqual(spur1.edgeKey, spur2.edgeKey);
|
||||
});
|
||||
|
||||
it('changes when different parts of the same input source are used', () => {
|
||||
const root = new LegacyQuotientRoot(testModel);
|
||||
|
||||
const {distributions} = buildCantLinearFixture();
|
||||
const inputSrc = {
|
||||
segment: {
|
||||
transitionId: distributions[0][0].sample.id,
|
||||
start: 0
|
||||
},
|
||||
subsetId: generateSubsetId(),
|
||||
bestProbFromSet: distributions[0][0].p
|
||||
};
|
||||
|
||||
const spur1 = new LegacyQuotientSpur(root, distributions[0], inputSrc);
|
||||
const spur2 = new LegacyQuotientSpur(root, distributions[0], {
|
||||
...inputSrc,
|
||||
segment: {
|
||||
...inputSrc.segment,
|
||||
end: 1
|
||||
}
|
||||
});
|
||||
const spur3 = new LegacyQuotientSpur(root, distributions[0], {
|
||||
...inputSrc,
|
||||
segment: {
|
||||
...inputSrc.segment,
|
||||
start: inputSrc.segment.start + 1
|
||||
}
|
||||
});
|
||||
|
||||
assert.notEqual(spur1.edgeKey, spur2.edgeKey);
|
||||
assert.notEqual(spur2.edgeKey, spur3.edgeKey);
|
||||
assert.notEqual(spur3.edgeKey, spur1.edgeKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('split()', () => {
|
||||
describe(`on token comprised of single-char transforms: [crt][ae][nr][t]`, () => {
|
||||
const runSplit = (splitIndex: number) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue