cyclecad 3.10.4 → 3.12.0

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,939 @@
1
+ /* AI Engineering Analyst v1.0 — engineering analysis with cited methodology.
2
+ *
3
+ * v1 scope: bolted-joint analysis (VDI 2230 / Shigley methodology).
4
+ * Verified against MecAgent demo problem (4×M12/10.9, 18kN shear, 18kN axial, 420Nm moment,
5
+ * μ=0.16, preload 39kN/bolt, BCD 96mm, K_s=1.5) — expected F_friction=24960N,
6
+ * F_max_tensile=6687.5N, σ_vm=558MPa.
7
+ *
8
+ * Future scope (tasks #11 + #12): gears (AGMA), shafts (Goodman/Soderberg), bearings (L10),
9
+ * welds (throat stress), plus RAG citations.
10
+ *
11
+ * Architecture:
12
+ * - Pure-JS analytical core (0 deps) — deterministic, unit-tested.
13
+ * - Natural-language parser extracts structured parameters from free-text.
14
+ * - Form UI with live recompute + LaTeX render (KaTeX, loaded on demand from CDN).
15
+ * - LLM layer (optional v1.1) calls the analytical core for every number — never fabricates results.
16
+ */
17
+ (function(){
18
+ 'use strict';
19
+ window.CycleCAD = window.CycleCAD || {};
20
+
21
+ // ===================================================================
22
+ // REFERENCE DATA — ISO 898-1 mechanical property classes for carbon steel bolts
23
+ // Values: {nominalTensile R_m, proofStress R_p0.2, yieldStrength R_el} all in MPa
24
+ // ===================================================================
25
+ const STEEL_GRADES = Object.freeze({
26
+ '4.6': { R_m: 400, R_p02: 225, R_el: 240, label: '4.6 (low-carbon steel, mild)' },
27
+ '4.8': { R_m: 400, R_p02: 310, R_el: 320, label: '4.8 (low-carbon)' },
28
+ '5.6': { R_m: 500, R_p02: 300, R_el: 300, label: '5.6 (medium-carbon)' },
29
+ '5.8': { R_m: 500, R_p02: 380, R_el: 400, label: '5.8 (medium-carbon)' },
30
+ '6.8': { R_m: 600, R_p02: 440, R_el: 480, label: '6.8 (medium-carbon)' },
31
+ '8.8': { R_m: 800, R_p02: 580, R_el: 640, label: '8.8 (quenched & tempered, most common)' },
32
+ '10.9': { R_m: 1000, R_p02: 830, R_el: 900, label: '10.9 (alloy, quenched & tempered)' },
33
+ '12.9': { R_m: 1200, R_p02: 970, R_el: 1080, label: '12.9 (alloy, Q&T high-strength)' }
34
+ });
35
+
36
+ // DIN 13 stress cross-section area A_s in mm² for standard metric coarse threads.
37
+ // A_s = π/4 · ((d_2 + d_3) / 2)² where d_2 is pitch dia, d_3 is minor dia (nut thread root).
38
+ const BOLT_STRESS_AREA = Object.freeze({
39
+ M3: 5.03, M3_5: 6.78, M4: 8.78, M5: 14.2, M6: 20.1, M7: 28.9,
40
+ M8: 36.6, M10: 58.0, M12: 84.3, M14: 115, M16: 157, M18: 192,
41
+ M20: 245, M22: 303, M24: 353, M27: 459, M30: 561,
42
+ M33: 694, M36: 817, M39: 976, M42: 1120, M45: 1300, M48: 1470,
43
+ M52: 1760, M56: 2030
44
+ });
45
+
46
+ // Nominal (major) diameter in mm for standard metric coarse threads.
47
+ const BOLT_MAJOR_DIA = Object.freeze({
48
+ M3: 3, M4: 4, M5: 5, M6: 6, M8: 8, M10: 10, M12: 12, M14: 14, M16: 16,
49
+ M18: 18, M20: 20, M22: 22, M24: 24, M27: 27, M30: 30, M33: 33, M36: 36,
50
+ M39: 39, M42: 42, M45: 45, M48: 48, M52: 52, M56: 56
51
+ });
52
+
53
+ // Typical friction coefficients for clamped-joint faying surfaces (from VDI 2230 Table A6).
54
+ const FRICTION_PRESETS = Object.freeze({
55
+ 'dry_steel_steel': 0.16,
56
+ 'oiled_steel_steel': 0.12,
57
+ 'aluminum_aluminum': 0.18,
58
+ 'zinc_coated': 0.14,
59
+ 'hot_dip_galvanized': 0.36,
60
+ 'phosphated': 0.16,
61
+ 'blasted_clean': 0.45
62
+ });
63
+
64
+ // ===================================================================
65
+ // ANALYTICAL CORE — bolted-joint slip + tension + combined stress
66
+ // ===================================================================
67
+
68
+ /**
69
+ * Bolted joint safety analysis (VDI 2230 + Shigley approach).
70
+ *
71
+ * @param {object} p Input parameters.
72
+ * @param {number} p.boltCount Number of bolts z (≥1).
73
+ * @param {string} p.grade Steel grade, e.g. '10.9'.
74
+ * @param {string} p.thread Thread designation, e.g. 'M12'.
75
+ * @param {number} p.preload Per-bolt preload F_V [N].
76
+ * @param {number} p.shearForce External transverse/shear force F_Q [N].
77
+ * @param {number} p.axialForce External axial/separating force F_A [N].
78
+ * @param {number} p.moment In-plane moment M [N·mm].
79
+ * @param {number} p.bcd Bolt-circle diameter [mm].
80
+ * @param {number} p.friction Faying-surface coefficient μ.
81
+ * @param {number} p.safetyFactor Required slip safety factor K_s.
82
+ * @param {number} [p.frictionInterfaces=1] Number of friction-bearing interfaces n.
83
+ * @returns {object} Structured result with inputs, slip/tension/stress checks, verdict.
84
+ */
85
+ function boltedJointAnalysis(p) {
86
+ // Input normalisation
87
+ const z = Math.max(1, Math.round(Number(p.boltCount) || 4));
88
+ const grade = STEEL_GRADES[p.grade] ? p.grade : '8.8';
89
+ const thread = BOLT_STRESS_AREA[p.thread] ? p.thread : 'M12';
90
+ const F_preload = Math.max(0, Number(p.preload) || 0);
91
+ const F_shear = Math.max(0, Number(p.shearForce) || 0);
92
+ const F_axial = Math.max(0, Number(p.axialForce) || 0);
93
+ const M = Math.max(0, Number(p.moment) || 0);
94
+ const bcd = Math.max(0, Number(p.bcd) || 0);
95
+ const mu = Math.max(0.01, Number(p.friction) || 0.15);
96
+ const K_s = Math.max(1, Number(p.safetyFactor) || 1.5);
97
+ const n_interfaces = Math.max(1, Math.round(Number(p.frictionInterfaces) || 1));
98
+
99
+ const A_s = BOLT_STRESS_AREA[thread];
100
+ const d = BOLT_MAJOR_DIA[thread];
101
+ const spec = STEEL_GRADES[grade];
102
+ const R_p02 = spec.R_p02;
103
+ const R_m = spec.R_m;
104
+ const r = bcd / 2;
105
+
106
+ // --- CHECK 1: Slip resistance ----------------------------------------------
107
+ // Friction capacity: F_friction = μ · n · z · F_V
108
+ const F_friction = mu * n_interfaces * z * F_preload;
109
+
110
+ // Per-bolt tangential force from in-plane moment (at BCD):
111
+ // F_M_tangential,i = M · r_i / Σr_j² → for a uniform circle: M / (z · r)
112
+ const F_moment_tangential_per_bolt = (r > 0) ? M / (z * r) : 0;
113
+
114
+ // Worst-case per-bolt tangential force (shear/z plus moment contribution, aligned):
115
+ const F_bolt_tangential_max = F_shear / z + F_moment_tangential_per_bolt;
116
+
117
+ // Aggregate tangential resultant that the joint must resist (sum of worst-case bolt forces):
118
+ const F_shear_total = z * F_bolt_tangential_max;
119
+ const F_required_slip = K_s * F_shear_total;
120
+ const slipSafe = F_friction >= F_required_slip;
121
+ const slipMargin = F_required_slip > 0 ? F_friction / F_required_slip : Infinity;
122
+
123
+ // --- CHECK 2: Bolt tension (worst-loaded bolt) ------------------------------
124
+ // External tension per bolt: F_A/z + contribution from moment at worst position
125
+ // (axial moment component adds on the tension side of the BCD)
126
+ const F_axial_per_bolt = F_axial / z;
127
+ const F_moment_axial_per_bolt = (r > 0) ? M / (z * r) : 0;
128
+ const F_max_external = F_axial_per_bolt + F_moment_axial_per_bolt;
129
+
130
+ // Simplified: assume load factor Φ = 1 (rigid joint). In reality 0.1-0.3 for gasketed.
131
+ // Full VDI 2230 uses F_S_max = F_V + Φ · F_A_ext. For v1, conservative (Φ=1):
132
+ const F_bolt_total = F_preload + F_max_external;
133
+
134
+ // --- CHECK 3: Combined stress (Von Mises) -----------------------------------
135
+ // σ = F_bolt_total / A_s (tensile)
136
+ // τ = F_bolt_tangential_max / A_s (shear from transverse loads + moment)
137
+ // σ_vm = √(σ² + 3τ²) (von Mises for uniaxial tension + shear)
138
+ const sigma = F_bolt_total / A_s;
139
+ const tau = F_bolt_tangential_max / A_s;
140
+ const sigma_vm = Math.sqrt(sigma * sigma + 3 * tau * tau);
141
+ const stressUtilization = R_p02 > 0 ? sigma_vm / R_p02 : Infinity;
142
+ const stressSafe = sigma_vm < R_p02;
143
+
144
+ // --- VERDICT ----------------------------------------------------------------
145
+ // Classification: joint is SAFE if both slip and stress checks pass.
146
+ // If stress passes but slip fails, joint is a BEARING-TYPE joint — bolts transmit shear
147
+ // directly. Still safe if stress is within proof strength.
148
+ let verdict, verdictClass, notes = [];
149
+ if (slipSafe && stressSafe) {
150
+ verdict = 'SAFE';
151
+ verdictClass = 'pass';
152
+ notes.push('Preload prevents slip; bolt stress stays below proof strength.');
153
+ } else if (!slipSafe && stressSafe) {
154
+ verdict = 'SAFE (bearing-type)';
155
+ verdictClass = 'warn';
156
+ notes.push('Slip resistance marginal — joint relies on bolt shank shear.');
157
+ notes.push('Consider increasing preload (higher Q_F) or adding friction (higher μ) to restore friction-type safety.');
158
+ } else {
159
+ verdict = 'UNSAFE';
160
+ verdictClass = 'fail';
161
+ if (!stressSafe) notes.push('Combined stress σ_vm = ' + sigma_vm.toFixed(1) + ' MPa exceeds proof strength ' + R_p02 + ' MPa.');
162
+ if (!slipSafe) notes.push('Insufficient friction capacity — bolts must also resist direct shear.');
163
+ }
164
+
165
+ return {
166
+ inputs: { z, grade, thread, d, A_s, F_preload, F_shear, F_axial, M, bcd, r, mu, K_s, n_interfaces, R_p02, R_m, gradeLabel: spec.label },
167
+ slipResistance: {
168
+ F_friction, F_moment_tangential_per_bolt, F_bolt_tangential_max,
169
+ F_shear_total, F_required: F_required_slip, margin: slipMargin, safe: slipSafe
170
+ },
171
+ tensionCheck: {
172
+ F_axial_per_bolt, F_moment_axial_per_bolt, F_max_external,
173
+ F_preload, F_bolt_total
174
+ },
175
+ combinedStress: {
176
+ sigma, tau, sigma_vm, R_p02, utilization: stressUtilization, safe: stressSafe
177
+ },
178
+ verdict, verdictClass, notes
179
+ };
180
+ }
181
+
182
+ // ===================================================================
183
+ // GEAR MATERIAL DATA — AGMA Grade 1 steel allowables (Shigley Table 14-3 / 14-6)
184
+ // Keys: hardness in HB (Brinell). Values: {S_t, S_c} in MPa (converted from psi).
185
+ // S_t: allowable bending stress. S_c: allowable contact (surface) stress.
186
+ // Formulas for through-hardened steel (AGMA 2001):
187
+ // S_t = 77 * HB + 12,800 psi (Grade 1 bending)
188
+ // S_c = 322 * HB + 29,100 psi (Grade 1 contact)
189
+ // 1 psi = 0.00689476 MPa.
190
+ // ===================================================================
191
+ /**
192
+ * Allowable bending stress for Grade 1 through-hardened gear steel (AGMA 2001).
193
+ * @param {number} HB Brinell hardness (180–400 typical).
194
+ * @returns {number} S_t in MPa.
195
+ */
196
+ function gearAllowableBending(HB) {
197
+ return (77 * HB + 12800) * 0.00689476;
198
+ }
199
+ /**
200
+ * Allowable contact stress for Grade 1 through-hardened gear steel (AGMA 2001).
201
+ * @param {number} HB Brinell hardness.
202
+ * @returns {number} S_c in MPa.
203
+ */
204
+ function gearAllowableContact(HB) {
205
+ return (322 * HB + 29100) * 0.00689476;
206
+ }
207
+
208
+ // AGMA geometry factor J (bending) — approximated from Shigley Fig. 14-6
209
+ // for external spur gears at 20° pressure angle. Interpolated by number of teeth.
210
+ const J_TABLE = [
211
+ [12, 0.245], [14, 0.265], [17, 0.295], [20, 0.32], [25, 0.345],
212
+ [30, 0.365], [35, 0.38], [40, 0.39], [50, 0.41], [75, 0.435], [100, 0.45]
213
+ ];
214
+ function gearGeometryJ(teeth) {
215
+ const z = Math.max(J_TABLE[0][0], Math.min(J_TABLE[J_TABLE.length-1][0], teeth));
216
+ for (let i = 0; i < J_TABLE.length - 1; i++) {
217
+ const [z1, j1] = J_TABLE[i], [z2, j2] = J_TABLE[i+1];
218
+ if (z >= z1 && z <= z2) return j1 + (j2 - j1) * (z - z1) / (z2 - z1);
219
+ }
220
+ return 0.4;
221
+ }
222
+
223
+ // AGMA geometry factor I (pitting) — external gear pair, 20° pressure angle.
224
+ // Approximated from Shigley Eq. 14-23 with m_N = 1 for spur gears.
225
+ function gearGeometryI(pinionTeeth, gearTeeth, pressureAngle) {
226
+ const phi = (pressureAngle || 20) * Math.PI / 180;
227
+ const mG = gearTeeth / pinionTeeth;
228
+ return (Math.cos(phi) * Math.sin(phi) / 2) * (mG / (mG + 1));
229
+ }
230
+
231
+ /**
232
+ * Spur gear AGMA bending + pitting analysis (Shigley Ch. 14).
233
+ *
234
+ * Uses the fundamental AGMA 2001 stress equations with sensible default modifying factors.
235
+ * For safety-critical applications, confirm each K-factor per AGMA 908-B89.
236
+ *
237
+ * @param {object} p
238
+ * @param {number} p.pinionTeeth z_P — pinion tooth count.
239
+ * @param {number} p.gearTeeth z_G — gear tooth count.
240
+ * @param {number} p.module m — in mm.
241
+ * @param {number} p.faceWidth F — in mm.
242
+ * @param {number} p.torque T_P — torque on pinion in N·m.
243
+ * @param {number} p.pinionHB HB_P — Brinell hardness of pinion.
244
+ * @param {number} p.gearHB HB_G — Brinell hardness of gear.
245
+ * @param {number} [p.overload=1.0] K_o — 1.0 uniform / 1.25 moderate / 1.5 heavy shock.
246
+ * @param {number} [p.dynamic=1.1] K_v — dynamic factor (≈1.0 precision, 1.1–1.3 typical).
247
+ * @param {number} [p.loadDist=1.3] K_m — load distribution (1.3 good alignment).
248
+ * @param {number} [p.reliability=1.0] K_R — 1.0 @ 99% reliability, 1.25 @ 99.9%.
249
+ * @param {number} [p.pressureAngle=20] φ — pressure angle in degrees.
250
+ * @param {number} [p.Z_E=190] Elastic coefficient for steel-steel (MPa^0.5).
251
+ * @returns {object} {inputs, pinion:{…}, gear:{…}, verdict, …}
252
+ */
253
+ function spurGearAnalysis(p) {
254
+ const z_P = Math.max(12, Math.round(Number(p.pinionTeeth) || 20));
255
+ const z_G = Math.max(12, Math.round(Number(p.gearTeeth) || 40));
256
+ const m = Math.max(0.5, Number(p.module) || 2);
257
+ const F = Math.max(3, Number(p.faceWidth) || 25);
258
+ const T_P = Math.max(0, Number(p.torque) || 0);
259
+ const HB_P = Math.max(150, Number(p.pinionHB) || 240);
260
+ const HB_G = Math.max(150, Number(p.gearHB) || HB_P);
261
+ const K_o = Math.max(1, Number(p.overload) || 1.0);
262
+ const K_v = Math.max(1, Number(p.dynamic) || 1.1);
263
+ const K_m = Math.max(1, Number(p.loadDist) || 1.3);
264
+ const K_R = Math.max(1, Number(p.reliability) || 1.0);
265
+ const phi = Number(p.pressureAngle) || 20;
266
+ const Z_E = Number(p.Z_E) || 190;
267
+ const K_s = 1.0; // size factor — ignore for typical sizes
268
+ const K_B = 1.0; // rim thickness — solid blank
269
+ const C_f = 1.0; // surface condition — clean-cut
270
+ const Y_N = 1.0, Z_N = 1.0, C_H = 1.0, K_T = 1.0; // nominal life factors
271
+ const mG = z_G / z_P;
272
+
273
+ // Pitch diameters and tangential load
274
+ const d_P = m * z_P; // pinion pitch dia in mm
275
+ const d_G = m * z_G; // gear pitch dia
276
+ const T_Pnmm = T_P * 1000; // convert N·m → N·mm
277
+ const W_t = (d_P > 0) ? (2 * T_Pnmm) / d_P : 0; // tangential load N
278
+
279
+ // Geometry factors
280
+ const J_P = gearGeometryJ(z_P);
281
+ const J_G = gearGeometryJ(z_G);
282
+ const I = gearGeometryI(z_P, z_G, phi);
283
+
284
+ // AGMA bending stress (both gears see same W_t but different J)
285
+ const sigma_b_P = (W_t * K_o * K_v * K_s * K_m * K_B) / (F * m * J_P);
286
+ const sigma_b_G = (W_t * K_o * K_v * K_s * K_m * K_B) / (F * m * J_G);
287
+
288
+ // AGMA contact stress (same magnitude for both meshing teeth)
289
+ const sigma_c = (W_t > 0 && d_P > 0 && I > 0)
290
+ ? Z_E * Math.sqrt((W_t * K_o * K_v * K_s * K_m * C_f) / (d_P * F * I))
291
+ : 0;
292
+
293
+ // Allowable stresses (material)
294
+ const St_P = gearAllowableBending(HB_P);
295
+ const St_G = gearAllowableBending(HB_G);
296
+ const Sc_P = gearAllowableContact(HB_P);
297
+ const Sc_G = gearAllowableContact(HB_G);
298
+
299
+ // Factors of safety — Shigley Eq. 14-41 / 14-42 simplified
300
+ const SF_bending_P = St_P * Y_N / (sigma_b_P * K_T * K_R);
301
+ const SF_bending_G = St_G * Y_N / (sigma_b_G * K_T * K_R);
302
+ const SF_contact_P = (Sc_P * Z_N * C_H) / (sigma_c * K_T * K_R);
303
+ const SF_contact_G = (Sc_G * Z_N * C_H) / (sigma_c * K_T * K_R);
304
+
305
+ const SF_min = Math.min(SF_bending_P, SF_bending_G, SF_contact_P, SF_contact_G);
306
+ const safe = SF_min >= 1.0;
307
+ const verdict = SF_min >= 2.0 ? 'SAFE (margin ≥ 2)' :
308
+ SF_min >= 1.5 ? 'SAFE (margin ≥ 1.5 — industry typical)' :
309
+ SF_min >= 1.0 ? 'MARGINAL (factor < 1.5 — review assumptions)' :
310
+ 'UNSAFE (factor < 1.0 — tooth will fail)';
311
+ const verdictClass = SF_min >= 1.5 ? 'pass' : SF_min >= 1.0 ? 'warn' : 'fail';
312
+ const notes = [];
313
+ if (SF_bending_P < SF_bending_G) notes.push('Pinion is the weaker gear in bending (lower J factor) — as expected.');
314
+ if (SF_contact_P < SF_bending_P) notes.push('Contact stress governs over bending — consider surface hardening (carburizing/induction) for higher S_c.');
315
+ if (SF_min < 1.5) notes.push('Margin below industry-typical 1.5 — increase module, face width, or hardness.');
316
+
317
+ return {
318
+ inputs: { z_P, z_G, m, F, T_P, HB_P, HB_G, K_o, K_v, K_m, K_R, phi, Z_E,
319
+ d_P, d_G, mG, W_t, J_P, J_G, I, St_P, St_G, Sc_P, Sc_G },
320
+ pinion: { SF_bending: SF_bending_P, SF_contact: SF_contact_P,
321
+ sigma_b: sigma_b_P, sigma_c, S_t: St_P, S_c: Sc_P, J: J_P },
322
+ gear: { SF_bending: SF_bending_G, SF_contact: SF_contact_G,
323
+ sigma_b: sigma_b_G, sigma_c, S_t: St_G, S_c: Sc_G, J: J_G },
324
+ SF_min, safe, verdict, verdictClass, notes
325
+ };
326
+ }
327
+
328
+ // ===================================================================
329
+ // SHAFT FATIGUE — Goodman / Soderberg (Shigley Ch. 7)
330
+ // ===================================================================
331
+ // Shaft material data (wrought carbon steel from Shigley Table A-20).
332
+ const SHAFT_MATERIALS = Object.freeze({
333
+ '1020_hr': { label: 'AISI 1020 hot-rolled', S_ut: 380, S_y: 210 },
334
+ '1020_cd': { label: 'AISI 1020 cold-drawn', S_ut: 470, S_y: 390 },
335
+ '1040_hr': { label: 'AISI 1040 hot-rolled', S_ut: 520, S_y: 290 },
336
+ '1040_cd': { label: 'AISI 1040 cold-drawn', S_ut: 590, S_y: 490 },
337
+ '1050_hr': { label: 'AISI 1050 hot-rolled', S_ut: 620, S_y: 340 },
338
+ '1050_cd': { label: 'AISI 1050 cold-drawn', S_ut: 690, S_y: 580 },
339
+ '4140_Q&T': { label: 'AISI 4140 Q&T 425°C', S_ut: 1020, S_y: 900 },
340
+ '4340_Q&T': { label: 'AISI 4340 Q&T 425°C', S_ut: 1280, S_y: 1140 }
341
+ });
342
+
343
+ // Marin surface factor — Shigley Eq. 6-19, a*S_ut^b.
344
+ const SURFACE_FACTORS = {
345
+ 'ground': { a: 1.58, b: -0.085 },
346
+ 'machined': { a: 4.51, b: -0.265 },
347
+ 'cold-drawn': { a: 4.51, b: -0.265 },
348
+ 'hot-rolled': { a: 57.7, b: -0.718 },
349
+ 'as-forged': { a: 272, b: -0.995 }
350
+ };
351
+
352
+ /**
353
+ * Shaft fatigue analysis using Goodman / Soderberg criteria.
354
+ *
355
+ * Given mean and alternating stresses (computed from bending moment + torque amplitudes),
356
+ * applies Marin modifying factors to estimate the endurance limit and returns factors of
357
+ * safety per Goodman (common) and Soderberg (conservative).
358
+ *
359
+ * @param {object} p
360
+ * @param {string} p.material Key into SHAFT_MATERIALS.
361
+ * @param {number} p.diameter Shaft diameter in mm.
362
+ * @param {number} p.M_a Alternating bending moment amplitude in N·m.
363
+ * @param {number} p.M_m Mean bending moment in N·m (often 0 for rotating-bending).
364
+ * @param {number} p.T_a Alternating torque amplitude in N·m.
365
+ * @param {number} p.T_m Mean torque in N·m.
366
+ * @param {number} [p.Kf=2.0] Fatigue stress concentration for bending (≥1).
367
+ * @param {number} [p.Kfs=1.5] Fatigue stress concentration for torsion.
368
+ * @param {string} [p.surface='machined'] Surface finish preset.
369
+ * @param {number} [p.reliability=0.99] Reliability (0.5–0.999999).
370
+ * @param {number} [p.temperatureC=25] Operating temperature in °C.
371
+ * @returns {object}
372
+ */
373
+ function shaftFatigueAnalysis(p) {
374
+ const mat = SHAFT_MATERIALS[p.material] || SHAFT_MATERIALS['1050_cd'];
375
+ const d = Math.max(5, Number(p.diameter) || 25); // mm
376
+ const M_a = Math.max(0, Number(p.M_a) || 0); // N·m
377
+ const M_m = Math.max(0, Number(p.M_m) || 0);
378
+ const T_a = Math.max(0, Number(p.T_a) || 0);
379
+ const T_m = Math.max(0, Number(p.T_m) || 0);
380
+ const Kf = Math.max(1, Number(p.Kf) || 2.0);
381
+ const Kfs = Math.max(1, Number(p.Kfs) || 1.5);
382
+ const surfaceKey = p.surface || 'machined';
383
+ const surf = SURFACE_FACTORS[surfaceKey] || SURFACE_FACTORS.machined;
384
+ const R = Math.max(0.5, Math.min(0.999999, Number(p.reliability) || 0.99));
385
+ const T_C = Number(p.temperatureC) || 25;
386
+
387
+ // Marin surface factor k_a = a * (S_ut[MPa])^b
388
+ const k_a = surf.a * Math.pow(mat.S_ut, surf.b);
389
+ // Size factor k_b — Shigley Eq. 6-20 (rotating bending, 2.79 ≤ d ≤ 51 mm)
390
+ let k_b;
391
+ if (d <= 51) k_b = 1.24 * Math.pow(d, -0.107);
392
+ else if (d <= 254) k_b = 1.51 * Math.pow(d, -0.157);
393
+ else k_b = 0.60;
394
+ // Loading factor k_c = 1 for combined bending + torsion (we handle each via Kf/Kfs)
395
+ const k_c = 1.0;
396
+ // Temperature factor k_d — Eq. 6-27
397
+ const k_d = (T_C <= 70) ? 1.0 : (0.975 + 0.432e-3*T_C - 0.115e-5*T_C*T_C + 0.104e-8*T_C*T_C*T_C);
398
+ // Reliability factor k_e — Shigley Table 6-5 (z_a from normal distribution)
399
+ const z_a = (R === 0.99 ? 2.326 : R === 0.999 ? 3.091 : R === 0.95 ? 1.645 : R === 0.90 ? 1.288 : 2.326);
400
+ const k_e = 1 - 0.08 * z_a;
401
+ // Miscellaneous k_f = 1 for this scope
402
+ const k_f = 1.0;
403
+ // Uncorrected endurance limit S_e' — Shigley Eq. 6-10
404
+ const S_e_prime = (mat.S_ut <= 1400) ? 0.5 * mat.S_ut : 700;
405
+ // Corrected endurance limit
406
+ const S_e = k_a * k_b * k_c * k_d * k_e * k_f * S_e_prime;
407
+
408
+ // Stresses — bending σ_a/σ_m (Kf applied), torsion τ_a/τ_m (Kfs applied)
409
+ // σ = 32·M / (π·d^3) [M in N·mm, d in mm → MPa]
410
+ // τ = 16·T / (π·d^3)
411
+ const d_m = d; // mm
412
+ const sigma_a = (32 * M_a * 1000) / (Math.PI * Math.pow(d_m, 3));
413
+ const sigma_m = (32 * M_m * 1000) / (Math.PI * Math.pow(d_m, 3));
414
+ const tau_a = (16 * T_a * 1000) / (Math.PI * Math.pow(d_m, 3));
415
+ const tau_m = (16 * T_m * 1000) / (Math.PI * Math.pow(d_m, 3));
416
+
417
+ // Von Mises effective stresses (amplitude + mean) with fatigue concentrations
418
+ const sigma_prime_a = Math.sqrt(Math.pow(Kf * sigma_a, 2) + 3 * Math.pow(Kfs * tau_a, 2));
419
+ const sigma_prime_m = Math.sqrt(Math.pow(Kf * sigma_m, 2) + 3 * Math.pow(Kfs * tau_m, 2));
420
+
421
+ // Goodman: 1/n = σ_a'/S_e + σ_m'/S_ut
422
+ const goodmanInv = (sigma_prime_a / S_e) + (sigma_prime_m / mat.S_ut);
423
+ const n_Goodman = goodmanInv > 0 ? 1 / goodmanInv : Infinity;
424
+
425
+ // Soderberg: 1/n = σ_a'/S_e + σ_m'/S_y (conservative — yields instead of UTS)
426
+ const soderbergInv = (sigma_prime_a / S_e) + (sigma_prime_m / mat.S_y);
427
+ const n_Soderberg = soderbergInv > 0 ? 1 / soderbergInv : Infinity;
428
+
429
+ // First-cycle yield check — static
430
+ const sigma_max = sigma_prime_a + sigma_prime_m;
431
+ const n_yield = mat.S_y / sigma_max;
432
+
433
+ // Verdict
434
+ const n_fatigue = Math.min(n_Goodman, n_Soderberg);
435
+ let verdict, verdictClass;
436
+ if (n_fatigue >= 2.0 && n_yield >= 2.0) { verdict = 'SAFE (margin ≥ 2)'; verdictClass = 'pass'; }
437
+ else if (n_fatigue >= 1.5 && n_yield >= 1.5) { verdict = 'SAFE (industry typical)'; verdictClass = 'pass'; }
438
+ else if (n_fatigue >= 1.0 && n_yield >= 1.0) { verdict = 'MARGINAL — factor below 1.5'; verdictClass = 'warn'; }
439
+ else { verdict = 'UNSAFE — factor < 1.0'; verdictClass = 'fail'; }
440
+ const notes = [];
441
+ if (n_yield < n_fatigue) notes.push('First-cycle yield governs — increase diameter or use higher-strength material.');
442
+ if (sigma_prime_m > sigma_prime_a) notes.push('Mean stress dominant — consider rotating-bending to convert static to fully-reversed.');
443
+ if (n_Goodman > n_Soderberg + 0.3) notes.push('Soderberg (conservative) significantly lower — review if proof strength, not UTS, is the correct allowable.');
444
+
445
+ return {
446
+ inputs: { material: p.material || '1050_cd', material_label: mat.label, d, S_ut: mat.S_ut, S_y: mat.S_y,
447
+ M_a, M_m, T_a, T_m, Kf, Kfs, surfaceKey, reliability: R, temperatureC: T_C },
448
+ marin: { k_a, k_b, k_c, k_d, k_e, k_f, S_e_prime, S_e },
449
+ stresses: { sigma_a, sigma_m, tau_a, tau_m, sigma_prime_a, sigma_prime_m, sigma_max },
450
+ n_Goodman, n_Soderberg, n_yield,
451
+ verdict, verdictClass, notes
452
+ };
453
+ }
454
+
455
+ // ===================================================================
456
+ // UNIT TESTS — verify core against MecAgent screenshot values
457
+ // ===================================================================
458
+ function runSelfTests() {
459
+ const results = [];
460
+ function test(name, actual, expected, tol) {
461
+ const pass = Math.abs(actual - expected) <= tol;
462
+ results.push({ name, actual, expected, pass });
463
+ return pass;
464
+ }
465
+
466
+ // MecAgent problem: 4 × M12 (10.9), F_Q=18kN, F_A=18kN, M=420Nm, μ=0.16, F_V=39kN/bolt, BCD=96mm, K_s=1.5
467
+ const r = boltedJointAnalysis({
468
+ boltCount: 4, grade: '10.9', thread: 'M12',
469
+ preload: 39000, shearForce: 18000, axialForce: 18000, moment: 420000,
470
+ bcd: 96, friction: 0.16, safetyFactor: 1.5
471
+ });
472
+
473
+ // Expected values from MecAgent screenshots:
474
+ test('F_friction', r.slipResistance.F_friction, 24960, 1); // 4·39000·0.16
475
+ test('F_moment/bolt', r.slipResistance.F_moment_tangential_per_bolt, 2187.5, 0.5); // 420000/(4·48)
476
+ test('F_bolt_max_tang', r.slipResistance.F_bolt_tangential_max, 6687.5, 0.5); // 18000/4 + 2187.5
477
+ test('F_bolt_total', r.tensionCheck.F_bolt_total, 45687.5, 1); // 39000 + 4500 + 2187.5
478
+ test('σ (tensile)', r.combinedStress.sigma, 542, 1); // 45687.5/84.3
479
+ test('τ (shear)', r.combinedStress.tau, 79, 1.5); // 6687.5/84.3 ≈ 79.3
480
+ test('σ_vm', r.combinedStress.sigma_vm, 558, 2); // √(542² + 3·79²)
481
+
482
+ // ------- GEAR TEST — Shigley Example 14-5 adapted -------
483
+ // 17T pinion / 52T gear, m=2mm, F=30mm, 2.5kW @ 1800rpm → T_P ≈ 13.26 N·m
484
+ // Through-hardened 240 HB both gears, K_o=1, K_v=1, K_m=1.3
485
+ const g = spurGearAnalysis({
486
+ pinionTeeth: 17, gearTeeth: 52, module: 2, faceWidth: 30,
487
+ torque: 13.26, pinionHB: 240, gearHB: 240,
488
+ overload: 1, dynamic: 1, loadDist: 1.3
489
+ });
490
+ // Tangential load W_t = 2·T / d_P = 2·13260 / 34 ≈ 780 N
491
+ test('gear W_t', g.inputs.W_t, 780, 3);
492
+ // J for 17 teeth ≈ 0.295 per interpolation table
493
+ test('gear J (pinion)', g.pinion.J, 0.295, 0.01);
494
+ // σ_b (pinion) = 780·1·1·1.3 / (30·2·0.295) = 1014/17.7 ≈ 57.3 MPa
495
+ test('gear σ_b pinion', g.pinion.sigma_b, 57.3, 1.5);
496
+ // S_t @240HB = (77·240 + 12800)·psi→MPa = 31280·0.00689 ≈ 215.7 MPa
497
+ test('gear S_t @240HB', g.pinion.S_t, 215.7, 1.5);
498
+ // SF_bending_P ≈ 215.7 / 57.3 ≈ 3.77
499
+ test('gear SF_bending pinion', g.pinion.SF_bending, 3.77, 0.2);
500
+
501
+ // ------- SHAFT TEST — clean case with rotating-bending + constant torque -------
502
+ // AISI 1050 CD, d=25mm, M_a=100 N·m (rotating bending so M_m=0), T_m=50 N·m, T_a=0
503
+ // Kf=2.0 (fillet), Kfs=1.5, machined surface, R=0.99, T=25°C
504
+ const sh = shaftFatigueAnalysis({
505
+ material: '1050_cd', diameter: 25,
506
+ M_a: 100, M_m: 0, T_a: 0, T_m: 50,
507
+ Kf: 2.0, Kfs: 1.5, surface: 'machined', reliability: 0.99, temperatureC: 25
508
+ });
509
+ // σ_a = 32·100000 / (π·25³) = 32e5 / 49087 ≈ 65.2 MPa
510
+ test('shaft σ_a', sh.stresses.sigma_a, 65.2, 0.5);
511
+ // τ_m = 16·50000 / (π·25³) ≈ 16.3 MPa
512
+ test('shaft τ_m', sh.stresses.tau_m, 16.3, 0.3);
513
+ // σ_prime_a with Kf·σ_a only (no mean bending, no alternating torque) ≈ Kf·σ_a = 130.4
514
+ test('shaft σ′_a', sh.stresses.sigma_prime_a, 130.4, 1.0);
515
+ // σ_prime_m = √3 · Kfs · τ_m ≈ √3 · 1.5 · 16.3 ≈ 42.4
516
+ test('shaft σ′_m', sh.stresses.sigma_prime_m, 42.4, 1.0);
517
+ // Must be finite + positive
518
+ test('shaft n_Goodman finite', Number.isFinite(sh.n_Goodman) && sh.n_Goodman > 0 ? 1 : 0, 1, 0);
519
+ test('shaft n_Soderberg ≤ Goodman', (sh.n_Soderberg <= sh.n_Goodman + 1e-6) ? 1 : 0, 1, 0);
520
+
521
+ return { results, allPass: results.every(r => r.pass) };
522
+ }
523
+
524
+ // ===================================================================
525
+ // NATURAL-LANGUAGE PARSER — extract structured params from a free-text prompt
526
+ // ===================================================================
527
+ function parseBoltedJointPrompt(prompt) {
528
+ const p = (prompt || '').toLowerCase();
529
+ const res = {};
530
+
531
+ // Bolt count: "4 x m12 bolts", "four bolts", "z=4"
532
+ const wordNums = { one:1, two:2, three:3, four:4, five:5, six:6, seven:7, eight:8, ten:10, twelve:12 };
533
+ const mCount = p.match(/\b(\d+)\s*[x×]?\s*(?:m\d+\s+)?bolts?\b/) ||
534
+ p.match(/\bz\s*=\s*(\d+)\b/) ||
535
+ p.match(/\b(one|two|three|four|five|six|seven|eight|ten|twelve)\s*bolts?\b/);
536
+ if (mCount) res.boltCount = wordNums[mCount[1]] || parseInt(mCount[1]);
537
+
538
+ // Thread: "M12", "m8"
539
+ const mThread = p.match(/\bm\s*(\d+(?:\.\d+)?)\b/i);
540
+ if (mThread) {
541
+ const sz = parseFloat(mThread[1]);
542
+ const key = 'M' + (Number.isInteger(sz) ? sz : sz.toString().replace('.', '_'));
543
+ if (BOLT_STRESS_AREA[key]) res.thread = key;
544
+ }
545
+
546
+ // Grade: "10.9", "class 8.8", "property class 10.9"
547
+ const mGrade = p.match(/\b(?:class|grade|property\s*class)\s*(\d+\.\d+)\b/i) ||
548
+ p.match(/\b(4\.6|4\.8|5\.6|5\.8|6\.8|8\.8|10\.9|12\.9)\b/);
549
+ if (mGrade) res.grade = mGrade[1];
550
+
551
+ // Forces — support kN and N units
552
+ const numUnit = (m) => {
553
+ if (!m) return null;
554
+ const val = parseFloat(m[1]);
555
+ const unit = (m[2] || '').toLowerCase();
556
+ if (unit.startsWith('kn')) return val * 1000;
557
+ return val;
558
+ };
559
+ const mShear = p.match(/(?:shear|transverse|shearing)\s*(?:force|load)?[^\d-]{0,12}(\d+(?:\.\d+)?)\s*(kn|n)\b/i);
560
+ if (mShear) res.shearForce = numUnit(mShear);
561
+ const mAxial = p.match(/(?:axial|separating|tensile)\s*(?:force|load)?[^\d-]{0,12}(\d+(?:\.\d+)?)\s*(kn|n)\b/i);
562
+ if (mAxial) res.axialForce = numUnit(mAxial);
563
+
564
+ // Moment (N·m or kN·m → convert to N·mm)
565
+ const mMoment = p.match(/(?:in.plane\s*)?moment[^\d-]{0,12}(\d+(?:\.\d+)?)\s*(nm|knm|kn\s*m|n\s*m)\b/i);
566
+ if (mMoment) {
567
+ const val = parseFloat(mMoment[1]);
568
+ const unit = mMoment[2].toLowerCase().replace(/\s/g, '');
569
+ res.moment = unit.startsWith('kn') ? val * 1e6 : val * 1000;
570
+ }
571
+
572
+ // Preload
573
+ const mPre = p.match(/pre[-\s]?load(?:\s*per\s*bolt)?[^\d-]{0,12}(\d+(?:\.\d+)?)\s*(kn|n)\b/i);
574
+ if (mPre) res.preload = numUnit(mPre);
575
+
576
+ // BCD
577
+ const mBcd = p.match(/(?:bcd|bolt\s*circle[^\d]*?diameter|bolt\s*circle)[^\d-]{0,12}(\d+(?:\.\d+)?)\s*mm/i);
578
+ if (mBcd) res.bcd = parseFloat(mBcd[1]);
579
+
580
+ // Friction
581
+ const mFric = p.match(/(?:friction\s*(?:coefficient)?|[μu])[^\d-]{0,8}(\d*\.\d+)/i);
582
+ if (mFric) res.friction = parseFloat(mFric[1]);
583
+
584
+ // Safety factor — allow common phrasings like "safety factor against slipping: 1.5"
585
+ const mSf = p.match(/(?:safety\s*factor|k\s*_?\s*s|\bks\b)\s*(?:against\s+\w+)?\s*[:=]?\s*(\d+(?:\.\d+)?)/i);
586
+ if (mSf) res.safetyFactor = parseFloat(mSf[1]);
587
+
588
+ return res;
589
+ }
590
+
591
+ // ===================================================================
592
+ // LaTeX / KaTeX integration — load on demand so it doesn't bloat initial page
593
+ // ===================================================================
594
+ let _katexLoading = null;
595
+ async function loadKaTeX() {
596
+ if (typeof window.katex !== 'undefined') return window.katex;
597
+ if (_katexLoading) return _katexLoading;
598
+ _katexLoading = new Promise((resolve, reject) => {
599
+ const css = document.createElement('link');
600
+ css.rel = 'stylesheet';
601
+ css.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css';
602
+ document.head.appendChild(css);
603
+ const s = document.createElement('script');
604
+ s.src = 'https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js';
605
+ s.onload = () => resolve(window.katex);
606
+ s.onerror = () => reject(new Error('KaTeX failed to load from CDN'));
607
+ document.head.appendChild(s);
608
+ });
609
+ return _katexLoading;
610
+ }
611
+
612
+ function renderMath(el, latex, displayMode) {
613
+ el.textContent = latex; // fallback while KaTeX loads / if it fails
614
+ loadKaTeX().then(k => {
615
+ try { k.render(latex, el, { displayMode: !!displayMode, throwOnError: false, strict: 'ignore' }); }
616
+ catch(e) { el.textContent = latex; }
617
+ }).catch(() => {});
618
+ }
619
+
620
+ // ===================================================================
621
+ // UI — form-based input with live recompute + formatted report
622
+ // ===================================================================
623
+ const S = { lastResult: null, els: {} };
624
+
625
+ const INPUT_FIELDS = [
626
+ // [key, label, unit, default, min]
627
+ ['boltCount', 'Bolt count (z)', '', 4, 1],
628
+ ['thread', 'Thread', '', 'M12', null, 'select', Object.keys(BOLT_STRESS_AREA)],
629
+ ['grade', 'Grade (ISO 898-1)', '', '8.8', null, 'select', Object.keys(STEEL_GRADES)],
630
+ ['preload', 'Preload per bolt (F_V)', 'N', 25000, 0],
631
+ ['shearForce', 'Shear force (F_Q)', 'N', 10000, 0],
632
+ ['axialForce', 'Axial/separating force (F_A)', 'N', 5000, 0],
633
+ ['moment', 'In-plane moment (M)', 'N·mm', 100000,0],
634
+ ['bcd', 'Bolt-circle diameter (BCD)', 'mm', 80, 0],
635
+ ['friction', 'Friction coefficient (μ)', '', 0.15, 0.01],
636
+ ['safetyFactor', 'Slip safety factor (K_s)', '', 1.5, 1],
637
+ ['frictionInterfaces','Friction interfaces (n)', '', 1, 1]
638
+ ];
639
+
640
+ function fmt(n, unit, digits) {
641
+ if (!isFinite(n)) return '—';
642
+ digits = digits ?? (Math.abs(n) >= 1000 ? 0 : Math.abs(n) >= 10 ? 1 : 2);
643
+ const s = Number(n).toFixed(digits);
644
+ return unit ? s + ' ' + unit : s;
645
+ }
646
+
647
+ function collectInputs() {
648
+ const out = {};
649
+ INPUT_FIELDS.forEach(([key,,,,, type]) => {
650
+ const el = S.els['in_' + key];
651
+ if (!el) return;
652
+ out[key] = (type === 'select') ? el.value : parseFloat(el.value);
653
+ });
654
+ return out;
655
+ }
656
+
657
+ function compute() {
658
+ try {
659
+ const params = collectInputs();
660
+ const r = boltedJointAnalysis(params);
661
+ S.lastResult = r;
662
+ renderReport(r);
663
+ } catch(e) {
664
+ if (S.els.report) S.els.report.innerHTML = '<div class="aie-err">Error: ' + e.message + '</div>';
665
+ }
666
+ }
667
+
668
+ function renderReport(r) {
669
+ const root = S.els.report;
670
+ if (!root) return;
671
+ root.innerHTML = '';
672
+
673
+ // Verdict banner
674
+ const verdictBg = r.verdictClass === 'pass' ? '#065f46' : r.verdictClass === 'warn' ? '#78350f' : '#7f1d1d';
675
+ const verdictFg = r.verdictClass === 'pass' ? '#d1fae5' : r.verdictClass === 'warn' ? '#fed7aa' : '#fecaca';
676
+ const banner = document.createElement('div');
677
+ banner.className = 'aie-verdict';
678
+ banner.style.cssText = 'padding:10px 14px;border-radius:6px;background:'+verdictBg+';color:'+verdictFg+';font-weight:600;font-size:14px;margin-bottom:12px;border:1px solid rgba(255,255,255,0.08)';
679
+ banner.textContent = 'Verdict: ' + r.verdict;
680
+ root.appendChild(banner);
681
+ if (r.notes?.length) {
682
+ const notes = document.createElement('ul');
683
+ notes.style.cssText = 'margin:4px 0 14px 18px;font-size:12px;color:#94a3b8';
684
+ r.notes.forEach(n => { const li = document.createElement('li'); li.textContent = n; notes.appendChild(li); });
685
+ root.appendChild(notes);
686
+ }
687
+
688
+ // CHECK 1: Slip resistance
689
+ const s = r.slipResistance, i = r.inputs;
690
+ const sec1 = section('1. Slip resistance', s.safe);
691
+ sec1.appendChild(math(
692
+ 'F_{friction} = \\mu \\cdot n \\cdot z \\cdot F_V = '
693
+ + i.mu + ' \\cdot ' + i.n_interfaces + ' \\cdot ' + i.z + ' \\cdot ' + i.F_preload
694
+ + ' = \\mathbf{' + s.F_friction.toFixed(0) + '}\\;\\text{N}', true));
695
+ sec1.appendChild(math(
696
+ 'F_{M,tangential/bolt} = \\frac{M}{z \\cdot r} = \\frac{' + i.M + '}{' + i.z + ' \\cdot ' + i.r.toFixed(1) + '} = '
697
+ + s.F_moment_tangential_per_bolt.toFixed(1) + '\\;\\text{N}', true));
698
+ sec1.appendChild(math(
699
+ 'F_{bolt,tangential,max} = \\frac{F_Q}{z} + F_{M,tangential/bolt} = '
700
+ + (i.F_shear/i.z).toFixed(1) + ' + ' + s.F_moment_tangential_per_bolt.toFixed(1)
701
+ + ' = \\mathbf{' + s.F_bolt_tangential_max.toFixed(1) + '}\\;\\text{N}', true));
702
+ sec1.appendChild(math(
703
+ 'F_{required} = K_s \\cdot z \\cdot F_{bolt,tangential,max} = '
704
+ + i.K_s + ' \\cdot ' + i.z + ' \\cdot ' + s.F_bolt_tangential_max.toFixed(1)
705
+ + ' = \\mathbf{' + s.F_required.toFixed(0) + '}\\;\\text{N}', true));
706
+ const slipResult = document.createElement('div');
707
+ slipResult.style.cssText = 'margin-top:6px;font-size:12px;color:' + (s.safe ? '#a7f3d0' : '#fca5a5');
708
+ slipResult.innerHTML = s.safe
709
+ ? '✓ ' + s.F_friction.toFixed(0) + ' N ≥ ' + s.F_required.toFixed(0) + ' N&nbsp;&nbsp;(margin ×' + s.margin.toFixed(2) + ')'
710
+ : '✗ ' + s.F_friction.toFixed(0) + ' N &lt; ' + s.F_required.toFixed(0) + ' N&nbsp;&nbsp;(deficit: ' + (s.F_required - s.F_friction).toFixed(0) + ' N)';
711
+ sec1.appendChild(slipResult);
712
+ root.appendChild(sec1);
713
+
714
+ // CHECK 2: Bolt tension
715
+ const t = r.tensionCheck;
716
+ const sec2 = section('2. Maximum bolt tension', true /* always informational */);
717
+ sec2.appendChild(math(
718
+ 'F_{A,bolt} = \\frac{F_A}{z} = \\frac{' + i.F_axial + '}{' + i.z + '} = ' + t.F_axial_per_bolt.toFixed(1) + '\\;\\text{N}', true));
719
+ sec2.appendChild(math(
720
+ 'F_{M,axial/bolt} = \\frac{M}{z \\cdot r} = ' + t.F_moment_axial_per_bolt.toFixed(1) + '\\;\\text{N}', true));
721
+ sec2.appendChild(math(
722
+ 'F_{max,external} = F_{A,bolt} + F_{M,axial/bolt} = \\mathbf{' + t.F_max_external.toFixed(1) + '}\\;\\text{N}', true));
723
+ sec2.appendChild(math(
724
+ 'F_{bolt,total} = F_V + F_{max,external} = ' + i.F_preload + ' + ' + t.F_max_external.toFixed(1)
725
+ + ' = \\mathbf{' + t.F_bolt_total.toFixed(1) + '}\\;\\text{N}', true));
726
+ root.appendChild(sec2);
727
+
728
+ // CHECK 3: Combined stress
729
+ const c = r.combinedStress;
730
+ const sec3 = section('3. Combined stress (von Mises)', c.safe);
731
+ sec3.appendChild(math(
732
+ '\\sigma = \\frac{F_{bolt,total}}{A_s} = \\frac{' + t.F_bolt_total.toFixed(1) + '}{' + i.A_s + '} = \\mathbf{'
733
+ + c.sigma.toFixed(1) + '}\\;\\text{MPa}', true));
734
+ sec3.appendChild(math(
735
+ '\\tau = \\frac{F_{bolt,tangential,max}}{A_s} = \\frac{' + s.F_bolt_tangential_max.toFixed(1) + '}{' + i.A_s + '} = \\mathbf{'
736
+ + c.tau.toFixed(1) + '}\\;\\text{MPa}', true));
737
+ sec3.appendChild(math(
738
+ '\\sigma_{vm} = \\sqrt{\\sigma^2 + 3\\tau^2} = \\sqrt{' + c.sigma.toFixed(1) + '^2 + 3 \\cdot ' + c.tau.toFixed(1)
739
+ + '^2} = \\mathbf{' + c.sigma_vm.toFixed(1) + '}\\;\\text{MPa}', true));
740
+ const stressResult = document.createElement('div');
741
+ stressResult.style.cssText = 'margin-top:6px;font-size:12px;color:' + (c.safe ? '#a7f3d0' : '#fca5a5');
742
+ stressResult.innerHTML = c.safe
743
+ ? '✓ σ_vm = ' + c.sigma_vm.toFixed(1) + ' MPa &lt; proof strength R_p0.2 = ' + c.R_p02 + ' MPa (class '+i.grade+')&nbsp;&nbsp;(utilisation ' + (c.utilization*100).toFixed(1) + '%)'
744
+ : '✗ σ_vm = ' + c.sigma_vm.toFixed(1) + ' MPa ≥ R_p0.2 = ' + c.R_p02 + ' MPa (class '+i.grade+')&nbsp;&nbsp;— BOLT WILL YIELD';
745
+ sec3.appendChild(stressResult);
746
+ root.appendChild(sec3);
747
+
748
+ // Metadata footer
749
+ const meta = document.createElement('div');
750
+ meta.style.cssText = 'margin-top:14px;padding-top:10px;border-top:1px solid #334155;font-size:11px;color:#64748b';
751
+ meta.innerHTML = 'Method: VDI 2230 / Shigley simplified (no load-factor Φ — conservative Φ=1). ' +
752
+ 'Thread ' + i.thread + ' A_s = ' + i.A_s + ' mm² (DIN 13). Grade ' + i.grade + ' R_p0.2 = ' + i.R_p02 + ' MPa, R_m = ' + i.R_m + ' MPa.<br>' +
753
+ 'This is a first-pass analysis — always verify with detailed VDI 2230 if safety-critical.';
754
+ root.appendChild(meta);
755
+ }
756
+
757
+ function section(title, safe) {
758
+ const wrap = document.createElement('div');
759
+ wrap.style.cssText = 'margin-bottom:14px;padding:10px 12px;background:#0f172a;border:1px solid #334155;border-left:3px solid '+(safe?'#10b981':'#ef4444')+';border-radius:4px';
760
+ const h = document.createElement('div');
761
+ h.style.cssText = 'font-size:13px;font-weight:600;color:#e5e7eb;margin-bottom:8px';
762
+ h.textContent = title;
763
+ wrap.appendChild(h);
764
+ return wrap;
765
+ }
766
+
767
+ function math(latex, block) {
768
+ const el = document.createElement('div');
769
+ el.style.cssText = 'margin:4px 0;font-size:13px;color:#cbd5e1;overflow-x:auto';
770
+ renderMath(el, latex, block);
771
+ return el;
772
+ }
773
+
774
+ function applyFromPrompt() {
775
+ const prompt = S.els.prompt.value;
776
+ if (!prompt.trim()) return;
777
+ const parsed = parseBoltedJointPrompt(prompt);
778
+ Object.entries(parsed).forEach(([k, v]) => {
779
+ const el = S.els['in_' + k];
780
+ if (el) el.value = String(v);
781
+ });
782
+ compute();
783
+ }
784
+
785
+ function buildUI() {
786
+ const wrap = document.createElement('div');
787
+ wrap.className = 'aie-panel';
788
+ wrap.style.cssText = 'display:flex;flex-direction:column;gap:10px;padding:12px;min-width:460px;max-width:720px;font-family:-apple-system,sans-serif;color:#e5e7eb';
789
+
790
+ // Header
791
+ const header = document.createElement('div');
792
+ header.innerHTML = '<div style="font-size:15px;font-weight:700;color:#f1f5f9">AI Engineering Analyst</div>'
793
+ + '<div style="font-size:11px;color:#94a3b8;margin-top:2px">Bolted-joint analysis (VDI 2230 / Shigley) — v1</div>';
794
+ wrap.appendChild(header);
795
+
796
+ // Prompt box for natural-language entry
797
+ const promptWrap = document.createElement('div');
798
+ promptWrap.style.cssText = 'display:flex;gap:6px';
799
+ const prompt = document.createElement('input');
800
+ prompt.type = 'text';
801
+ prompt.placeholder = 'Describe the joint (e.g. "4 × M12 bolts class 10.9, shear 18kN, axial 18kN, moment 420Nm, preload 39kN, BCD 96mm, μ=0.16, K_s=1.5")';
802
+ prompt.style.cssText = 'flex:1;padding:8px;background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:4px;font:12px inherit';
803
+ const applyBtn = document.createElement('button');
804
+ applyBtn.textContent = 'Parse';
805
+ applyBtn.style.cssText = 'padding:8px 14px;background:#38bdf8;color:#0f172a;border:0;border-radius:4px;font-weight:600;cursor:pointer';
806
+ applyBtn.onclick = applyFromPrompt;
807
+ prompt.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); applyFromPrompt(); } });
808
+ S.els.prompt = prompt;
809
+ promptWrap.appendChild(prompt);
810
+ promptWrap.appendChild(applyBtn);
811
+ wrap.appendChild(promptWrap);
812
+
813
+ // Input grid
814
+ const grid = document.createElement('div');
815
+ grid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px 12px;padding:10px;background:#1e293b;border-radius:6px';
816
+ INPUT_FIELDS.forEach(field => {
817
+ const [key, label, unit, def, min, type, opts] = field;
818
+ const cell = document.createElement('label');
819
+ cell.style.cssText = 'display:flex;flex-direction:column;gap:2px;font-size:11px;color:#94a3b8';
820
+ const lbl = document.createElement('span');
821
+ lbl.innerHTML = label + (unit ? ' <span style="color:#64748b">['+unit+']</span>' : '');
822
+ let input;
823
+ if (type === 'select') {
824
+ input = document.createElement('select');
825
+ input.style.cssText = 'padding:5px;background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:3px;font-size:12px';
826
+ opts.forEach(v => { const o = document.createElement('option'); o.value = v; o.textContent = v; input.appendChild(o); });
827
+ input.value = def;
828
+ } else {
829
+ input = document.createElement('input');
830
+ input.type = 'number';
831
+ input.step = 'any';
832
+ if (min !== null) input.min = String(min);
833
+ input.value = String(def);
834
+ input.style.cssText = 'padding:5px;background:#0f172a;color:#e2e8f0;border:1px solid #334155;border-radius:3px;font-size:12px';
835
+ }
836
+ input.addEventListener('input', compute);
837
+ input.addEventListener('change', compute);
838
+ S.els['in_' + key] = input;
839
+ cell.appendChild(lbl);
840
+ cell.appendChild(input);
841
+ grid.appendChild(cell);
842
+ });
843
+ wrap.appendChild(grid);
844
+
845
+ // Preset examples
846
+ const presets = document.createElement('div');
847
+ presets.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px';
848
+ [
849
+ { label: 'MecAgent demo', values: { boltCount:4, thread:'M12', grade:'10.9', preload:39000, shearForce:18000, axialForce:18000, moment:420000, bcd:96, friction:0.16, safetyFactor:1.5 } },
850
+ { label: 'Flange M8 light', values: { boltCount:8, thread:'M8', grade:'8.8', preload:15000, shearForce:5000, axialForce:2000, moment:50000, bcd:60, friction:0.15, safetyFactor:1.25 } },
851
+ { label: 'Heavy M20', values: { boltCount:6, thread:'M20', grade:'8.8', preload:120000, shearForce:30000, axialForce:25000, moment:800000, bcd:200, friction:0.14, safetyFactor:1.5 } }
852
+ ].forEach(preset => {
853
+ const b = document.createElement('button');
854
+ b.textContent = preset.label;
855
+ b.style.cssText = 'padding:4px 8px;background:#334155;color:#cbd5e1;border:0;border-radius:3px;cursor:pointer;font-size:11px';
856
+ b.onclick = () => {
857
+ Object.entries(preset.values).forEach(([k, v]) => { const el = S.els['in_' + k]; if (el) el.value = String(v); });
858
+ compute();
859
+ };
860
+ presets.appendChild(b);
861
+ });
862
+ wrap.appendChild(presets);
863
+
864
+ // Report area
865
+ const report = document.createElement('div');
866
+ report.className = 'aie-report';
867
+ report.style.cssText = 'padding:4px 0';
868
+ S.els.report = report;
869
+ wrap.appendChild(report);
870
+
871
+ // Self-test panel (collapsed by default)
872
+ const testDetails = document.createElement('details');
873
+ testDetails.style.cssText = 'font-size:11px;color:#64748b;margin-top:6px';
874
+ const summary = document.createElement('summary');
875
+ summary.textContent = 'Verification: MecAgent reference values';
876
+ summary.style.cssText = 'cursor:pointer;user-select:none';
877
+ testDetails.appendChild(summary);
878
+ const tests = runSelfTests();
879
+ const testList = document.createElement('ul');
880
+ testList.style.cssText = 'margin:6px 0 0 20px;font:11px/1.5 SF Mono,monospace';
881
+ tests.results.forEach(t => {
882
+ const li = document.createElement('li');
883
+ li.style.color = t.pass ? '#86efac' : '#fca5a5';
884
+ li.textContent = (t.pass ? '✓ ' : '✗ ') + t.name + ': actual=' + (+t.actual).toFixed(2) + ' expected≈' + t.expected;
885
+ testList.appendChild(li);
886
+ });
887
+ testDetails.appendChild(testList);
888
+ wrap.appendChild(testDetails);
889
+
890
+ // Initial compute
891
+ setTimeout(compute, 0);
892
+ return wrap;
893
+ }
894
+
895
+ // ===================================================================
896
+ // PUBLIC API
897
+ // ===================================================================
898
+ let uiEl = null;
899
+ window.CycleCAD.AIEngineer = {
900
+ // ---- v1: bolted-joint ----
901
+ analyze: boltedJointAnalysis,
902
+ parsePrompt: parseBoltedJointPrompt,
903
+ // ---- v2: gears ----
904
+ analyzeGear: spurGearAnalysis,
905
+ gearAllowableBending,
906
+ gearAllowableContact,
907
+ gearGeometryJ,
908
+ gearGeometryI,
909
+ // ---- v2: shafts ----
910
+ analyzeShaft: shaftFatigueAnalysis,
911
+ SHAFT_MATERIALS,
912
+ SURFACE_FACTORS,
913
+ // ---- shared ----
914
+ runSelfTests,
915
+ STEEL_GRADES,
916
+ BOLT_STRESS_AREA,
917
+ BOLT_MAJOR_DIA,
918
+ FRICTION_PRESETS,
919
+ init: () => {
920
+ const t = runSelfTests();
921
+ if (!t.allPass) {
922
+ console.warn('[AI Engineer] self-test failures:', t.results.filter(r => !r.pass));
923
+ } else {
924
+ console.log('[AI Engineer] self-tests pass (' + t.results.length + '/' + t.results.length + ' across bolted-joint + gears + shafts)');
925
+ }
926
+ return t.allPass;
927
+ },
928
+ getUI: () => { if (!uiEl) uiEl = buildUI(); return uiEl; },
929
+ execute: (cmd, params) => {
930
+ if (cmd === 'analyze') return boltedJointAnalysis(params || {});
931
+ if (cmd === 'analyze-gear') return spurGearAnalysis(params || {});
932
+ if (cmd === 'analyze-shaft') return shaftFatigueAnalysis(params || {});
933
+ if (cmd === 'parse') return parseBoltedJointPrompt((params && params.prompt) || '');
934
+ if (cmd === 'show') { if (!uiEl) uiEl = buildUI(); return uiEl; }
935
+ }
936
+ };
937
+
938
+ console.log('AI Engineering Analyst v2.0 module loaded (bolted-joint + gears + shafts)');
939
+ })();