trajectoryClassifier.ts 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  1. import { DeepImmutable, Nullable } from "../types";
  2. import { Matrix, Vector3 } from "../Maths/math.vector";
  3. // This implementation was based on the original MIT-licensed TRACE repository
  4. // from https://github.com/septagon/TRACE.
  5. /**
  6. * Generic implementation of Levenshtein distance.
  7. */
  8. namespace Levenshtein {
  9. /**
  10. * Alphabet from which to construct sequences to be compared using Levenshtein
  11. * distance.
  12. */
  13. export class Alphabet<T> {
  14. private _characterToIdx: Map<T, number>;
  15. private _insertionCosts: number[];
  16. private _deletionCosts: number[];
  17. private _substitutionCosts: number[][];
  18. /**
  19. * Serialize the Alphabet to JSON string.
  20. * @returns JSON serialization
  21. */
  22. public serialize(): string {
  23. let jsonObject: any = {};
  24. let characters = new Array<T>(this._characterToIdx.size);
  25. this._characterToIdx.forEach((v, k) => {
  26. characters[v] = k;
  27. });
  28. jsonObject["characters"] = characters;
  29. jsonObject["insertionCosts"] = this._insertionCosts;
  30. jsonObject["deletionCosts"] = this._deletionCosts;
  31. jsonObject["substitutionCosts"] = this._substitutionCosts;
  32. return JSON.stringify(jsonObject);
  33. }
  34. /**
  35. * Parse an Alphabet from a JSON serialization.
  36. * @param json JSON string to deserialize
  37. * @returns deserialized Alphabet
  38. */
  39. public static Deserialize<T>(json: string): Alphabet<T> {
  40. let jsonObject = JSON.parse(json);
  41. let alphabet = new Alphabet(jsonObject["characters"] as T[]);
  42. alphabet._insertionCosts = jsonObject["insertionCosts"];
  43. alphabet._deletionCosts = jsonObject["deletionCosts"];
  44. alphabet._substitutionCosts = jsonObject["substitutionCosts"];
  45. return alphabet;
  46. }
  47. /**
  48. * Create a new Alphabet.
  49. * @param characters characters of the alphabet
  50. * @param charToInsertionCost function mapping characters to insertion costs
  51. * @param charToDeletionCost function mapping characters to deletion costs
  52. * @param charsToSubstitutionCost function mapping character pairs to substitution costs
  53. */
  54. public constructor(
  55. characters: Array<T>,
  56. charToInsertionCost: Nullable<(char: T) => number> = null,
  57. charToDeletionCost: Nullable<(char: T) => number> = null,
  58. charsToSubstitutionCost: Nullable<(outChar: T, inChar: T) => number> = null) {
  59. charToInsertionCost = charToInsertionCost ?? (() => 1);
  60. charToDeletionCost = charToDeletionCost ?? (() => 1);
  61. charsToSubstitutionCost = charsToSubstitutionCost ?? ((a: T, b: T) => a === b ? 0 : 1);
  62. this._characterToIdx = new Map<T, number>();
  63. this._insertionCosts = new Array<number>(characters.length);
  64. this._deletionCosts = new Array<number>(characters.length);
  65. this._substitutionCosts = new Array<Array<number>>(characters.length);
  66. let c: T;
  67. for (let outerIdx = 0; outerIdx < characters.length; ++outerIdx) {
  68. c = characters[outerIdx];
  69. this._characterToIdx.set(c, outerIdx);
  70. this._insertionCosts[outerIdx] = charToInsertionCost(c);
  71. this._deletionCosts[outerIdx] = charToDeletionCost(c);
  72. this._substitutionCosts[outerIdx] = new Array<number>(characters.length);
  73. for (let innerIdx = outerIdx; innerIdx < characters.length; ++innerIdx) {
  74. this._substitutionCosts[outerIdx][innerIdx] = charsToSubstitutionCost(c, characters[innerIdx]);
  75. }
  76. }
  77. }
  78. /**
  79. * Get the index (internally-assigned number) for a character.
  80. * @param char character
  81. * @returns index
  82. */
  83. public getCharacterIdx(char: T): number {
  84. return this._characterToIdx.get(char)!;
  85. }
  86. /**
  87. * Get the insertion cost of a character from its index.
  88. * @param idx character index
  89. * @returns insertion cost
  90. */
  91. public getInsertionCost(idx: number): number {
  92. return this._insertionCosts[idx];
  93. }
  94. /**
  95. * Get the deletion cost of a character from its index.
  96. * @param idx character index
  97. * @returns deletion cost
  98. */
  99. public getDeletionCost(idx: number): number {
  100. return this._deletionCosts[idx];
  101. }
  102. /**
  103. * Gets the cost to substitute two characters. NOTE: this cost is
  104. * required to be bi-directional, meaning it cannot matter which of
  105. * the provided characters is being removed and which is being inserted.
  106. * @param idx1 the first character index
  107. * @param idx2 the second character index
  108. * @returns substitution cost
  109. */
  110. public getSubstitutionCost(idx1: number, idx2: number): number {
  111. let min = Math.min(idx1, idx2);
  112. let max = Math.max(idx1, idx2);
  113. return this._substitutionCosts[min][max];
  114. }
  115. }
  116. /**
  117. * Character sequence intended to be compared against other Sequences created
  118. * with the same Alphabet in order to compute Levenshtein distance.
  119. */
  120. export class Sequence<T> {
  121. private _alphabet: Alphabet<T>;
  122. private _characters: number[];
  123. // Scratch values
  124. private static readonly MAX_SEQUENCE_LENGTH = 256;
  125. private static _costMatrix =
  126. [...Array(Sequence.MAX_SEQUENCE_LENGTH + 1)].map((n) => new Array<number>(Sequence.MAX_SEQUENCE_LENGTH + 1));
  127. private static _insertionCost: number;
  128. private static _deletionCost: number;
  129. private static _substitutionCost: number;
  130. /**
  131. * Serialize to JSON string. JSON representation does NOT include the Alphabet
  132. * from which this Sequence was created; Alphabet must be independently
  133. * serialized.
  134. * @returns JSON string
  135. */
  136. public serialize(): string {
  137. return JSON.stringify(this._characters);
  138. }
  139. /**
  140. * Deserialize from JSON string and Alphabet. This should be the same Alphabet
  141. * from which the Sequence was originally created, which must be serialized and
  142. * deserialized independently so that it can be passed in here.
  143. * @param json JSON string representation of Sequence
  144. * @param alphabet Alphabet from which Sequence was originally created
  145. * @returns Sequence
  146. */
  147. public static Deserialize<T>(json: string, alphabet: Alphabet<T>): Sequence<T> {
  148. let sequence = new Sequence([], alphabet);
  149. sequence._characters = JSON.parse(json);
  150. return sequence;
  151. }
  152. /**
  153. * Create a new Sequence.
  154. * @param characters characters in the new Sequence
  155. * @param alphabet Alphabet, which must include all used characters
  156. */
  157. public constructor(characters: T[], alphabet: Alphabet<T>) {
  158. if (characters.length > Sequence.MAX_SEQUENCE_LENGTH) {
  159. throw new Error("Sequences longer than " + Sequence.MAX_SEQUENCE_LENGTH + " not supported.");
  160. }
  161. this._alphabet = alphabet;
  162. this._characters = characters.map((c) => this._alphabet.getCharacterIdx(c));
  163. }
  164. /**
  165. * Get the distance between this Sequence and another.
  166. * @param other sequence to compare to
  167. * @returns Levenshtein distance
  168. */
  169. public distance(other: Sequence<T>): number {
  170. return Sequence._distance<T>(this, other);
  171. }
  172. /**
  173. * Compute the Levenshtein distance between two Sequences.
  174. * @param a first Sequence
  175. * @param b second Sequence
  176. * @returns Levenshtein distance
  177. */
  178. private static _distance<T>(a: Sequence<T>, b: Sequence<T>): number {
  179. const alphabet = a._alphabet;
  180. if (alphabet !== b._alphabet) {
  181. throw new Error("Cannot Levenshtein compare Sequences built from different alphabets.");
  182. }
  183. const aChars = a._characters;
  184. const bChars = b._characters;
  185. const aLength = aChars.length;
  186. const bLength = bChars.length;
  187. let costMatrix = Sequence._costMatrix;
  188. costMatrix[0][0] = 0;
  189. for (let idx = 0; idx < aLength; ++idx) {
  190. costMatrix[idx + 1][0] = costMatrix[idx][0] + alphabet.getInsertionCost(aChars[idx]);
  191. }
  192. for (let idx = 0; idx < bLength; ++idx) {
  193. costMatrix[0][idx + 1] = costMatrix[0][idx] + alphabet.getInsertionCost(bChars[idx]);
  194. }
  195. for (let aIdx = 0; aIdx < aLength; ++aIdx) {
  196. for (let bIdx = 0; bIdx < bLength; ++bIdx) {
  197. Sequence._insertionCost = costMatrix[aIdx + 1][bIdx] + alphabet.getInsertionCost(bChars[bIdx]);
  198. Sequence._deletionCost = costMatrix[aIdx][bIdx + 1] + alphabet.getDeletionCost(aChars[aIdx]);
  199. Sequence._substitutionCost = costMatrix[aIdx][bIdx] + alphabet.getSubstitutionCost(aChars[aIdx], bChars[bIdx]);
  200. costMatrix[aIdx + 1][bIdx + 1] = Math.min(
  201. Sequence._insertionCost,
  202. Sequence._deletionCost,
  203. Sequence._substitutionCost);
  204. }
  205. }
  206. return costMatrix[aLength][bLength];
  207. }
  208. }
  209. }
  210. /**
  211. * A 3D trajectory consisting of an order list of vectors describing a
  212. * path of motion through 3D space.
  213. */
  214. export class Trajectory {
  215. private _points: Vector3[];
  216. private readonly _segmentLength: number;
  217. /**
  218. * Serialize to JSON.
  219. * @returns serialized JSON string
  220. */
  221. public serialize(): string {
  222. return JSON.stringify(this);
  223. }
  224. /**
  225. * Deserialize from JSON.
  226. * @param json serialized JSON string
  227. * @returns deserialized Trajectory
  228. */
  229. public static Deserialize(json: string): Trajectory {
  230. let jsonObject = JSON.parse(json);
  231. let trajectory = new Trajectory(jsonObject["_segmentLength"]);
  232. trajectory._points = jsonObject["_points"].map((pt: any) => {
  233. return new Vector3(pt["_x"], pt["_y"], pt["_z"]);
  234. });
  235. return trajectory;
  236. }
  237. /**
  238. * Create a new empty Trajectory.
  239. * @param segmentLength radius of discretization for Trajectory points
  240. */
  241. public constructor(segmentLength: number = 0.01) {
  242. this._points = [];
  243. this._segmentLength = segmentLength;
  244. }
  245. /**
  246. * Get the length of the Trajectory.
  247. * @returns length of the Trajectory
  248. */
  249. public getLength(): number {
  250. return this._points.length * this._segmentLength;
  251. }
  252. /**
  253. * Append a new point to the Trajectory.
  254. * NOTE: This implementation has many allocations.
  255. * @param point point to append to the Trajectory
  256. */
  257. public add(point: DeepImmutable<Vector3>): void {
  258. let numPoints = this._points.length;
  259. if (numPoints === 0) {
  260. this._points.push(point.clone());
  261. } else {
  262. const getT = () =>
  263. this._segmentLength / Vector3.Distance(this._points[numPoints - 1], point);
  264. for (let t = getT(); t <= 1.0; t = getT()) {
  265. let newPoint = this._points[numPoints - 1].scale(1.0 - t);
  266. point.scaleAndAddToRef(t, newPoint);
  267. this._points.push(newPoint);
  268. ++numPoints;
  269. }
  270. }
  271. }
  272. /**
  273. * Create a new Trajectory with a segment length chosen to make it
  274. * probable that the new Trajectory will have a specified number of
  275. * segments. This operation is imprecise.
  276. * @param targetResolution number of segments desired
  277. * @returns new Trajectory with approximately the requested number of segments
  278. */
  279. public resampleAtTargetResolution(targetResolution: number): Trajectory {
  280. var resampled = new Trajectory(this.getLength() / targetResolution);
  281. this._points.forEach((pt) => {
  282. resampled.add(pt);
  283. });
  284. return resampled;
  285. }
  286. /**
  287. * Convert Trajectory segments into tokenized representation. This
  288. * representation is an array of numbers where each nth number is the
  289. * index of the token which is most similar to the nth segment of the
  290. * Trajectory.
  291. * @param tokens list of vectors which serve as discrete tokens
  292. * @returns list of indices of most similar token per segment
  293. */
  294. public tokenize(tokens: DeepImmutable<Vector3[]>): number[] {
  295. let tokenization: number[] = [];
  296. let segmentDir = new Vector3();
  297. for (let idx = 2; idx < this._points.length; ++idx) {
  298. if (Trajectory._transformSegmentDirToRef(
  299. this._points[idx - 2],
  300. this._points[idx - 1],
  301. this._points[idx],
  302. segmentDir)) {
  303. tokenization.push(Trajectory._tokenizeSegment(segmentDir, tokens));
  304. }
  305. }
  306. return tokenization;
  307. }
  308. private static _forwardDir = new Vector3();
  309. private static _inverseFromVec = new Vector3();
  310. private static _upDir = new Vector3();
  311. private static _fromToVec = new Vector3();
  312. private static _lookMatrix = new Matrix();
  313. /**
  314. * Transform the rotation (i.e., direction) of a segment to isolate
  315. * the relative transformation represented by the segment. This operation
  316. * may or may not succeed due to singularities in the equations that define
  317. * motion relativity in this context.
  318. * @param priorVec the origin of the prior segment
  319. * @param fromVec the origin of the current segment
  320. * @param toVec the destination of the current segment
  321. * @param result reference to output variable
  322. * @returns whether or not transformation was successful
  323. */
  324. private static _transformSegmentDirToRef(
  325. priorVec: DeepImmutable<Vector3>,
  326. fromVec: DeepImmutable<Vector3>,
  327. toVec: DeepImmutable<Vector3>,
  328. result: Vector3): boolean {
  329. const DOT_PRODUCT_SAMPLE_REJECTION_THRESHOLD = 0.98;
  330. fromVec.subtractToRef(priorVec, Trajectory._forwardDir);
  331. Trajectory._forwardDir.normalize();
  332. fromVec.scaleToRef(-1, Trajectory._inverseFromVec);
  333. Trajectory._inverseFromVec.normalize();
  334. if (Math.abs(Vector3.Dot(Trajectory._forwardDir, Trajectory._inverseFromVec)) > DOT_PRODUCT_SAMPLE_REJECTION_THRESHOLD) {
  335. return false;
  336. }
  337. Vector3.CrossToRef(Trajectory._forwardDir, Trajectory._inverseFromVec, Trajectory._upDir);
  338. Trajectory._upDir.normalize();
  339. Matrix.LookAtLHToRef(priorVec, fromVec, Trajectory._upDir, Trajectory._lookMatrix);
  340. toVec.subtractToRef(fromVec, Trajectory._fromToVec);
  341. Trajectory._fromToVec.normalize();
  342. Vector3.TransformNormalToRef(Trajectory._fromToVec, Trajectory._lookMatrix, result);
  343. return true;
  344. }
  345. private static _bestMatch: number;
  346. private static _score: number;
  347. private static _bestScore: number;
  348. /**
  349. * Determine which token vector is most similar to the
  350. * segment vector.
  351. * @param segment segment vector
  352. * @param tokens token vector list
  353. * @returns index of the most similar token to the segment
  354. */
  355. private static _tokenizeSegment(
  356. segment: DeepImmutable<Vector3>,
  357. tokens: DeepImmutable<Vector3[]>): number {
  358. Trajectory._bestMatch = 0;
  359. Trajectory._score = Vector3.Dot(segment, tokens[0]);
  360. Trajectory._bestScore = Trajectory._score;
  361. for (let idx = 1; idx < tokens.length; ++idx) {
  362. Trajectory._score = Vector3.Dot(segment, tokens[idx]);
  363. if (Trajectory._score > Trajectory._bestScore) {
  364. Trajectory._bestMatch = idx;
  365. Trajectory._bestScore = Trajectory._score;
  366. }
  367. }
  368. return Trajectory._bestMatch;
  369. }
  370. }
  371. /**
  372. * Collection of vectors intended to be used as the basis of Trajectory
  373. * tokenization for Levenshtein distance comparison. Canonically, a
  374. * Vector3Alphabet will resemble a "spikeball" of vectors distributed
  375. * roughly evenly over the surface of the unit sphere.
  376. */
  377. class Vector3Alphabet {
  378. /**
  379. * Characters in the alphabet.
  380. * NOTE: There is no reason for this property to exist and this class should just extend
  381. * Array<Vector3>, except that doing so produces bizarre build-time errors indicating that
  382. * the ES5 library itself fails its own TypeDoc validation.
  383. */
  384. public chars: Vector3[];
  385. /**
  386. * Helper method to create new "spikeball" Vector3Alphabets. Uses a naive
  387. * optimize-from-random strategy to space points around the unit sphere
  388. * surface as a simple alternative to really doing the math to tile the
  389. * sphere.
  390. * @param alphabetSize size of the desired alphabet
  391. * @param iterations number of iterations over which to optimize the "spikeball"
  392. * @param startingStepSize distance factor to move points in early optimization iterations
  393. * @param endingStepSize distance factor to move points in late optimization iterations
  394. * @param fixedValues alphabet "characters" that are required and cannot be moved by optimization
  395. * @returns a new randomly generated and optimized Vector3Alphabet of the specified size
  396. */
  397. public static Generate(
  398. alphabetSize: number = 64,
  399. iterations: number = 256,
  400. startingStepSize: number = 0.1,
  401. endingStepSize: number = 0.001,
  402. fixedValues: DeepImmutable<Vector3[]> = []): Vector3Alphabet {
  403. const EPSILON = 0.001;
  404. const EPSILON_SQUARED = EPSILON * EPSILON;
  405. let alphabet = new Vector3Alphabet(alphabetSize);
  406. for (let idx = 0; idx < alphabetSize; ++idx) {
  407. alphabet.chars[idx] = new Vector3(
  408. Math.random() - 0.5,
  409. Math.random() - 0.5,
  410. Math.random() - 0.5);
  411. alphabet.chars[idx].normalize();
  412. }
  413. for (let idx = 0; idx < fixedValues.length; ++idx) {
  414. alphabet.chars[idx].copyFrom(fixedValues[idx]);
  415. }
  416. let stepSize: number;
  417. let distSq: number;
  418. let force = new Vector3();
  419. let scratch = new Vector3();
  420. const lerp = (l: number, r: number, t: number) => (1.0 - t) * l + t * r;
  421. for (let iteration = 0; iteration < iterations; ++iteration) {
  422. stepSize = lerp(startingStepSize, endingStepSize, iteration / (iterations - 1));
  423. for (let idx = fixedValues.length; idx < alphabet.chars.length; ++idx) {
  424. force.copyFromFloats(0, 0, 0);
  425. alphabet.chars.forEach((pt) => {
  426. alphabet.chars[idx].subtractToRef(pt, scratch);
  427. distSq = scratch.lengthSquared();
  428. if (distSq > EPSILON_SQUARED) {
  429. scratch.scaleAndAddToRef(1 / (scratch.lengthSquared() * distSq), force);
  430. }
  431. });
  432. force.scaleInPlace(stepSize);
  433. alphabet.chars[idx].addInPlace(force);
  434. alphabet.chars[idx].normalize();
  435. }
  436. }
  437. return alphabet;
  438. }
  439. /**
  440. * Serialize to JSON.
  441. * @returns JSON serialization
  442. */
  443. public serialize(): string {
  444. return JSON.stringify(this.chars);
  445. }
  446. /**
  447. * Deserialize from JSON.
  448. * @param json JSON serialization
  449. * @returns deserialized Vector3Alphabet
  450. */
  451. public static Deserialize(json: string): Vector3Alphabet {
  452. let jsonObject = JSON.parse(json);
  453. let alphabet = new Vector3Alphabet(jsonObject.length);
  454. for (let idx = 0; idx < jsonObject.length; ++idx) {
  455. alphabet.chars[idx] = new Vector3(
  456. jsonObject[idx]["_x"],
  457. jsonObject[idx]["_y"],
  458. jsonObject[idx]["_z"]);
  459. }
  460. return alphabet;
  461. }
  462. private constructor(size: number) {
  463. this.chars = new Array(size);
  464. }
  465. }
  466. /**
  467. * Class which formalizes the manner in which a Vector3Alphabet is used to tokenize and
  468. * describe a Trajectory. This class houses the functionality which determines what
  469. * attributes of Trajectories are and are not considered important, such as scale.
  470. */
  471. class TrajectoryDescriptor {
  472. private static readonly FINEST_DESCRIPTOR_RESOLUTION = 32;
  473. private _sequences: Levenshtein.Sequence<number>[];
  474. /**
  475. * Serialize to JSON.
  476. * @returns JSON serialization
  477. */
  478. public serialize(): string {
  479. return JSON.stringify(this._sequences.map((sequence) => sequence.serialize()));
  480. }
  481. /**
  482. * Deserialize from JSON string and Alphabet. This should be the same Alphabet
  483. * from which the descriptor was originally created, which must be serialized and
  484. * deserialized independently so that it can be passed in here.
  485. * @param json JSON serialization
  486. * @param alphabet Alphabet from which descriptor was originally created
  487. * @returns deserialized TrajectoryDescriptor
  488. */
  489. public static Deserialize(json: string, alphabet: Levenshtein.Alphabet<number>): TrajectoryDescriptor {
  490. let descriptor = new TrajectoryDescriptor();
  491. descriptor._sequences =
  492. (JSON.parse(json) as string[]).map(
  493. (s) => Levenshtein.Sequence.Deserialize(s, alphabet));
  494. return descriptor;
  495. }
  496. /**
  497. * Create a new TrajectoryDescriptor to describe a provided Trajectory according
  498. * to the provided alphabets.
  499. * @param trajectory Trajectory to be described
  500. * @param vector3Alphabet Vector3Alphabet to be used to tokenize the Trajectory
  501. * @param levenshteinAlphabet Levenshtein.Alphabet to be used as basis for comparison with other descriptors
  502. * @returns TrajectoryDescriptor describing provided Trajectory
  503. */
  504. public static CreateFromTrajectory(
  505. trajectory: Trajectory,
  506. vector3Alphabet: Vector3Alphabet,
  507. levenshteinAlphabet: Levenshtein.Alphabet<number>): TrajectoryDescriptor {
  508. return TrajectoryDescriptor.CreateFromTokenizationPyramid(
  509. TrajectoryDescriptor._getTokenizationPyramid(trajectory, vector3Alphabet),
  510. levenshteinAlphabet);
  511. }
  512. /**
  513. * Create a new TrajectoryDescriptor from a pre-existing pyramid of tokens.
  514. * NOTE: This function exists to support an outdated serialization mechanism and should
  515. * be deleted if it is no longer useful.
  516. * @param pyramid tokenization pyramid
  517. * @param levenshteinAlphabet Levenshtein.Alphabet to be uses as basis for comparison with other descriptors
  518. * @returns TrajectoryDescriptor describing the Trajectory from which the pyramid was built
  519. */
  520. public static CreateFromTokenizationPyramid(
  521. pyramid: number[][],
  522. levenshteinAlphabet: Levenshtein.Alphabet<number>) : TrajectoryDescriptor {
  523. let descriptor = new TrajectoryDescriptor();
  524. descriptor._sequences = pyramid.map((tokens) => new Levenshtein.Sequence<number>(tokens, levenshteinAlphabet));
  525. return descriptor;
  526. }
  527. private constructor() {
  528. this._sequences = [];
  529. }
  530. /**
  531. * Create the tokenization pyramid for the provided Trajectory according to the given
  532. * Vector3Alphabet.
  533. * @param trajectory Trajectory to be tokenized
  534. * @param alphabet Vector3Alphabet containing tokens
  535. * @param targetResolution finest resolution of descriptor
  536. * @returns tokenization pyramid for Trajectory
  537. */
  538. private static _getTokenizationPyramid(
  539. trajectory: Trajectory,
  540. alphabet: Vector3Alphabet,
  541. targetResolution: number = TrajectoryDescriptor.FINEST_DESCRIPTOR_RESOLUTION): number[][] {
  542. let pyramid: number[][] = [];
  543. for (let res = targetResolution; res > 4; res = Math.floor(res / 2)) {
  544. pyramid.push(trajectory.resampleAtTargetResolution(res).tokenize(alphabet.chars));
  545. }
  546. return pyramid;
  547. }
  548. /**
  549. * Calculate a distance metric between this TrajectoryDescriptor and another. This is
  550. * essentially a similarity score and does not directly represent Euclidean distance,
  551. * edit distance, or any other formal distance metric.
  552. * @param other TrajectoryDescriptor from which to determine distance
  553. * @returns distance, a nonnegative similarity score where larger values indicate dissimilarity
  554. */
  555. public distance(other: TrajectoryDescriptor): number {
  556. let totalDistance = 0;
  557. let weight: number;
  558. for (let idx = 0; idx < this._sequences.length; ++idx) {
  559. weight = Math.pow(2, idx);
  560. totalDistance += (weight * this._sequences[idx].distance(other._sequences[idx]));
  561. }
  562. return totalDistance;
  563. }
  564. }
  565. /**
  566. * A set of TrajectoryDescriptors defined to be "the same." This is essentially a helper
  567. * class to facilitate methods of Trajectory clustering.
  568. */
  569. class TrajectoryClass {
  570. private static readonly MIN_AVERAGE_DISTANCE = 1;
  571. private _descriptors: TrajectoryDescriptor[];
  572. private _centroidIdx: number;
  573. private _averageDistance: number;
  574. /**
  575. * Serialize to JSON.
  576. * @returns JSON serialization
  577. */
  578. public serialize(): string {
  579. let jsonObject: any = {};
  580. jsonObject.descriptors = this._descriptors.map((desc) => desc.serialize());
  581. jsonObject.centroidIdx = this._centroidIdx;
  582. jsonObject.averageDistance = this._averageDistance;
  583. return JSON.stringify(jsonObject);
  584. }
  585. /**
  586. * Deserialize from JSON string and Alphabet. This should be the same Alphabet
  587. * from which the descriptors were originally created, which must be serialized and
  588. * deserialized independently so that it can be passed in here.
  589. * @param json JSON string representation
  590. * @param alphabet Alphabet from which TrajectoryDescriptors were originally created
  591. * @returns deserialized TrajectoryDescriptor
  592. */
  593. public static Deserialize(json: string, alphabet: Levenshtein.Alphabet<number>): TrajectoryClass {
  594. let jsonObject = JSON.parse(json);
  595. let described = new TrajectoryClass();
  596. described._descriptors = jsonObject.descriptors.map((s: string) => TrajectoryDescriptor.Deserialize(s, alphabet));
  597. described._centroidIdx = jsonObject.centroidIdx;
  598. described._averageDistance = jsonObject.averageDistance;
  599. return described;
  600. }
  601. /**
  602. * Create a new DescribedTrajectory.
  603. * @param descriptors currently-known TrajectoryDescriptors, if any
  604. */
  605. public constructor(descriptors: TrajectoryDescriptor[] = []) {
  606. this._descriptors = descriptors;
  607. this._centroidIdx = -1;
  608. this._averageDistance = 0;
  609. this._refreshDescription();
  610. }
  611. /**
  612. * Add a new TrajectoryDescriptor to the list of descriptors known to describe
  613. * this same DescribedTrajectory.
  614. * @param descriptor descriptor to be added
  615. */
  616. public add(descriptor: TrajectoryDescriptor): void {
  617. this._descriptors.push(descriptor);
  618. this._refreshDescription();
  619. }
  620. /**
  621. * Compute the cost, which is inversely related to the likelihood that the provided
  622. * TrajectoryDescriptor describes a Trajectory that is considered to be the same as
  623. * the class represented by this DescribedTrajectory.
  624. * @param descriptor the descriptor to be costed
  625. * @returns cost of the match, which is a nonnegative similarity metric where larger values indicate dissimiliarity
  626. */
  627. public getMatchCost(descriptor: TrajectoryDescriptor): number {
  628. return descriptor.distance(this._descriptors[this._centroidIdx]) / this._averageDistance;
  629. }
  630. /**
  631. * Compute the minimum distance between the queried TrajectoryDescriptor and a
  632. * descriptor which is a member of this collection. This is an alternative way of
  633. * conceptualizing match cost from getMatchCost(), and it serves a different function.
  634. * @param descriptor the descriptor to find the minimum distance to
  635. * @returns minimum descriptor distance to a member descriptor of this DescribedTrajectory
  636. */
  637. public getMatchMinimumDistance(descriptor: TrajectoryDescriptor): number {
  638. return Math.min(...this._descriptors.map((desc) => desc.distance(descriptor)));
  639. }
  640. /**
  641. * Refreshes the internal representation of this DescribedTrajectory.
  642. */
  643. private _refreshDescription(): void {
  644. this._centroidIdx = -1;
  645. let sum: number;
  646. let distances = this._descriptors.map((a) => {
  647. sum = 0;
  648. this._descriptors.forEach((b) => {
  649. sum += a.distance(b);
  650. });
  651. return sum;
  652. });
  653. for (let idx = 0; idx < distances.length; ++idx) {
  654. if (this._centroidIdx < 0 || distances[idx] < distances[this._centroidIdx]) {
  655. this._centroidIdx = idx;
  656. }
  657. }
  658. this._averageDistance = 0;
  659. this._descriptors.forEach((desc) => {
  660. this._averageDistance += desc.distance(this._descriptors[this._centroidIdx]);
  661. });
  662. if (this._descriptors.length > 0) {
  663. this._averageDistance = Math.max(this._averageDistance / this._descriptors.length, TrajectoryClass.MIN_AVERAGE_DISTANCE);
  664. }
  665. }
  666. }
  667. /**
  668. * Class representing a set of known, named trajectories to which Trajectories can be
  669. * added and using which Trajectories can be recognized.
  670. */
  671. export class TrajectoryClassifier {
  672. private _maximumAllowableMatchCost: number = 4;
  673. private _vector3Alphabet: Vector3Alphabet;
  674. private _levenshteinAlphabet: Levenshtein.Alphabet<number>;
  675. private _nameToDescribedTrajectory: Map<string, TrajectoryClass>;
  676. /**
  677. * Serialize to JSON.
  678. * @returns JSON serialization
  679. */
  680. public serialize(): string {
  681. let jsonObject: any = {};
  682. jsonObject.maximumAllowableMatchCost = this._maximumAllowableMatchCost;
  683. jsonObject.vector3Alphabet = this._vector3Alphabet.serialize();
  684. jsonObject.levenshteinAlphabet = this._levenshteinAlphabet.serialize();
  685. jsonObject.nameToDescribedTrajectory = [];
  686. this._nameToDescribedTrajectory.forEach((described, name) => {
  687. jsonObject.nameToDescribedTrajectory.push(name);
  688. jsonObject.nameToDescribedTrajectory.push(described.serialize());
  689. });
  690. return JSON.stringify(jsonObject);
  691. }
  692. /**
  693. * Deserialize from JSON.
  694. * @param json JSON serialization
  695. * @returns deserialized TrajectorySet
  696. */
  697. public static Deserialize(json: string): TrajectoryClassifier {
  698. let jsonObject = JSON.parse(json);
  699. let classifier = new TrajectoryClassifier();
  700. classifier._maximumAllowableMatchCost = jsonObject.maximumAllowableMatchCost;
  701. classifier._vector3Alphabet = Vector3Alphabet.Deserialize(jsonObject.vector3Alphabet);
  702. classifier._levenshteinAlphabet = Levenshtein.Alphabet.Deserialize<number>(jsonObject.levenshteinAlphabet);
  703. for (let idx = 0; idx < jsonObject.nameToDescribedTrajectory.length; idx += 2) {
  704. classifier._nameToDescribedTrajectory.set(
  705. jsonObject.nameToDescribedTrajectory[idx],
  706. TrajectoryClass.Deserialize(jsonObject.nameToDescribedTrajectory[idx + 1], classifier._levenshteinAlphabet));
  707. }
  708. return classifier;
  709. }
  710. /**
  711. * Initialize a new empty TrajectorySet with auto-generated Alphabets.
  712. * VERY naive, need to be generating these things from known
  713. * sets. Better version later, probably eliminating this one.
  714. * @returns auto-generated TrajectorySet
  715. */
  716. public static Generate(): TrajectoryClassifier {
  717. let vecs = Vector3Alphabet.Generate(64, 256, 0.1, 0.001, [Vector3.Forward()]);
  718. let alphabet = new Levenshtein.Alphabet<number>(
  719. Array.from(Array(vecs.chars.length), (_, idx) => idx),
  720. (idx) => idx === 0 ? 0 : 1,
  721. (idx) => idx === 0 ? 0 : 1,
  722. (a, b) => Math.min(1 - Vector3.Dot(vecs.chars[a], vecs.chars[b]), 1));
  723. let trajectorySet = new TrajectoryClassifier();
  724. trajectorySet._vector3Alphabet = vecs;
  725. trajectorySet._levenshteinAlphabet = alphabet;
  726. return trajectorySet;
  727. }
  728. private constructor() {
  729. this._nameToDescribedTrajectory = new Map<string, TrajectoryClass>();
  730. }
  731. /**
  732. * Add a new Trajectory to the set with a given name.
  733. * @param trajectory new Trajectory to be added
  734. * @param classification name to which to add the Trajectory
  735. */
  736. public addTrajectoryToClassification(trajectory: Trajectory, classification: string): void {
  737. if (!this._nameToDescribedTrajectory.has(classification)) {
  738. this._nameToDescribedTrajectory.set(classification, new TrajectoryClass());
  739. }
  740. this._nameToDescribedTrajectory.get(classification)!.add(
  741. TrajectoryDescriptor.CreateFromTrajectory(
  742. trajectory,
  743. this._vector3Alphabet,
  744. this._levenshteinAlphabet));
  745. }
  746. /**
  747. * Remove a known named trajectory and all Trajectories associated with it.
  748. * @param classification name to remove
  749. * @returns whether anything was removed
  750. */
  751. public deleteClassification(classification: string): boolean {
  752. return this._nameToDescribedTrajectory.delete(classification);
  753. }
  754. /**
  755. * Attempt to recognize a Trajectory from among all the classifications
  756. * already known to the classifier.
  757. * @param trajectory Trajectory to be recognized
  758. * @returns classification of Trajectory if recognized, null otherwise
  759. */
  760. public classifyTrajectory(trajectory: Trajectory): Nullable<string> {
  761. let descriptor = TrajectoryDescriptor.CreateFromTrajectory(
  762. trajectory,
  763. this._vector3Alphabet,
  764. this._levenshteinAlphabet);
  765. let allowableMatches: string[] = [];
  766. this._nameToDescribedTrajectory.forEach((trajectoryClass, classification) => {
  767. if (trajectoryClass.getMatchCost(descriptor) < this._maximumAllowableMatchCost) {
  768. allowableMatches.push(classification);
  769. }
  770. });
  771. if (allowableMatches.length === 0) {
  772. return null;
  773. }
  774. let bestIdx = 0;
  775. let bestMatch = this._nameToDescribedTrajectory.get(allowableMatches[bestIdx])!.getMatchMinimumDistance(descriptor);
  776. let match: number;
  777. for (let idx = 0; idx < allowableMatches.length; ++idx) {
  778. match = this._nameToDescribedTrajectory.get(allowableMatches[idx])!.getMatchMinimumDistance(descriptor);
  779. if (match < bestMatch) {
  780. bestMatch = match;
  781. bestIdx = idx;
  782. }
  783. }
  784. return allowableMatches[bestIdx];
  785. }
  786. }