@gridspace/raster-path 1.0.2 → 1.0.4

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.
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Workload Calculator for Radial Toolpath Generation
3
+ *
4
+ * Estimates GPU workload and optimal batch sizing based on:
5
+ * - Model geometry (triangle count, bounds)
6
+ * - Rasterization parameters (resolution, angular step)
7
+ * - Tool characteristics (diameter)
8
+ *
9
+ * Based on empirical testing:
10
+ * - Tool diameter scales LINEARLY (not quadratically)
11
+ * - Resolution scales INVERSELY SQUARED (finer = more pixels)
12
+ * - Angular step scales INVERSELY LINEAR (finer = more strips)
13
+ * - Triangle count has complex relationship with geometry
14
+ */
15
+
16
+ export class WorkloadCalculator {
17
+ constructor() {
18
+ // Calibration constants (to be tuned from test data)
19
+ this.BASELINE_TOOL_DIAMETER = 2.5; // mm (most common size)
20
+ this.BASELINE_RESOLUTION = 0.05; // mm (reference resolution)
21
+ this.BASELINE_ANGULAR_STEP = 1.0; // degrees
22
+
23
+ // Minimum batch parameters
24
+ this.MIN_ANGLES_PER_BATCH = 20; // Absolute minimum
25
+ this.TARGET_MIN_BATCH_TIME_MS = 400; // Target minimum batch duration
26
+
27
+ // GPU overhead constants
28
+ this.GPU_DISPATCH_OVERHEAD_MS = 25; // Per-batch overhead
29
+
30
+ // Watchdog timeout thresholds
31
+ this.WATCHDOG_WARNING_MS = 2000; // Warn if single operation > 2s
32
+ this.WATCHDOG_CRITICAL_MS = 5000; // Critical if single operation > 5s
33
+ this.WATCHDOG_MAX_SAFE_MS = 1500; // Target to stay under watchdog
34
+ }
35
+
36
+ /**
37
+ * Calculate workload for radial toolpath generation
38
+ *
39
+ * @param {Object} params
40
+ * @param {number} params.triangleCount - Number of terrain triangles
41
+ * @param {Object} params.bounds - Model bounds {minX, maxX, minY, maxY, minZ, maxZ}
42
+ * @param {number} params.resolution - Grid resolution in mm
43
+ * @param {number} params.rotationStep - Angular step in degrees
44
+ * @param {number} params.toolDiameter - Tool diameter in mm
45
+ * @returns {Object} Workload estimate with recommended batch size
46
+ */
47
+ calculateRadialWorkload(params) {
48
+ const {
49
+ triangleCount,
50
+ bounds,
51
+ resolution,
52
+ rotationStep,
53
+ toolDiameter
54
+ } = params;
55
+
56
+ // Calculate radial geometry
57
+ const radialRadius = Math.sqrt(
58
+ Math.max(Math.abs(bounds.minY), Math.abs(bounds.maxY)) ** 2 +
59
+ Math.max(Math.abs(bounds.minZ), Math.abs(bounds.maxZ)) ** 2
60
+ );
61
+ const radialHeight = bounds.maxX - bounds.minX;
62
+ const radialVolume = Math.PI * radialRadius * radialRadius * radialHeight;
63
+
64
+ // Calculate grid dimensions
65
+ const gridWidth = Math.ceil((2 * radialRadius) / resolution);
66
+ const gridHeight = Math.ceil(radialHeight / resolution);
67
+ const pixelsPerAngle = gridWidth * gridHeight;
68
+
69
+ // Calculate number of strips
70
+ const numAngles = Math.ceil(360 / rotationStep);
71
+
72
+ // Calculate triangle density
73
+ const triangleDensity = triangleCount / radialVolume;
74
+
75
+ // Calculate work factors (normalized to baseline)
76
+ const resolutionFactor = (this.BASELINE_RESOLUTION / resolution) ** 2; // Inverse square
77
+ const angularFactor = this.BASELINE_ANGULAR_STEP / rotationStep; // Inverse linear
78
+ const toolFactor = toolDiameter / this.BASELINE_TOOL_DIAMETER; // Linear
79
+
80
+ // Formula 6 (User's proposal + tool diameter):
81
+ // work ∝ triangleDensity × (1/resolution²) × (1/angularStep) × toolDiameter
82
+ const workScore = triangleDensity * resolutionFactor * angularFactor * toolFactor;
83
+
84
+ // Formula 2 (Simpler):
85
+ // work ∝ triangleCount × pixelsPerAngle × toolDiameter
86
+ const workPerAngle = triangleCount * pixelsPerAngle * toolFactor;
87
+
88
+ // Estimate time per angle (calibrated from empirical data)
89
+ // Base calibration: ~3-5ms per angle for baseline parameters
90
+ const baseTimePerAngle = 4.0; // ms (to be tuned from test data)
91
+ const estimatedTimePerAngle = baseTimePerAngle * workScore / 1000;
92
+
93
+ // Calculate optimal batch size based on target batch duration
94
+ const optimalAnglesPerBatch = Math.ceil(
95
+ this.TARGET_MIN_BATCH_TIME_MS / estimatedTimePerAngle
96
+ );
97
+
98
+ // Apply constraints
99
+ const recommendedAnglesPerBatch = Math.max(
100
+ this.MIN_ANGLES_PER_BATCH,
101
+ Math.min(optimalAnglesPerBatch, numAngles)
102
+ );
103
+
104
+ const numBatches = Math.ceil(numAngles / recommendedAnglesPerBatch);
105
+
106
+ // Estimate total time
107
+ const rasterTimePerAngle = estimatedTimePerAngle * 0.3; // ~30% raster
108
+ const toolpathTimePerAngle = estimatedTimePerAngle * 0.7; // ~70% toolpath
109
+
110
+ const totalRasterTime = rasterTimePerAngle * numAngles;
111
+ const totalToolpathTime = toolpathTimePerAngle * numAngles;
112
+ const totalOverhead = this.GPU_DISPATCH_OVERHEAD_MS * numBatches;
113
+ const estimatedTotalTime = totalRasterTime + totalToolpathTime + totalOverhead;
114
+
115
+ // Estimate time per batch (for watchdog detection)
116
+ const estimatedBatchTime = (estimatedTimePerAngle * recommendedAnglesPerBatch) + this.GPU_DISPATCH_OVERHEAD_MS;
117
+
118
+ // Watchdog timeout risk assessment
119
+ let watchdogRisk = 'safe';
120
+ let watchdogMessage = 'Batch size is within safe watchdog limits';
121
+ let watchdogSuggestion = null;
122
+
123
+ if (estimatedBatchTime >= this.WATCHDOG_CRITICAL_MS) {
124
+ watchdogRisk = 'critical';
125
+ watchdogMessage = `CRITICAL: Estimated batch time (${estimatedBatchTime.toFixed(0)}ms) exceeds watchdog kill threshold (${this.WATCHDOG_CRITICAL_MS}ms)`;
126
+ // Calculate safer batch size
127
+ const safeAnglesPerBatch = Math.floor(this.WATCHDOG_MAX_SAFE_MS / estimatedTimePerAngle);
128
+ watchdogSuggestion = {
129
+ anglesPerBatch: Math.max(this.MIN_ANGLES_PER_BATCH, safeAnglesPerBatch),
130
+ message: `Reduce batch size to ${Math.max(this.MIN_ANGLES_PER_BATCH, safeAnglesPerBatch)} angles or adjust parameters`
131
+ };
132
+ } else if (estimatedBatchTime >= this.WATCHDOG_WARNING_MS) {
133
+ watchdogRisk = 'warning';
134
+ watchdogMessage = `WARNING: Estimated batch time (${estimatedBatchTime.toFixed(0)}ms) approaching watchdog threshold (${this.WATCHDOG_WARNING_MS}ms)`;
135
+ // Calculate safer batch size
136
+ const safeAnglesPerBatch = Math.floor(this.WATCHDOG_MAX_SAFE_MS / estimatedTimePerAngle);
137
+ watchdogSuggestion = {
138
+ anglesPerBatch: Math.max(this.MIN_ANGLES_PER_BATCH, safeAnglesPerBatch),
139
+ message: `Consider reducing batch size to ${Math.max(this.MIN_ANGLES_PER_BATCH, safeAnglesPerBatch)} angles`
140
+ };
141
+ } else if (estimatedBatchTime >= this.WATCHDOG_MAX_SAFE_MS) {
142
+ watchdogRisk = 'caution';
143
+ watchdogMessage = `CAUTION: Estimated batch time (${estimatedBatchTime.toFixed(0)}ms) above safe target (${this.WATCHDOG_MAX_SAFE_MS}ms)`;
144
+ }
145
+
146
+ return {
147
+ // Workload metrics
148
+ workScore,
149
+ workPerAngle,
150
+ triangleDensity,
151
+ pixelsPerAngle,
152
+
153
+ // Grid info
154
+ gridWidth,
155
+ gridHeight,
156
+ numAngles,
157
+
158
+ // Timing estimates
159
+ estimatedTimePerAngle,
160
+ estimatedBatchTime,
161
+ estimatedTotalTime,
162
+ totalRasterTime,
163
+ totalToolpathTime,
164
+ totalOverhead,
165
+
166
+ // Batch recommendations
167
+ recommendedAnglesPerBatch,
168
+ numBatches,
169
+ overheadPercent: (totalOverhead / estimatedTotalTime) * 100,
170
+
171
+ // Watchdog risk assessment
172
+ watchdog: {
173
+ risk: watchdogRisk,
174
+ message: watchdogMessage,
175
+ estimatedBatchTime,
176
+ suggestion: watchdogSuggestion
177
+ },
178
+
179
+ // Work factors
180
+ factors: {
181
+ resolution: resolutionFactor,
182
+ angular: angularFactor,
183
+ tool: toolFactor,
184
+ density: triangleDensity
185
+ }
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Calculate memory requirements for radial mode
191
+ *
192
+ * @param {Object} params - Same as calculateRadialWorkload
193
+ * @returns {Object} Memory estimate in MB
194
+ */
195
+ calculateRadialMemory(params) {
196
+ const { bounds, resolution, rotationStep, toolDiameter } = params;
197
+
198
+ const radialRadius = Math.sqrt(
199
+ Math.max(Math.abs(bounds.minY), Math.abs(bounds.maxY)) ** 2 +
200
+ Math.max(Math.abs(bounds.minZ), Math.abs(bounds.maxZ)) ** 2
201
+ );
202
+ const radialHeight = bounds.maxX - bounds.minX;
203
+
204
+ const gridWidth = Math.ceil((2 * radialRadius) / resolution);
205
+ const gridHeight = Math.ceil(radialHeight / resolution);
206
+ const numAngles = Math.ceil(360 / rotationStep);
207
+
208
+ // Each pixel is 4 bytes (float32)
209
+ const bytesPerAngle = gridWidth * gridHeight * 4;
210
+ const totalMemoryBytes = bytesPerAngle * numAngles;
211
+ const totalMemoryMB = totalMemoryBytes / (1024 * 1024);
212
+
213
+ return {
214
+ gridWidth,
215
+ gridHeight,
216
+ numAngles,
217
+ bytesPerAngle,
218
+ totalMemoryBytes,
219
+ totalMemoryMB,
220
+ exceedsLimit: totalMemoryMB > 256 // Typical GPU limit
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Suggest optimal parameters for memory-constrained scenarios
226
+ *
227
+ * @param {Object} params - Same as calculateRadialWorkload
228
+ * @param {number} maxMemoryMB - Maximum allowed memory in MB
229
+ * @returns {Object} Suggested parameter adjustments
230
+ */
231
+ suggestOptimalParameters(params, maxMemoryMB = 256) {
232
+ const memory = this.calculateRadialMemory(params);
233
+
234
+ if (!memory.exceedsLimit && memory.totalMemoryMB < maxMemoryMB) {
235
+ return {
236
+ needsAdjustment: false,
237
+ currentMemory: memory.totalMemoryMB,
238
+ message: 'Current parameters are within memory limits'
239
+ };
240
+ }
241
+
242
+ // Calculate required scale factor
243
+ const scaleFactor = Math.sqrt(maxMemoryMB / memory.totalMemoryMB);
244
+
245
+ // Suggest coarser resolution
246
+ const suggestedResolution = params.resolution / scaleFactor;
247
+ const roundedResolution = Math.ceil(suggestedResolution * 1000) / 1000;
248
+
249
+ // Suggest coarser angular step
250
+ const suggestedAngularStep = params.rotationStep / scaleFactor;
251
+ const roundedAngularStep = Math.ceil(suggestedAngularStep * 10) / 10;
252
+
253
+ return {
254
+ needsAdjustment: true,
255
+ currentMemory: memory.totalMemoryMB,
256
+ targetMemory: maxMemoryMB,
257
+ suggestions: {
258
+ resolution: roundedResolution,
259
+ angularStep: roundedAngularStep
260
+ },
261
+ message: `Memory limit exceeded. Suggested: resolution=${roundedResolution}mm or angularStep=${roundedAngularStep}°`
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Pretty-print workload analysis
267
+ *
268
+ * @param {Object} params - Workload parameters
269
+ */
270
+ printAnalysis(params) {
271
+ const workload = this.calculateRadialWorkload(params);
272
+ const memory = this.calculateRadialMemory(params);
273
+
274
+ console.log('=== Workload Analysis ===');
275
+ console.log(`Model: ${params.triangleCount.toLocaleString()} triangles`);
276
+ console.log(`Resolution: ${params.resolution}mm`);
277
+ console.log(`Angular step: ${params.rotationStep}°`);
278
+ console.log(`Tool diameter: ${params.toolDiameter}mm`);
279
+ console.log('');
280
+ console.log('Grid:');
281
+ console.log(` ${workload.gridWidth} × ${workload.gridHeight} pixels`);
282
+ console.log(` ${workload.numAngles} strips`);
283
+ console.log(` ${workload.pixelsPerAngle.toLocaleString()} pixels per strip`);
284
+ console.log('');
285
+ console.log('Work factors (vs baseline):');
286
+ console.log(` Resolution: ${workload.factors.resolution.toFixed(2)}x`);
287
+ console.log(` Angular: ${workload.factors.angular.toFixed(2)}x`);
288
+ console.log(` Tool: ${workload.factors.tool.toFixed(2)}x`);
289
+ console.log(` Density: ${workload.factors.density.toFixed(2)} tri/mm³`);
290
+ console.log('');
291
+ console.log('Estimated timing:');
292
+ console.log(` Per angle: ${workload.estimatedTimePerAngle.toFixed(2)}ms`);
293
+ console.log(` Total: ${workload.estimatedTotalTime.toFixed(0)}ms`);
294
+ console.log('');
295
+ console.log('Batching:');
296
+ console.log(` Recommended: ${workload.recommendedAnglesPerBatch} angles/batch`);
297
+ console.log(` Batches: ${workload.numBatches}`);
298
+ console.log(` Overhead: ${workload.overheadPercent.toFixed(1)}%`);
299
+ console.log(` Est. batch time: ${workload.estimatedBatchTime.toFixed(0)}ms`);
300
+ console.log('');
301
+ console.log('Watchdog Risk:');
302
+ const riskSymbol = {
303
+ 'safe': '✓',
304
+ 'caution': '⚠️',
305
+ 'warning': '⚠️⚠️',
306
+ 'critical': '🛑'
307
+ }[workload.watchdog.risk];
308
+ console.log(` ${riskSymbol} ${workload.watchdog.risk.toUpperCase()}`);
309
+ console.log(` ${workload.watchdog.message}`);
310
+ if (workload.watchdog.suggestion) {
311
+ console.log(` Suggestion: ${workload.watchdog.suggestion.message}`);
312
+ }
313
+ console.log('');
314
+ console.log('Memory:');
315
+ console.log(` ${memory.totalMemoryMB.toFixed(1)} MB`);
316
+ console.log(` ${memory.exceedsLimit ? '⚠️ EXCEEDS LIMIT' : '✓ Within limits'}`);
317
+ }
318
+ }