@tscircuit/capacity-autorouter 0.0.58 → 0.0.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -48,6 +48,12 @@ var BaseSolver = class {
48
48
  failedSubSolvers;
49
49
  timeToSolve;
50
50
  stats = {};
51
+ /**
52
+ * For cached solvers
53
+ **/
54
+ cacheHit;
55
+ cacheKey;
56
+ cacheToSolveSpaceTransform;
51
57
  /** DO NOT OVERRIDE! Override _step() instead */
52
58
  step() {
53
59
  if (this.solved) return;
@@ -8053,13 +8059,17 @@ var MultiHeadPolyLineIntraNodeSolver = class extends BaseSolver {
8053
8059
  return [newNeighbor];
8054
8060
  }
8055
8061
  checkIfSolved(candidate) {
8056
- return candidate.minGaps.every((minGap) => minGap >= this.obstacleMargin) && candidate.polyLines.every((polyLine) => {
8062
+ const minGapsToOtherConnectionsValid = candidate.minGaps.every(
8063
+ (minGap) => minGap >= this.obstacleMargin
8064
+ );
8065
+ const allPointsWithinBounds = candidate.polyLines.every((polyLine) => {
8057
8066
  return polyLine.mPoints.every((mPoint) => {
8058
- const padding = (mPoint.z1 !== mPoint.z2 ? this.viaDiameter / 2 : this.traceWidth / 2) * // Forgiveness outside bounds
8067
+ const padding = (mPoint.z1 !== mPoint.z2 ? this.viaDiameter / 2 : 0) * // Forgiveness outside bounds
8059
8068
  0.9;
8060
8069
  return withinBounds(mPoint, this.bounds, padding);
8061
8070
  });
8062
8071
  });
8072
+ return minGapsToOtherConnectionsValid && allPointsWithinBounds;
8063
8073
  }
8064
8074
  _step() {
8065
8075
  if (this.phase === "setup") {
@@ -10259,6 +10269,7 @@ var UnravelSectionSolver = class extends BaseSolver {
10259
10269
  tunedNodeCapacityMap;
10260
10270
  MAX_CANDIDATES = 500;
10261
10271
  iterationsSinceImprovement = 0;
10272
+ hyperParameters;
10262
10273
  selectedCandidateIndex = null;
10263
10274
  queuedOrExploredCandidatePointModificationHashes = /* @__PURE__ */ new Set();
10264
10275
  constructorParams;
@@ -10267,6 +10278,10 @@ var UnravelSectionSolver = class extends BaseSolver {
10267
10278
  this.constructorParams = params;
10268
10279
  this.MUTABLE_HOPS = params.MUTABLE_HOPS ?? this.MUTABLE_HOPS;
10269
10280
  this.MAX_ITERATIONS = 5e4;
10281
+ this.hyperParameters = {
10282
+ ...params.hyperParameters,
10283
+ MAX_ITERATIONS_WITHOUT_IMPROVEMENT: 200
10284
+ };
10270
10285
  this.nodeMap = params.nodeMap;
10271
10286
  this.dedupedSegments = params.dedupedSegments;
10272
10287
  if (params.dedupedSegmentMap) {
@@ -10429,16 +10444,21 @@ var UnravelSectionSolver = class extends BaseSolver {
10429
10444
  });
10430
10445
  return {
10431
10446
  pointModifications,
10432
- issues,
10433
10447
  g,
10434
10448
  h: 0,
10435
10449
  f: g,
10436
10450
  operationsPerformed: 0,
10437
- candidateHash: createPointModificationsHash(pointModifications)
10451
+ candidateHash: createPointModificationsHash(pointModifications),
10438
10452
  // candidateFullHash: createFullPointModificationsHash(
10439
10453
  // this.unravelSection.segmentPointMap,
10440
10454
  // pointModifications,
10441
10455
  // ),
10456
+ // Ensure original candidate has issues calculated
10457
+ issues: getIssuesInSection(
10458
+ this.unravelSection,
10459
+ this.nodeMap,
10460
+ pointModifications
10461
+ )
10442
10462
  };
10443
10463
  }
10444
10464
  get nextCandidate() {
@@ -10690,6 +10710,10 @@ var UnravelSectionSolver = class extends BaseSolver {
10690
10710
  _step() {
10691
10711
  const candidate = this.candidates.shift();
10692
10712
  this.iterationsSinceImprovement++;
10713
+ if (this.iterationsSinceImprovement > this.hyperParameters.MAX_ITERATIONS_WITHOUT_IMPROVEMENT) {
10714
+ this.solved = true;
10715
+ return;
10716
+ }
10693
10717
  if (!candidate) {
10694
10718
  this.solved = true;
10695
10719
  return;
@@ -10733,6 +10757,8 @@ var UnravelSectionSolver = class extends BaseSolver {
10733
10757
  } else {
10734
10758
  candidate = this.candidates[this.selectedCandidateIndex];
10735
10759
  }
10760
+ } else if (this.solved) {
10761
+ candidate = this.bestCandidate;
10736
10762
  } else {
10737
10763
  candidate = this.lastProcessedCandidate || this.candidates[0];
10738
10764
  }
@@ -10898,6 +10924,653 @@ New: (${modifiedPoint.x.toFixed(2)}, ${modifiedPoint.y.toFixed(2)}, ${modifiedPo
10898
10924
  }
10899
10925
  };
10900
10926
 
10927
+ // lib/solvers/UnravelSolver/CachedUnravelSectionSolver.ts
10928
+ import objectHash from "object-hash";
10929
+
10930
+ // lib/cache/InMemoryCache.ts
10931
+ var InMemoryCache = class {
10932
+ isSyncCache = true;
10933
+ cacheHits = 0;
10934
+ cacheMisses = 0;
10935
+ cache = /* @__PURE__ */ new Map();
10936
+ /**
10937
+ * Retrieves a cached solution synchronously based on the cache key.
10938
+ * Increments cache hit/miss counters.
10939
+ * @param cacheKey The key to look up in the cache.
10940
+ * @returns The cached solution if found, otherwise undefined.
10941
+ */
10942
+ getCachedSolutionSync(cacheKey) {
10943
+ const cachedSolution = this.cache.get(cacheKey);
10944
+ if (cachedSolution !== void 0) {
10945
+ this.cacheHits++;
10946
+ return structuredClone(cachedSolution);
10947
+ } else {
10948
+ this.cacheMisses++;
10949
+ return void 0;
10950
+ }
10951
+ }
10952
+ /**
10953
+ * Retrieves a cached solution asynchronously. Wraps the synchronous method.
10954
+ * @param cacheKey The key to look up in the cache.
10955
+ * @returns A promise that resolves with the cached solution or undefined.
10956
+ */
10957
+ async getCachedSolution(cacheKey) {
10958
+ return this.getCachedSolutionSync(cacheKey);
10959
+ }
10960
+ /**
10961
+ * Stores a solution in the cache synchronously.
10962
+ * Uses structured cloning to store a copy, preventing external modifications.
10963
+ * @param cacheKey The key under which to store the solution.
10964
+ * @param cachedSolution The solution data to cache.
10965
+ */
10966
+ setCachedSolutionSync(cacheKey, cachedSolution) {
10967
+ this.cache.set(cacheKey, structuredClone(cachedSolution));
10968
+ }
10969
+ /**
10970
+ * Stores a solution in the cache asynchronously. Wraps the synchronous method.
10971
+ * @param cacheKey The key under which to store the solution.
10972
+ * @param cachedSolution The solution data to cache.
10973
+ * @returns A promise that resolves when the solution is cached.
10974
+ */
10975
+ async setCachedSolution(cacheKey, cachedSolution) {
10976
+ this.setCachedSolutionSync(cacheKey, cachedSolution);
10977
+ }
10978
+ /**
10979
+ * Clears the entire cache and resets hit/miss counters.
10980
+ */
10981
+ clearCache() {
10982
+ this.cache.clear();
10983
+ this.cacheHits = 0;
10984
+ this.cacheMisses = 0;
10985
+ }
10986
+ getAllCacheKeys() {
10987
+ return Array.from(this.cache.keys());
10988
+ }
10989
+ };
10990
+
10991
+ // lib/cache/LocalStorageCache.ts
10992
+ var CACHE_PREFIX = "tscircuit_autorouter_cache_";
10993
+ var LocalStorageCache = class {
10994
+ isSyncCache = true;
10995
+ cacheHits = 0;
10996
+ cacheMisses = 0;
10997
+ constructor() {
10998
+ if (typeof localStorage === "undefined") {
10999
+ console.warn(
11000
+ "LocalStorage is not available. LocalStorageCache will not function."
11001
+ );
11002
+ }
11003
+ }
11004
+ getKey(cacheKey) {
11005
+ return `${CACHE_PREFIX}${cacheKey}`;
11006
+ }
11007
+ /**
11008
+ * Retrieves a cached solution synchronously from localStorage.
11009
+ * Increments cache hit/miss counters.
11010
+ * @param cacheKey The key to look up in the cache.
11011
+ * @returns The cached solution if found and parsed correctly, otherwise undefined.
11012
+ */
11013
+ getCachedSolutionSync(cacheKey) {
11014
+ if (typeof localStorage === "undefined") return void 0;
11015
+ const key = this.getKey(cacheKey);
11016
+ try {
11017
+ const cachedItem = localStorage.getItem(key);
11018
+ if (cachedItem !== null) {
11019
+ const solution = JSON.parse(cachedItem);
11020
+ this.cacheHits++;
11021
+ return solution;
11022
+ } else {
11023
+ this.cacheMisses++;
11024
+ return void 0;
11025
+ }
11026
+ } catch (error) {
11027
+ console.error(`Error getting cached solution sync for ${key}:`, error);
11028
+ this.cacheMisses++;
11029
+ return void 0;
11030
+ }
11031
+ }
11032
+ /**
11033
+ * Retrieves a cached solution asynchronously. Wraps the synchronous method.
11034
+ * @param cacheKey The key to look up in the cache.
11035
+ * @returns A promise that resolves with the cached solution or undefined.
11036
+ */
11037
+ async getCachedSolution(cacheKey) {
11038
+ return this.getCachedSolutionSync(cacheKey);
11039
+ }
11040
+ /**
11041
+ * Stores a solution in localStorage synchronously.
11042
+ * The solution is JSON stringified before storing.
11043
+ * @param cacheKey The key under which to store the solution.
11044
+ * @param cachedSolution The solution data to cache.
11045
+ */
11046
+ setCachedSolutionSync(cacheKey, cachedSolution) {
11047
+ if (typeof localStorage === "undefined") return;
11048
+ const key = this.getKey(cacheKey);
11049
+ try {
11050
+ const stringifiedSolution = JSON.stringify(cachedSolution);
11051
+ localStorage.setItem(key, stringifiedSolution);
11052
+ } catch (error) {
11053
+ console.error(`Error setting cached solution sync for ${key}:`, error);
11054
+ if (error instanceof DOMException && (error.name === "QuotaExceededError" || error.name === "NS_ERROR_DOM_QUOTA_REACHED")) {
11055
+ console.warn(
11056
+ `LocalStorage quota exceeded. Failed to cache solution for ${key}. Consider clearing the cache.`
11057
+ );
11058
+ }
11059
+ }
11060
+ }
11061
+ /**
11062
+ * Stores a solution in the cache asynchronously. Wraps the synchronous method.
11063
+ * @param cacheKey The key under which to store the solution.
11064
+ * @param cachedSolution The solution data to cache.
11065
+ * @returns A promise that resolves when the solution is cached.
11066
+ */
11067
+ async setCachedSolution(cacheKey, cachedSolution) {
11068
+ this.setCachedSolutionSync(cacheKey, cachedSolution);
11069
+ }
11070
+ /**
11071
+ * Clears all cache entries created by this instance from localStorage
11072
+ * and resets hit/miss counters.
11073
+ */
11074
+ clearCache() {
11075
+ if (typeof localStorage === "undefined") return;
11076
+ try {
11077
+ const keysToRemove = [];
11078
+ for (let i = 0; i < localStorage.length; i++) {
11079
+ const key = localStorage.key(i);
11080
+ if (key?.startsWith(CACHE_PREFIX)) {
11081
+ keysToRemove.push(key);
11082
+ }
11083
+ }
11084
+ keysToRemove.forEach((key) => localStorage.removeItem(key));
11085
+ console.log(
11086
+ `Cleared ${keysToRemove.length} items from LocalStorage cache.`
11087
+ );
11088
+ } catch (error) {
11089
+ console.error("Error clearing LocalStorage cache:", error);
11090
+ } finally {
11091
+ this.cacheHits = 0;
11092
+ this.cacheMisses = 0;
11093
+ }
11094
+ }
11095
+ getAllCacheKeys() {
11096
+ const cacheKeys = [];
11097
+ for (let i = 0; i < 1e4; i++) {
11098
+ const keyName = localStorage.key(i);
11099
+ if (!keyName) break;
11100
+ if (!keyName.includes(CACHE_PREFIX)) continue;
11101
+ cacheKeys.push(keyName);
11102
+ }
11103
+ return cacheKeys;
11104
+ }
11105
+ };
11106
+
11107
+ // lib/cache/setupGlobalCaches.ts
11108
+ function getGlobalLocalStorageCache() {
11109
+ if (!globalThis.TSCIRCUIT_AUTOROUTER_LOCAL_STORAGE_CACHE) {
11110
+ setupGlobalCaches();
11111
+ }
11112
+ return globalThis.TSCIRCUIT_AUTOROUTER_LOCAL_STORAGE_CACHE;
11113
+ }
11114
+ function setupGlobalCaches() {
11115
+ globalThis.TSCIRCUIT_AUTOROUTER_LOCAL_STORAGE_CACHE ??= new LocalStorageCache();
11116
+ globalThis.TSCIRCUIT_AUTOROUTER_IN_MEMORY_CACHE ??= new InMemoryCache();
11117
+ }
11118
+
11119
+ // node_modules/transformation-matrix/src/applyToPoint.js
11120
+ function applyToPoint(matrix, point) {
11121
+ return Array.isArray(point) ? [
11122
+ matrix.a * point[0] + matrix.c * point[1] + matrix.e,
11123
+ matrix.b * point[0] + matrix.d * point[1] + matrix.f
11124
+ ] : {
11125
+ x: matrix.a * point.x + matrix.c * point.y + matrix.e,
11126
+ y: matrix.b * point.x + matrix.d * point.y + matrix.f
11127
+ };
11128
+ }
11129
+
11130
+ // node_modules/transformation-matrix/src/translate.js
11131
+ function translate(tx, ty = 0) {
11132
+ return {
11133
+ a: 1,
11134
+ c: 0,
11135
+ e: tx,
11136
+ b: 0,
11137
+ d: 1,
11138
+ f: ty
11139
+ };
11140
+ }
11141
+
11142
+ // node_modules/transformation-matrix/src/rotate.js
11143
+ var { cos, sin, PI } = Math;
11144
+
11145
+ // node_modules/transformation-matrix/src/skew.js
11146
+ var { tan } = Math;
11147
+
11148
+ // node_modules/transformation-matrix/src/fromTransformAttribute.autogenerated.js
11149
+ function peg$subclass(child, parent) {
11150
+ function C() {
11151
+ this.constructor = child;
11152
+ }
11153
+ C.prototype = parent.prototype;
11154
+ child.prototype = new C();
11155
+ }
11156
+ function peg$SyntaxError(message, expected, found, location) {
11157
+ var self = Error.call(this, message);
11158
+ if (Object.setPrototypeOf) {
11159
+ Object.setPrototypeOf(self, peg$SyntaxError.prototype);
11160
+ }
11161
+ self.expected = expected;
11162
+ self.found = found;
11163
+ self.location = location;
11164
+ self.name = "SyntaxError";
11165
+ return self;
11166
+ }
11167
+ peg$subclass(peg$SyntaxError, Error);
11168
+ function peg$padEnd(str, targetLength, padString) {
11169
+ padString = padString || " ";
11170
+ if (str.length > targetLength) {
11171
+ return str;
11172
+ }
11173
+ targetLength -= str.length;
11174
+ padString += padString.repeat(targetLength);
11175
+ return str + padString.slice(0, targetLength);
11176
+ }
11177
+ peg$SyntaxError.prototype.format = function(sources) {
11178
+ var str = "Error: " + this.message;
11179
+ if (this.location) {
11180
+ var src = null;
11181
+ var k;
11182
+ for (k = 0; k < sources.length; k++) {
11183
+ if (sources[k].source === this.location.source) {
11184
+ src = sources[k].text.split(/\r\n|\n|\r/g);
11185
+ break;
11186
+ }
11187
+ }
11188
+ var s = this.location.start;
11189
+ var offset_s = this.location.source && typeof this.location.source.offset === "function" ? this.location.source.offset(s) : s;
11190
+ var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column;
11191
+ if (src) {
11192
+ var e = this.location.end;
11193
+ var filler = peg$padEnd("", offset_s.line.toString().length, " ");
11194
+ var line = src[s.line - 1];
11195
+ var last = s.line === e.line ? e.column : line.length + 1;
11196
+ var hatLen = last - s.column || 1;
11197
+ str += "\n --> " + loc + "\n" + filler + " |\n" + offset_s.line + " | " + line + "\n" + filler + " | " + peg$padEnd("", s.column - 1, " ") + peg$padEnd("", hatLen, "^");
11198
+ } else {
11199
+ str += "\n at " + loc;
11200
+ }
11201
+ }
11202
+ return str;
11203
+ };
11204
+ peg$SyntaxError.buildMessage = function(expected, found) {
11205
+ var DESCRIBE_EXPECTATION_FNS = {
11206
+ literal: function(expectation) {
11207
+ return '"' + literalEscape(expectation.text) + '"';
11208
+ },
11209
+ class: function(expectation) {
11210
+ var escapedParts = expectation.parts.map(function(part) {
11211
+ return Array.isArray(part) ? classEscape(part[0]) + "-" + classEscape(part[1]) : classEscape(part);
11212
+ });
11213
+ return "[" + (expectation.inverted ? "^" : "") + escapedParts.join("") + "]";
11214
+ },
11215
+ any: function() {
11216
+ return "any character";
11217
+ },
11218
+ end: function() {
11219
+ return "end of input";
11220
+ },
11221
+ other: function(expectation) {
11222
+ return expectation.description;
11223
+ }
11224
+ };
11225
+ function hex(ch) {
11226
+ return ch.charCodeAt(0).toString(16).toUpperCase();
11227
+ }
11228
+ function literalEscape(s) {
11229
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\0/g, "\\0").replace(/\t/g, "\\t").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/[\x00-\x0F]/g, function(ch) {
11230
+ return "\\x0" + hex(ch);
11231
+ }).replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) {
11232
+ return "\\x" + hex(ch);
11233
+ });
11234
+ }
11235
+ function classEscape(s) {
11236
+ return s.replace(/\\/g, "\\\\").replace(/\]/g, "\\]").replace(/\^/g, "\\^").replace(/-/g, "\\-").replace(/\0/g, "\\0").replace(/\t/g, "\\t").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/[\x00-\x0F]/g, function(ch) {
11237
+ return "\\x0" + hex(ch);
11238
+ }).replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) {
11239
+ return "\\x" + hex(ch);
11240
+ });
11241
+ }
11242
+ function describeExpectation(expectation) {
11243
+ return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation);
11244
+ }
11245
+ function describeExpected(expected2) {
11246
+ var descriptions = expected2.map(describeExpectation);
11247
+ var i, j;
11248
+ descriptions.sort();
11249
+ if (descriptions.length > 0) {
11250
+ for (i = 1, j = 1; i < descriptions.length; i++) {
11251
+ if (descriptions[i - 1] !== descriptions[i]) {
11252
+ descriptions[j] = descriptions[i];
11253
+ j++;
11254
+ }
11255
+ }
11256
+ descriptions.length = j;
11257
+ }
11258
+ switch (descriptions.length) {
11259
+ case 1:
11260
+ return descriptions[0];
11261
+ case 2:
11262
+ return descriptions[0] + " or " + descriptions[1];
11263
+ default:
11264
+ return descriptions.slice(0, -1).join(", ") + ", or " + descriptions[descriptions.length - 1];
11265
+ }
11266
+ }
11267
+ function describeFound(found2) {
11268
+ return found2 ? '"' + literalEscape(found2) + '"' : "end of input";
11269
+ }
11270
+ return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found.";
11271
+ };
11272
+
11273
+ // lib/solvers/UnravelSolver/CachedUnravelSectionSolver.ts
11274
+ var approximateCoordinate = (coord) => {
11275
+ return (Math.round(coord * 20) / 20).toFixed(2);
11276
+ };
11277
+ setupGlobalCaches();
11278
+ var CachedUnravelSectionSolver = class extends UnravelSectionSolver {
11279
+ cacheHit = false;
11280
+ cacheProvider;
11281
+ hasAttemptedToUseCache = false;
11282
+ constructor(params) {
11283
+ super(params);
11284
+ this.cacheProvider = params.cacheProvider === void 0 ? getGlobalLocalStorageCache() : params.cacheProvider;
11285
+ }
11286
+ _step() {
11287
+ if (!this.hasAttemptedToUseCache && this.cacheProvider) {
11288
+ if (this.attemptToUseCacheSync()) return;
11289
+ }
11290
+ super._step();
11291
+ if ((this.solved || this.failed) && this.cacheProvider) {
11292
+ this.saveToCacheSync();
11293
+ }
11294
+ }
11295
+ computeCacheKeyAndTransform() {
11296
+ const rootNode = this.nodeMap.get(this.rootNodeId);
11297
+ const realToCacheTransform = translate(
11298
+ -rootNode.center.x,
11299
+ -rootNode.center.y
11300
+ );
11301
+ const nodeIdMap = /* @__PURE__ */ new Map();
11302
+ const reverseNodeIdMap = /* @__PURE__ */ new Map();
11303
+ const segmentIdMap = /* @__PURE__ */ new Map();
11304
+ const reverseSegmentIdMap = /* @__PURE__ */ new Map();
11305
+ const segmentPointIdMap = /* @__PURE__ */ new Map();
11306
+ const reverseSegmentPointIdMap = /* @__PURE__ */ new Map();
11307
+ let nodeCounter = 0;
11308
+ let segmentCounter = 0;
11309
+ let spCounter = 0;
11310
+ const sortedNodeIds = [...this.unravelSection.allNodeIds].sort(
11311
+ (aNId, bNId) => {
11312
+ const n1 = this.nodeMap.get(aNId);
11313
+ const n2 = this.nodeMap.get(bNId);
11314
+ if (n1.center.x !== n2.center.x) {
11315
+ return n1.center.x - n2.center.x;
11316
+ }
11317
+ return n1.center.y - n2.center.y;
11318
+ }
11319
+ );
11320
+ for (const nodeId of sortedNodeIds) {
11321
+ const normId = `node_${nodeCounter++}`;
11322
+ nodeIdMap.set(nodeId, normId);
11323
+ reverseNodeIdMap.set(normId, nodeId);
11324
+ }
11325
+ const sortedSegmentPointIds = [
11326
+ ...Array.from(this.unravelSection.segmentPointMap.entries()).sort(([, a], [, b]) => {
11327
+ if (a.x !== b.x) {
11328
+ return a.x - b.x;
11329
+ }
11330
+ return a.y - b.y;
11331
+ }).map(([id]) => id)
11332
+ ].sort();
11333
+ for (const spId of sortedSegmentPointIds) {
11334
+ const normSpId = `sp_${spCounter++}`;
11335
+ segmentPointIdMap.set(spId, normSpId);
11336
+ reverseSegmentPointIdMap.set(normSpId, spId);
11337
+ const segmentId = this.unravelSection.segmentPointMap.get(spId).segmentId;
11338
+ if (!segmentIdMap.has(segmentId)) {
11339
+ const normSegId = `seg_${segmentCounter++}`;
11340
+ segmentIdMap.set(segmentId, normSegId);
11341
+ reverseSegmentIdMap.set(normSegId, segmentId);
11342
+ }
11343
+ }
11344
+ const normalizedNodes = {};
11345
+ for (const [nodeId, normNodeId] of nodeIdMap.entries()) {
11346
+ const node = this.nodeMap.get(nodeId);
11347
+ const transformedCenter = applyToPoint(realToCacheTransform, node.center);
11348
+ normalizedNodes[normNodeId] = {
11349
+ // ...node,
11350
+ width: node.width,
11351
+ // TODO: Scale width/height if transform includes scaling
11352
+ height: node.height,
11353
+ availableZ: node.availableZ,
11354
+ center: {
11355
+ x: approximateCoordinate(transformedCenter.x),
11356
+ y: approximateCoordinate(transformedCenter.y)
11357
+ }
11358
+ };
11359
+ }
11360
+ const normalizedSegmentPoints = {};
11361
+ for (const [spId, normSpId] of segmentPointIdMap.entries()) {
11362
+ const sp = this.unravelSection.segmentPointMap.get(spId);
11363
+ const transformedPoint = applyToPoint(realToCacheTransform, {
11364
+ x: sp.x,
11365
+ y: sp.y
11366
+ });
11367
+ normalizedSegmentPoints[normSpId] = {
11368
+ x: approximateCoordinate(transformedPoint.x),
11369
+ y: approximateCoordinate(transformedPoint.y),
11370
+ z: sp.z
11371
+ // Z is not transformed by 2D matrix
11372
+ // segmentId: segmentIdMap.get(sp.segmentId)!,
11373
+ // connectionName: sp.connectionName,
11374
+ };
11375
+ }
11376
+ const keyData = {
11377
+ hyperParameters: this.hyperParameters,
11378
+ normalizedNodes,
11379
+ normalizedSegmentPoints,
11380
+ mutableHops: this.MUTABLE_HOPS
11381
+ };
11382
+ const cacheKey = `unravelsec:${objectHash(keyData)}`;
11383
+ const cacheToSolveSpaceTransform = {
11384
+ realToCacheTransform,
11385
+ nodeIdMap,
11386
+ segmentIdMap,
11387
+ segmentPointIdMap,
11388
+ reverseNodeIdMap,
11389
+ reverseSegmentIdMap,
11390
+ reverseSegmentPointIdMap
11391
+ };
11392
+ this.cacheKey = cacheKey;
11393
+ this.cacheToSolveSpaceTransform = cacheToSolveSpaceTransform;
11394
+ return { cacheKey, cacheToSolveSpaceTransform };
11395
+ }
11396
+ applyCachedSolution(cachedSolution) {
11397
+ if (cachedSolution.success === false) {
11398
+ this.failed = true;
11399
+ return;
11400
+ }
11401
+ if (!this.cacheToSolveSpaceTransform) {
11402
+ console.error("Cache transform not available to apply cached solution.");
11403
+ return;
11404
+ }
11405
+ const {
11406
+ // realToCacheTransform, // Not needed to apply deltas
11407
+ reverseSegmentPointIdMap,
11408
+ reverseNodeIdMap
11409
+ // Needed if issues depend on node IDs
11410
+ } = this.cacheToSolveSpaceTransform;
11411
+ const pointModifications = /* @__PURE__ */ new Map();
11412
+ for (const [
11413
+ normSpId,
11414
+ normDelta
11415
+ // normDelta.dx and normDelta.dy are strings here
11416
+ ] of cachedSolution.bestCandidatePointModificationsDelta) {
11417
+ const originalSpId = reverseSegmentPointIdMap.get(normSpId);
11418
+ if (!originalSpId) {
11419
+ console.warn(
11420
+ `Could not find original ID for normalized SP ID: ${normSpId} when applying cache.`
11421
+ );
11422
+ continue;
11423
+ }
11424
+ const originalSegmentPoint = this.unravelSection.segmentPointMap.get(originalSpId);
11425
+ if (!originalSegmentPoint) {
11426
+ console.warn(
11427
+ `Could not find original segment point for ID: ${originalSpId} when applying cache.`
11428
+ );
11429
+ continue;
11430
+ }
11431
+ const modifiedPoint = {};
11432
+ if (normDelta.dx !== void 0) {
11433
+ const dxNum = parseFloat(normDelta.dx);
11434
+ if (!Number.isNaN(dxNum)) {
11435
+ modifiedPoint.x = originalSegmentPoint.x + dxNum;
11436
+ } else {
11437
+ console.warn(`Failed to parse cached dx coordinate: ${normDelta.dx}`);
11438
+ }
11439
+ }
11440
+ if (normDelta.dy !== void 0) {
11441
+ const dyNum = parseFloat(normDelta.dy);
11442
+ if (!Number.isNaN(dyNum)) {
11443
+ modifiedPoint.y = originalSegmentPoint.y + dyNum;
11444
+ } else {
11445
+ console.warn(`Failed to parse cached dy coordinate: ${normDelta.dy}`);
11446
+ }
11447
+ }
11448
+ if (normDelta.dz !== void 0) {
11449
+ modifiedPoint.z = originalSegmentPoint.z + normDelta.dz;
11450
+ }
11451
+ if (Object.keys(modifiedPoint).length > 0) {
11452
+ pointModifications.set(originalSpId, modifiedPoint);
11453
+ }
11454
+ }
11455
+ const issues = getIssuesInSection(
11456
+ this.unravelSection,
11457
+ this.nodeMap,
11458
+ pointModifications
11459
+ );
11460
+ this.bestCandidate = {
11461
+ pointModifications,
11462
+ issues,
11463
+ f: cachedSolution.bestCandidateF,
11464
+ g: cachedSolution.bestCandidateF,
11465
+ // Assume g is the main component off for cached solution
11466
+ h: 0,
11467
+ // Heuristic is 0 when solution is loaded
11468
+ operationsPerformed: -1,
11469
+ // Indicate it's from cache, operation count unknown
11470
+ candidateHash: createPointModificationsHash(pointModifications)
11471
+ };
11472
+ this.cacheHit = true;
11473
+ this.solved = true;
11474
+ }
11475
+ attemptToUseCacheSync() {
11476
+ this.hasAttemptedToUseCache = true;
11477
+ if (!this.cacheProvider?.isSyncCache) {
11478
+ console.log(
11479
+ "Cache provider is not synchronous, skipping sync cache check."
11480
+ );
11481
+ return false;
11482
+ }
11483
+ if (!this.cacheKey) {
11484
+ this.computeCacheKeyAndTransform();
11485
+ }
11486
+ if (!this.cacheKey) {
11487
+ console.error("Failed to compute cache key.");
11488
+ return false;
11489
+ }
11490
+ try {
11491
+ const cachedSolution = this.cacheProvider.getCachedSolutionSync(
11492
+ this.cacheKey
11493
+ );
11494
+ if (cachedSolution) {
11495
+ this.applyCachedSolution(cachedSolution);
11496
+ return true;
11497
+ } else {
11498
+ }
11499
+ } catch (error) {
11500
+ console.error("Error attempting to use cache:", error);
11501
+ }
11502
+ return false;
11503
+ }
11504
+ saveToCacheSync() {
11505
+ if (this.failed) {
11506
+ this.cacheProvider?.setCachedSolutionSync(this.cacheKey, {
11507
+ success: false
11508
+ });
11509
+ return;
11510
+ }
11511
+ if (!this.bestCandidate) return;
11512
+ const {
11513
+ // realToCacheTransform, // Not needed to calculate deltas
11514
+ segmentPointIdMap
11515
+ } = this.cacheToSolveSpaceTransform;
11516
+ const normalizedDeltas = [];
11517
+ for (const [
11518
+ originalSpId,
11519
+ modifiedPoint
11520
+ // This contains the absolute modified coordinates {x?, y?, z?}
11521
+ ] of this.bestCandidate.pointModifications.entries()) {
11522
+ const normSpId = segmentPointIdMap.get(originalSpId);
11523
+ if (!normSpId) {
11524
+ console.warn(
11525
+ `Could not find normalized ID for original SP ID: ${originalSpId} when saving to cache.`
11526
+ );
11527
+ continue;
11528
+ }
11529
+ const originalSegmentPoint = this.unravelSection.segmentPointMap.get(originalSpId);
11530
+ if (!originalSegmentPoint) {
11531
+ console.warn(
11532
+ `Could not find original segment point for ID: ${originalSpId} when saving cache.`
11533
+ );
11534
+ continue;
11535
+ }
11536
+ const normDelta = {};
11537
+ let hasDelta = false;
11538
+ if (modifiedPoint.x !== void 0) {
11539
+ const dx = modifiedPoint.x - originalSegmentPoint.x;
11540
+ const approxDx = approximateCoordinate(dx);
11541
+ if (parseFloat(approxDx) !== 0) {
11542
+ normDelta.dx = approxDx;
11543
+ hasDelta = true;
11544
+ }
11545
+ }
11546
+ if (modifiedPoint.y !== void 0) {
11547
+ const dy = modifiedPoint.y - originalSegmentPoint.y;
11548
+ const approxDy = approximateCoordinate(dy);
11549
+ if (parseFloat(approxDy) !== 0) {
11550
+ normDelta.dy = approxDy;
11551
+ hasDelta = true;
11552
+ }
11553
+ }
11554
+ if (modifiedPoint.z !== void 0) {
11555
+ const dz = modifiedPoint.z - originalSegmentPoint.z;
11556
+ if (dz !== 0) {
11557
+ normDelta.dz = dz;
11558
+ hasDelta = true;
11559
+ }
11560
+ }
11561
+ if (hasDelta) {
11562
+ normalizedDeltas.push([normSpId, normDelta]);
11563
+ }
11564
+ }
11565
+ const cachedSolution = {
11566
+ success: true,
11567
+ bestCandidatePointModificationsDelta: normalizedDeltas,
11568
+ bestCandidateF: this.bestCandidate.f
11569
+ };
11570
+ this.cacheProvider?.setCachedSolutionSync(this.cacheKey, cachedSolution);
11571
+ }
11572
+ };
11573
+
10901
11574
  // lib/solvers/UnravelSolver/getDedupedSegments.ts
10902
11575
  var getDedupedSegments = (assignedSegments) => {
10903
11576
  const dedupedSegments = [];
@@ -11024,12 +11697,19 @@ var UnravelMultiSectionSolver = class extends BaseSolver {
11024
11697
  attemptsToFixNode;
11025
11698
  activeSubSolver = null;
11026
11699
  segmentPointMap;
11700
+ cacheProvider = null;
11027
11701
  constructor({
11028
11702
  assignedSegments,
11029
11703
  colorMap,
11030
- nodes
11704
+ nodes,
11705
+ cacheProvider
11031
11706
  }) {
11032
11707
  super();
11708
+ this.stats.successfulOptimizations = 0;
11709
+ this.stats.failedOptimizations = 0;
11710
+ this.stats.cacheHits = 0;
11711
+ this.stats.cacheMisses = 0;
11712
+ this.cacheProvider = cacheProvider ?? null;
11033
11713
  this.MAX_ITERATIONS = 1e6;
11034
11714
  this.dedupedSegments = getDedupedSegments(assignedSegments);
11035
11715
  this.dedupedSegmentMap = /* @__PURE__ */ new Map();
@@ -11112,7 +11792,7 @@ var UnravelMultiSectionSolver = class extends BaseSolver {
11112
11792
  highestPfNodeId,
11113
11793
  (this.attemptsToFixNode.get(highestPfNodeId) ?? 0) + 1
11114
11794
  );
11115
- this.activeSubSolver = new UnravelSectionSolver({
11795
+ this.activeSubSolver = new CachedUnravelSectionSolver({
11116
11796
  dedupedSegments: this.dedupedSegments,
11117
11797
  dedupedSegmentMap: this.dedupedSegmentMap,
11118
11798
  nodeMap: this.nodeMap,
@@ -11123,15 +11803,26 @@ var UnravelMultiSectionSolver = class extends BaseSolver {
11123
11803
  MUTABLE_HOPS: this.MUTABLE_HOPS,
11124
11804
  segmentPointMap: this.segmentPointMap,
11125
11805
  nodeToSegmentPointMap: this.nodeToSegmentPointMap,
11126
- segmentToSegmentPointMap: this.segmentToSegmentPointMap
11806
+ segmentToSegmentPointMap: this.segmentToSegmentPointMap,
11807
+ cacheProvider: this.cacheProvider
11127
11808
  });
11128
11809
  }
11129
11810
  this.activeSubSolver.step();
11130
11811
  const { bestCandidate, originalCandidate, lastProcessedCandidate } = this.activeSubSolver;
11131
- const shouldEarlyStop = this.activeSubSolver.iterationsSinceImprovement > this.MAX_ITERATIONS_WITHOUT_IMPROVEMENT;
11132
- if (this.activeSubSolver.solved || shouldEarlyStop) {
11812
+ if (this.activeSubSolver.failed) {
11813
+ this.stats.failedOptimizations += 1;
11814
+ this.activeSubSolver = null;
11815
+ return;
11816
+ }
11817
+ if (this.activeSubSolver.solved) {
11818
+ if (this.activeSubSolver.cacheHit) {
11819
+ this.stats.cacheHits += 1;
11820
+ } else {
11821
+ this.stats.cacheMisses += 1;
11822
+ }
11133
11823
  const foundBetterSolution = bestCandidate && bestCandidate.g < originalCandidate.g;
11134
11824
  if (foundBetterSolution) {
11825
+ this.stats.successfulOptimizations += 1;
11135
11826
  for (const [
11136
11827
  segmentPointId,
11137
11828
  pointModification
@@ -11147,6 +11838,8 @@ var UnravelMultiSectionSolver = class extends BaseSolver {
11147
11838
  this.computeNodePf(this.nodeMap.get(nodeId))
11148
11839
  );
11149
11840
  }
11841
+ } else {
11842
+ this.stats.failedOptimizations += 1;
11150
11843
  }
11151
11844
  this.activeSubSolver = null;
11152
11845
  }
@@ -15091,6 +15784,7 @@ var AutoroutingPipelineSolver = class extends BaseSolver {
15091
15784
  }
15092
15785
  this.connMap = getConnectivityMapFromSimpleRouteJson(srj);
15093
15786
  this.colorMap = getColorMap(srj, this.connMap);
15787
+ this.cacheProvider = opts.cacheProvider ?? null;
15094
15788
  this.startTimeOfPhase = {};
15095
15789
  this.endTimeOfPhase = {};
15096
15790
  this.timeSpentOnPhase = {};
@@ -15121,6 +15815,7 @@ var AutoroutingPipelineSolver = class extends BaseSolver {
15121
15815
  connMap;
15122
15816
  srjWithPointPairs;
15123
15817
  capacityNodes = null;
15818
+ cacheProvider = null;
15124
15819
  pipelineDef = [
15125
15820
  definePipelineStep(
15126
15821
  "netToPointPairsSolver",
@@ -15268,7 +15963,8 @@ var AutoroutingPipelineSolver = class extends BaseSolver {
15268
15963
  {
15269
15964
  assignedSegments: cms.segmentToPointSolver?.solvedSegments || [],
15270
15965
  colorMap: cms.colorMap,
15271
- nodes: cms.capacityNodes
15966
+ nodes: cms.capacityNodes,
15967
+ cacheProvider: this.cacheProvider
15272
15968
  }
15273
15969
  ]
15274
15970
  ),