From eeac92d2654a4ce8849911aefcf1727ea02c3b30 Mon Sep 17 00:00:00 2001 From: DanieCuevas <43822444+DanielCuevas1208@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:02:08 -0700 Subject: [PATCH] feat: extend engineer mcp --- README.md | 29 +++++- docs/integration.md | 2 +- docs/mcp-tools.md | 38 ++++++++ examples/demo.ts | 22 +++++ package-lock.json | 4 +- package.json | 2 +- src/engine/fit.ts | 222 ++++++++++++++++++++++++++++++++++++++++++++ src/engine/index.ts | 1 + src/handlers.ts | 48 ++++++++++ src/schemas.ts | 46 +++++++++ src/server.ts | 6 ++ src/version.ts | 2 +- tests/fit.test.ts | 126 +++++++++++++++++++++++++ tests/tools.test.ts | 60 +++++++++++- 14 files changed, 600 insertions(+), 8 deletions(-) create mode 100644 src/engine/fit.ts create mode 100644 tests/fit.test.ts diff --git a/README.md b/README.md index e94b177..471b035 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ The release covers these domains: - Helical compression spring design. - Shaft torsion and first critical speed. - Bearing rating life to ISO 281. +- Interference fit analysis by Lamé thick-cylinder theory. - von Mises equivalent stress. - Cross-section properties. - Dimension-safe unit conversion. @@ -52,6 +53,7 @@ Warnings surface when a method uses an approximation. | `spring_design` | Spring rate, shear stress, and safety factor of a compression spring. | | `shaft_analysis` | Torsion stress, twist, and critical speed. | | `bearing_life` | ISO 281 rating life in revolutions and hours. | +| `press_fit` | Contact pressure, stresses, and torque capacity of an interference fit. | | `von_mises` | Equivalent stress and yield safety factor. | | `unit_convert` | Conversion between compatible units. | | `material_lookup` | Curated mechanical properties of materials. | @@ -147,6 +149,26 @@ References: - Machinery's Handbook (Thirty-first edition) ``` +A call to `press_fit` for a hardened-steel hub pressed onto a solid shaft with 0.05 mm of diametral interference: + +```text +Interface contact pressure 157.5 MPa +Hub tangential stress 262.5 MPa +Shaft tangential stress -157.5 MPa +Radial stress at interface -157.5 MPa +Hub von Mises stress 367.5 MPa +Shaft von Mises stress 157.5 MPa +Press-in force 55.67 kN +Torque capacity 695.8 N·m + +Method: Interference fit by Lamé thick-cylinder theory +Formula: p = u / (r ((1/Eh)((ro^2+r^2)/(ro^2-r^2)+nh) + (1/Es)((r^2+ri^2)/(r^2-ri^2)-ns))), u = I/2, ... +References: + - Shigley's Mechanical Engineering Design (Tenth edition, 2015) + - Machinery's Handbook (Thirty-first edition) + - Roark's Formulas for Stress and Strain (Eighth edition, 2011) +``` + A call to `unit_convert` with a torque-to-energy request fails safely: ```text @@ -169,7 +191,7 @@ Use a unit of the same quantity. The test suite is deterministic and offline. It covers the engines, the unit layer, the database, and the tools. -- 98 tests across 10 files. +- 115 tests across 11 files. - All tests pass on Node 22 and Node 24. - The CI workflow runs typecheck, tests, build, demo, and a package check. @@ -184,6 +206,8 @@ Run `npm test` to reproduce the results. - The critical speed is a first-mode approximation. - The spring design covers static round-wire springs only. It does not estimate fatigue life for cyclic loads. +- The press fit uses plane stress and assumes a uniform friction coefficient. + It ignores rotation, temperature, and surface roughness effects. - The built-in SQLite module of Node.js is still experimental. Check the cited sources for exact values. @@ -197,11 +221,12 @@ Each release stays useful on its own. - Helical compression spring design. The `spring_design` tool reports the spring rate, the shear stress, and the safety factor. +- Press-fit and interference-fit analysis. + The `press_fit` tool reports the contact pressure, the stresses, the press-in force, and the torque capacity. ### Remaining - Add fatigue analysis for cyclic loads. -- Add press-fit and interference-fit calculators. - Add more unit categories, including viscosity and thermal conductivity. - Add HTTP transport. - Add a catalog of ISO and DIN standard sections. diff --git a/docs/integration.md b/docs/integration.md index 042b7d4..1ced5cd 100644 --- a/docs/integration.md +++ b/docs/integration.md @@ -41,11 +41,11 @@ Engineer MCP grows in independent releases. Each release stays useful on its own ### Complete - Helical compression spring design. The `spring_design` tool computes the spring rate, the shear stress, and the safety factor. +- Press-fit and interference-fit analysis. The `press_fit` tool computes the contact pressure, the stresses, the press-in force, and the torque capacity. ### Remaining - Add fatigue analysis for cyclic loads. -- Add press-fit and interference-fit calculators. - Add more unit categories, including viscosity and thermal conductivity. - Add HTTP transport in addition to stdio. - Add a catalog of ISO and DIN standard sections. diff --git a/docs/mcp-tools.md b/docs/mcp-tools.md index d38b8c9..705d267 100644 --- a/docs/mcp-tools.md +++ b/docs/mcp-tools.md @@ -135,6 +135,44 @@ Inputs: - `speedRpm`: enables life in hours. - `requiredLifeHours`: enables the life margin. +## press_fit + +Compute the contact pressure and stresses of an interference fit. + +The tool uses the Lamé thick-cylinder solution in plane stress. The hub carries tensile hoop stress. The shaft carries compressive hoop stress. A solid shaft sees biaxial compression equal to the contact pressure. + +Inputs: + +- `hubOuterDiameter`: hub outer diameter in metres. +- `interfaceDiameter`: diameter where the hub bore meets the shaft outside in metres. +- `shaftInnerDiameter`: shaft inner diameter in metres. Omit or use zero for a solid shaft. +- `hubLength`: hub engagement length in metres. +- `diametralInterference`: total diametral interference in metres. +- `hubMaterial` and `shaftMaterial`: material names from the database. +- `hubElasticModulus` and `shaftElasticModulus`: elastic moduli in pascals. Use these instead of materials. +- `hubPoissonRatio` and `shaftPoissonRatio`: Poisson ratios. The default is `0.3`. +- `hubYieldStrength` and `shaftYieldStrength`: yield strengths in pascals. They enable the safety factors. +- `frictionCoefficient`: friction at the interface. The default is `0.15`. +- `appliedAxialForce`: applied axial force in newtons. It enables the axial joint safety factor. + +The tool reports the contact pressure, the hoop stresses, the von Mises stresses, the press-in force, and the torque capacity. The hub safety factor is the primary safety factor. The shaft and axial joint factors appear as quantities. + +The tool warns on a thin hub. It warns when an applied axial force exceeds the press-in force. + +Example: + +```json +{ + "hubOuterDiameter": 0.05, + "interfaceDiameter": 0.025, + "hubLength": 0.03, + "diametralInterference": 0.00005, + "hubMaterial": "Alloy steel 42CrMo4", + "shaftMaterial": "Alloy steel 42CrMo4", + "outputUnits": { "contactPressure": "MPa", "pressForce": "kN" } +} +``` + ## von_mises Compute the von Mises equivalent stress and yield safety factor of a stress state. diff --git a/examples/demo.ts b/examples/demo.ts index 2013998..2f973aa 100644 --- a/examples/demo.ts +++ b/examples/demo.ts @@ -73,6 +73,7 @@ type ToolHandlers = { spring_design: Handler; shaft_analysis: Handler; bearing_life: Handler; + press_fit: Handler; von_mises: Handler; unit_convert: Handler; material_lookup: Handler; @@ -87,6 +88,7 @@ const tools: NamedHandler[] = [ ["spring_design", toolHandlers.spring_design], ["shaft_analysis", toolHandlers.shaft_analysis], ["bearing_life", toolHandlers.bearing_life], + ["press_fit", toolHandlers.press_fit], ["von_mises", toolHandlers.von_mises], ["unit_convert", toolHandlers.unit_convert], ["unit_convert (torque to energy)", toolHandlers.unit_convert], @@ -137,6 +139,26 @@ const inputs: Array> = [ speedRpm: 1500, requiredLifeHours: 20000, }, + { + hubOuterDiameter: 0.05, + interfaceDiameter: 0.025, + hubLength: 0.03, + diametralInterference: 0.00005, + hubMaterial: "Alloy steel 42CrMo4", + shaftMaterial: "Alloy steel 42CrMo4", + frictionCoefficient: 0.15, + appliedAxialForce: 20000, + outputUnits: { + contactPressure: "MPa", + hubTangentialStress: "MPa", + shaftTangentialStress: "MPa", + radialStress: "MPa", + hubVonMisesStress: "MPa", + shaftVonMisesStress: "MPa", + pressForce: "kN", + torqueCapacity: "N·m", + }, + }, { mode: "cartesian", sigmaX: 120e6, diff --git a/package-lock.json b/package-lock.json index 5c98405..edcef02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@engineerkit/engineer-mcp", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@engineerkit/engineer-mcp", - "version": "0.2.0", + "version": "0.3.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", diff --git a/package.json b/package.json index 49eb29e..2a8612f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@engineerkit/engineer-mcp", - "version": "0.2.0", + "version": "0.3.0", "description": "MCP server for verified mechanical-engineering calculations, references, and material data.", "type": "module", "license": "MIT", diff --git a/src/engine/fit.ts b/src/engine/fit.ts new file mode 100644 index 0000000..905b610 --- /dev/null +++ b/src/engine/fit.ts @@ -0,0 +1,222 @@ +import type { Computation, MethodRecord, Quantity } from "../types.js"; + +export type FitInput = { + hubOuterDiameter: number; + interfaceDiameter: number; + shaftInnerDiameter?: number; + hubLength: number; + diametralInterference: number; + hubElasticModulus: number; + hubPoissonRatio?: number; + hubYieldStrength?: number; + shaftElasticModulus: number; + shaftPoissonRatio?: number; + shaftYieldStrength?: number; + frictionCoefficient?: number; + appliedAxialForce?: number; +}; + +export const PRESS_FIT_METHOD: MethodRecord = { + id: "press-fit", + name: "Interference fit by Lamé thick-cylinder theory", + formula: + "p = u / (r ((1/Eh)((ro^2+r^2)/(ro^2-r^2)+nh) + (1/Es)((r^2+ri^2)/(r^2-ri^2)-ns))), u = I/2, sigma_t,h = p (ro^2+r^2)/(ro^2-r^2), F = mu p pi d L, T = F r", + notes: + "Plane-stress Lamé solution for two cylinders in interference. The hub carries tensile hoop stress, the shaft carries compressive hoop stress. A solid shaft sees biaxial compression equal to the contact pressure. The press-in force assumes a uniform friction coefficient over the full engagement length. The default coefficient of 0.15 suits dry steel-on-steel.", + referenceIds: ["shigley-2015", "machinery-handbook", "roark-2011"], +}; + +const DEFAULT_POISSON_RATIO = 0.3; +const DEFAULT_FRICTION = 0.15; +const THIN_HUB_RATIO = 1.5; + +export function planeStressVonMises(sigmaTangential: number, sigmaRadial: number): number { + const [a, b, c] = [sigmaTangential, sigmaRadial, 0]; + return Math.sqrt(0.5 * ((a - b) ** 2 + (b - c) ** 2 + (c - a) ** 2)); +} + +export function analyzePressFit(input: FitInput): Computation { + if (!(input.hubOuterDiameter > 0)) { + throw new Error("hubOuterDiameter must be positive."); + } + if (!(input.interfaceDiameter > 0)) { + throw new Error("interfaceDiameter must be positive."); + } + if (input.interfaceDiameter >= input.hubOuterDiameter) { + throw new Error("hubOuterDiameter must exceed interfaceDiameter."); + } + const shaftInner = input.shaftInnerDiameter ?? 0; + if (shaftInner < 0 || shaftInner >= input.interfaceDiameter) { + throw new Error("shaftInnerDiameter must be smaller than interfaceDiameter."); + } + if (!(input.hubLength > 0)) { + throw new Error("hubLength must be positive."); + } + if (!(input.diametralInterference > 0)) { + throw new Error("diametralInterference must be positive."); + } + if (!(input.hubElasticModulus > 0)) { + throw new Error("hubElasticModulus must be positive."); + } + if (!(input.shaftElasticModulus > 0)) { + throw new Error("shaftElasticModulus must be positive."); + } + const hubPoisson = input.hubPoissonRatio ?? DEFAULT_POISSON_RATIO; + const shaftPoisson = input.shaftPoissonRatio ?? DEFAULT_POISSON_RATIO; + if (!(hubPoisson >= 0 && hubPoisson < 0.5)) { + throw new Error("hubPoissonRatio must be below 0.5."); + } + if (!(shaftPoisson >= 0 && shaftPoisson < 0.5)) { + throw new Error("shaftPoissonRatio must be below 0.5."); + } + const friction = input.frictionCoefficient ?? DEFAULT_FRICTION; + if (!(friction >= 0)) { + throw new Error("frictionCoefficient must be zero or positive."); + } + + const innerRadius = shaftInner / 2; + const interfaceRadius = input.interfaceDiameter / 2; + const outerRadius = input.hubOuterDiameter / 2; + const radialInterference = input.diametralInterference / 2; + + const hubTerm = (outerRadius ** 2 + interfaceRadius ** 2) / (outerRadius ** 2 - interfaceRadius ** 2) + hubPoisson; + const shaftTerm = (interfaceRadius ** 2 + innerRadius ** 2) / (interfaceRadius ** 2 - innerRadius ** 2) - shaftPoisson; + + const contactPressure = + radialInterference / (interfaceRadius * (hubTerm / input.hubElasticModulus + shaftTerm / input.shaftElasticModulus)); + + const hubTangentialStress = + (contactPressure * (outerRadius ** 2 + interfaceRadius ** 2)) / (outerRadius ** 2 - interfaceRadius ** 2); + const shaftTangentialStress = + (-contactPressure * (interfaceRadius ** 2 + innerRadius ** 2)) / (interfaceRadius ** 2 - innerRadius ** 2); + const radialStress = -contactPressure; + + const hubVonMisesStress = planeStressVonMises(hubTangentialStress, radialStress); + const shaftVonMisesStress = planeStressVonMises(shaftTangentialStress, radialStress); + + const contactArea = Math.PI * input.interfaceDiameter * input.hubLength; + const pressForce = friction * contactPressure * contactArea; + const torqueCapacity = pressForce * interfaceRadius; + + const quantities: Quantity[] = [ + { + key: "contactPressure", + label: "Interface contact pressure", + value: contactPressure, + unit: "Pa", + description: "Uniform radial pressure at the interface caused by the interference.", + }, + { + key: "hubTangentialStress", + label: "Hub tangential stress", + value: hubTangentialStress, + unit: "Pa", + description: "Tensile hoop stress at the hub bore.", + }, + { + key: "shaftTangentialStress", + label: "Shaft tangential stress", + value: shaftTangentialStress, + unit: "Pa", + description: "Compressive hoop stress at the shaft surface.", + }, + { + key: "radialStress", + label: "Radial stress at interface", + value: radialStress, + unit: "Pa", + description: "Compressive radial stress equal to the negative of the contact pressure.", + }, + { + key: "hubVonMisesStress", + label: "Hub von Mises stress", + value: hubVonMisesStress, + unit: "Pa", + description: "Equivalent stress at the hub bore under plane stress.", + }, + { + key: "shaftVonMisesStress", + label: "Shaft von Mises stress", + value: shaftVonMisesStress, + unit: "Pa", + description: "Equivalent stress in the shaft under plane stress.", + }, + { + key: "pressForce", + label: "Press-in force", + value: pressForce, + unit: "N", + description: "Axial force needed to assemble the joint at the given friction coefficient.", + }, + { + key: "torqueCapacity", + label: "Torque capacity", + value: torqueCapacity, + unit: "N·m", + description: "Torque the joint can transmit before slipping at the interface.", + }, + ]; + + const warnings: string[] = []; + if (outerRadius / interfaceRadius < THIN_HUB_RATIO) { + warnings.push("The hub is thin. The solution stays valid but check local yielding near the bore."); + } + + let safetyFactor: Quantity | undefined; + if (input.hubYieldStrength) { + safetyFactor = { + key: "hubSafetyFactor", + label: "Hub safety factor", + value: input.hubYieldStrength / hubVonMisesStress, + unit: "", + description: "Hub yield strength divided by the hub von Mises stress.", + }; + } + + if (input.shaftYieldStrength && shaftVonMisesStress > 0) { + quantities.push({ + key: "shaftSafetyFactor", + label: "Shaft safety factor", + value: input.shaftYieldStrength / shaftVonMisesStress, + unit: "", + description: "Shaft yield strength divided by the shaft von Mises stress.", + }); + } + + if (input.appliedAxialForce !== undefined) { + quantities.push({ + key: "axialJointSafetyFactor", + label: "Axial joint safety factor", + value: pressForce / input.appliedAxialForce, + unit: "", + description: "Press-in force divided by the applied axial force.", + }); + if (input.appliedAxialForce > pressForce) { + warnings.push("The applied axial force exceeds the press-in force. The joint may separate or slip."); + } + } + + return { + method: PRESS_FIT_METHOD, + inputs: { + hubOuterDiameter: input.hubOuterDiameter, + interfaceDiameter: input.interfaceDiameter, + shaftInnerDiameter: shaftInner, + hubLength: input.hubLength, + diametralInterference: input.diametralInterference, + radialInterference, + hubPoissonRatio: hubPoisson, + shaftPoissonRatio: shaftPoisson, + frictionCoefficient: friction, + hubElasticModulus: input.hubElasticModulus, + hubYieldStrength: input.hubYieldStrength, + shaftElasticModulus: input.shaftElasticModulus, + shaftYieldStrength: input.shaftYieldStrength, + appliedAxialForce: input.appliedAxialForce, + }, + quantities, + safetyFactor, + referenceIds: PRESS_FIT_METHOD.referenceIds, + warnings, + }; +} diff --git a/src/engine/index.ts b/src/engine/index.ts index 92492d7..05ef87c 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -1,4 +1,5 @@ export { analyzeBeam, BEAM_METHOD, type BeamInput, type BeamLoad, type BeamSupport } from "./beam.js"; +export { analyzePressFit, planeStressVonMises, PRESS_FIT_METHOD, type FitInput } from "./fit.js"; export { analyzeBearing, BEARING_METHOD, equivalentLoad, type BearingInput, type BearingType } from "./bearing.js"; export { analyzeBolt, BOLT_METHOD, tensileStressArea, type BoltGradeData, type BoltInput } from "./bolt.js"; export { computeSection, SECTION_METHOD, type SectionDef, type SectionProperties } from "./sections.js"; diff --git a/src/handlers.ts b/src/handlers.ts index c646e73..8bf978b 100644 --- a/src/handlers.ts +++ b/src/handlers.ts @@ -3,6 +3,7 @@ import { analyzeBeam, analyzeBearing, analyzeBolt, + analyzePressFit, analyzeShaft, analyzeSpring, computeSection, @@ -332,6 +333,52 @@ function bearingHandler(ctx: AppContext): Handler { }; } +function pressFitHandler(ctx: AppContext): Handler { + return (input) => { + const hubName = input.hubMaterial as string | undefined; + const hubValues = hubName ? materialValues(ctx, hubName) : undefined; + if (hubName && !hubValues) { + return failure("press_fit", `Unknown hub material: ${hubName}`, input); + } + const shaftName = input.shaftMaterial as string | undefined; + const shaftValues = shaftName ? materialValues(ctx, shaftName) : undefined; + if (shaftName && !shaftValues) { + return failure("press_fit", `Unknown shaft material: ${shaftName}`, input); + } + + const hubElasticModulus = (input.hubElasticModulus as number | undefined) ?? hubValues?.elasticModulusPa; + const shaftElasticModulus = (input.shaftElasticModulus as number | undefined) ?? shaftValues?.elasticModulusPa; + if (!hubElasticModulus || !shaftElasticModulus) { + return failure( + "press_fit", + "Provide hubElasticModulus and shaftElasticModulus, or known hubMaterial and shaftMaterial.", + input, + ); + } + + try { + const computation = analyzePressFit({ + hubOuterDiameter: input.hubOuterDiameter as number, + interfaceDiameter: input.interfaceDiameter as number, + shaftInnerDiameter: input.shaftInnerDiameter as number | undefined, + hubLength: input.hubLength as number, + diametralInterference: input.diametralInterference as number, + hubElasticModulus, + hubPoissonRatio: input.hubPoissonRatio as number | undefined, + hubYieldStrength: (input.hubYieldStrength as number | undefined) ?? hubValues?.yieldStrengthPa, + shaftElasticModulus, + shaftPoissonRatio: input.shaftPoissonRatio as number | undefined, + shaftYieldStrength: (input.shaftYieldStrength as number | undefined) ?? shaftValues?.yieldStrengthPa, + frictionCoefficient: input.frictionCoefficient as number | undefined, + appliedAxialForce: input.appliedAxialForce as number | undefined, + }); + return buildResult(ctx, "press_fit", computation, input.outputUnits as Record | undefined); + } catch (error) { + return failure("press_fit", error instanceof Error ? error.message : String(error), input); + } + }; +} + function stressHandler(ctx: AppContext): Handler { return (input) => { const mode = input.mode as "principal" | "cartesian"; @@ -445,6 +492,7 @@ export function createHandlers(ctx: AppContext): Record { shaft_analysis: shaftHandler(ctx), spring_design: springHandler(ctx), bearing_life: bearingHandler(ctx), + press_fit: pressFitHandler(ctx), von_mises: stressHandler(ctx), unit_convert: unitConvertHandler(ctx), material_lookup: materialHandler(ctx), diff --git a/src/schemas.ts b/src/schemas.ts index 7204c18..6651674 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -126,6 +126,51 @@ export const unitConvertSchema = z.object({ to: z.string().describe("Target unit symbol. Examples: psi, m, N, degF."), }); +export const pressFitSchema = z.object({ + hubOuterDiameter: z.number().positive().describe("Hub outer diameter in metres."), + interfaceDiameter: z + .number() + .positive() + .describe("Interface diameter in metres. The hub bore and the shaft outside diameter meet here."), + shaftInnerDiameter: z + .number() + .nonnegative() + .optional() + .describe("Shaft inner diameter in metres. Omit or use zero for a solid shaft."), + hubLength: z.number().positive().describe("Hub engagement length in metres."), + diametralInterference: z + .number() + .positive() + .describe("Total diametral interference in metres. Half of this value acts as the radial interference."), + hubMaterial: z.string().optional().describe("Hub material name from the database."), + hubElasticModulus: z.number().positive().optional().describe("Hub elastic modulus in pascals. Optional when hubMaterial is set."), + hubPoissonRatio: z.number().min(0).lt(0.5).optional().describe("Hub Poisson ratio. Defaults to 0.3."), + hubYieldStrength: z.number().positive().optional().describe("Hub yield strength in pascals. Enables the hub safety factor."), + shaftMaterial: z.string().optional().describe("Shaft material name from the database."), + shaftElasticModulus: z + .number() + .positive() + .optional() + .describe("Shaft elastic modulus in pascals. Optional when shaftMaterial is set."), + shaftPoissonRatio: z.number().min(0).lt(0.5).optional().describe("Shaft Poisson ratio. Defaults to 0.3."), + shaftYieldStrength: z + .number() + .positive() + .optional() + .describe("Shaft yield strength in pascals. Enables the shaft safety factor for a hollow shaft."), + frictionCoefficient: z + .number() + .min(0) + .optional() + .describe("Coefficient of friction at the interface. Defaults to 0.15 for dry steel-on-steel."), + appliedAxialForce: z + .number() + .positive() + .optional() + .describe("Applied axial force in newtons. Enables the axial joint safety factor."), + outputUnits, +}); + export const materialSchema = z.object({ query: z.string().min(1).describe("Material name or category to search. Matches are case-insensitive."), limit: z.number().int().min(1).max(50).optional().describe("Maximum number of rows to return. Defaults to 10."), @@ -138,5 +183,6 @@ export type SpringInput = z.infer; export type BearingInput = z.infer; export type SectionPropsInput = z.infer; export type StressInput = z.infer; +export type PressFitInput = z.infer; export type UnitConvertInput = z.infer; export type MaterialInput = z.infer; diff --git a/src/server.ts b/src/server.ts index 2029a75..bd136b0 100644 --- a/src/server.ts +++ b/src/server.ts @@ -7,6 +7,7 @@ import { bearingSchema, boltSchema, materialSchema, + pressFitSchema, sectionPropsSchema, shaftSchema, springSchema, @@ -48,6 +49,11 @@ const TOOL_SCHEMAS: Record = { description: "ISO 281 basic rating life L10 for ball and roller bearings, in revolutions and hours.", schema: bearingSchema, }, + press_fit: { + description: + "Interference fit analysis by Lamé thick-cylinder theory. Reports contact pressure, hub and shaft stresses, press-in force, and torque capacity.", + schema: pressFitSchema, + }, von_mises: { description: "von Mises equivalent stress, maximum shear stress, and yield safety factor for a stress state.", schema: stressSchema, diff --git a/src/version.ts b/src/version.ts index 9e86f0c..03714b1 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ -export const VERSION = "0.2.0"; +export const VERSION = "0.3.0"; export const SERVER_NAME = "engineer-mcp"; diff --git a/tests/fit.test.ts b/tests/fit.test.ts new file mode 100644 index 0000000..6214a1f --- /dev/null +++ b/tests/fit.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, it } from "vitest"; +import { analyzePressFit, planeStressVonMises, PRESS_FIT_METHOD } from "../src/engine/fit.js"; + +function quantity(result: ReturnType, key: string): number { + const found = result.quantities.find((q) => q.key === key); + expect(found).toBeDefined(); + return found?.value as number; +} + +function closeTo(value: number, expected: number): void { + expect(Math.abs(value - expected)).toBeLessThan(Math.abs(expected) * 1e-6); +} + +const solidSteel = { + hubOuterDiameter: 0.05, + interfaceDiameter: 0.025, + shaftInnerDiameter: 0, + hubLength: 0.03, + diametralInterference: 0.00005, + hubElasticModulus: 210e9, + shaftElasticModulus: 210e9, + hubPoissonRatio: 0.3, + shaftPoissonRatio: 0.3, + frictionCoefficient: 0.15, +}; + +describe("press fit engine", () => { + it("computes the contact pressure by the Lamé closed form", () => { + const result = analyzePressFit(solidSteel); + const r = 0.0125; + const u = 0.000025; + const E = 210e9; + closeTo(quantity(result, "contactPressure"), (3 * u * E) / (8 * r)); + expect(result.method.id).toBe("press-fit"); + }); + + it("reports the hub hoop stress as tension and the shaft as compression", () => { + const result = analyzePressFit(solidSteel); + const p = quantity(result, "contactPressure"); + const ratio = (0.025 ** 2 + 0.0125 ** 2) / (0.025 ** 2 - 0.0125 ** 2); + closeTo(quantity(result, "hubTangentialStress"), p * ratio); + closeTo(quantity(result, "shaftTangentialStress"), -p); + closeTo(quantity(result, "radialStress"), -p); + }); + + it("reports a shaft von Mises stress equal to the pressure magnitude", () => { + const result = analyzePressFit(solidSteel); + const p = quantity(result, "contactPressure"); + closeTo(quantity(result, "shaftVonMisesStress"), p); + }); + + it("computes the von Mises stress in the hub", () => { + const result = analyzePressFit(solidSteel); + const tangential = quantity(result, "hubTangentialStress"); + const radial = quantity(result, "radialStress"); + closeTo(quantity(result, "hubVonMisesStress"), planeStressVonMises(tangential, radial)); + }); + + it("computes the press-in force and torque capacity", () => { + const result = analyzePressFit(solidSteel); + const p = quantity(result, "contactPressure"); + closeTo(quantity(result, "pressForce"), 0.15 * p * Math.PI * 0.025 * 0.03); + closeTo(quantity(result, "torqueCapacity"), quantity(result, "pressForce") * 0.0125); + }); + + it("adds a hub safety factor when the hub yield strength is set", () => { + const result = analyzePressFit({ ...solidSteel, hubYieldStrength: 700e6 }); + expect(result.safetyFactor?.key).toBe("hubSafetyFactor"); + expect(result.safetyFactor?.value).toBeGreaterThan(0); + }); + + it("adds a shaft safety factor when the shaft yield strength is set", () => { + const result = analyzePressFit({ + ...solidSteel, + interfaceDiameter: 0.03, + hubOuterDiameter: 0.06, + shaftInnerDiameter: 0.01, + shaftYieldStrength: 500e6, + }); + expect(quantity(result, "shaftVonMisesStress")).toBeGreaterThan(0); + const shaftFactor = result.quantities.find((q) => q.key === "shaftSafetyFactor"); + expect(shaftFactor?.value).toBeGreaterThan(0); + }); + + it("reports the axial joint safety factor and warns when the joint slips", () => { + const result = analyzePressFit({ ...solidSteel, appliedAxialForce: 100000 }); + const axial = result.quantities.find((q) => q.key === "axialJointSafetyFactor"); + expect(axial).toBeDefined(); + expect(result.warnings.some((w) => w.includes("exceeds"))).toBe(true); + }); + + it("warns for a thin hub", () => { + const result = analyzePressFit({ ...solidSteel, hubOuterDiameter: 0.03 }); + expect(result.warnings.some((w) => w.includes("thin"))).toBe(true); + }); + + it("applies the default Poisson ratio and friction coefficient", () => { + const result = analyzePressFit({ + hubOuterDiameter: 0.05, + interfaceDiameter: 0.025, + hubLength: 0.03, + diametralInterference: 0.00005, + hubElasticModulus: 210e9, + shaftElasticModulus: 210e9, + }); + expect(result.inputs.hubPoissonRatio).toBe(0.3); + expect(result.inputs.shaftPoissonRatio).toBe(0.3); + expect(result.inputs.frictionCoefficient).toBe(0.15); + }); + + it("rejects an interface diameter equal to the hub outer diameter", () => { + expect(() => analyzePressFit({ ...solidSteel, hubOuterDiameter: 0.025 })).toThrow(/hubOuterDiameter/); + }); + + it("rejects a hollow shaft whose inner diameter reaches the interface", () => { + expect(() => analyzePressFit({ ...solidSteel, shaftInnerDiameter: 0.025 })).toThrow(/shaftInnerDiameter/); + }); + + it("rejects a zero interference", () => { + expect(() => analyzePressFit({ ...solidSteel, diametralInterference: 0 })).toThrow(/diametralInterference/); + }); + + it("cites the reference standards", () => { + expect(PRESS_FIT_METHOD.referenceIds).toContain("shigley-2015"); + }); +}); diff --git a/tests/tools.test.ts b/tests/tools.test.ts index dd63bf0..4a0cf2a 100644 --- a/tests/tools.test.ts +++ b/tests/tools.test.ts @@ -12,6 +12,7 @@ type Handlers = { spring_design: Handler; shaft_analysis: Handler; bearing_life: Handler; + press_fit: Handler; von_mises: Handler; unit_convert: Handler; material_lookup: Handler; @@ -31,7 +32,7 @@ function expectOk(response: Awaited>): ToolResult { } describe("tool registry", () => { - it("registers all nine tools", () => { + it("registers all ten tools", () => { expect(listTools().sort()).toEqual( [ "beam_bending", @@ -40,6 +41,7 @@ describe("tool registry", () => { "spring_design", "shaft_analysis", "bearing_life", + "press_fit", "von_mises", "unit_convert", "material_lookup", @@ -247,6 +249,62 @@ describe("bearing_life tool", () => { }); }); +describe("press_fit tool", () => { + it("computes an interference fit from materials and converts units", () => { + setup(); + const response = handlers.press_fit({ + hubOuterDiameter: 0.05, + interfaceDiameter: 0.025, + hubLength: 0.03, + diametralInterference: 0.00005, + hubMaterial: "Alloy steel 42CrMo4", + shaftMaterial: "Alloy steel 42CrMo4", + outputUnits: { contactPressure: "MPa", pressForce: "kN", torqueCapacity: "N·m" }, + }); + const result = expectOk(response); + + expect(result.tool).toBe("press_fit"); + expect(result.method.id).toBe("press-fit"); + expect(result.safetyFactor?.key).toBe("hubSafetyFactor"); + expect(result.references.length).toBeGreaterThan(0); + + const pressure = result.quantities.find((q) => q.key === "contactPressure"); + expect(pressure?.unit).toBe("MPa"); + expect(pressure?.value).toBeCloseTo(157.5, -1); + const pressForce = result.quantities.find((q) => q.key === "pressForce"); + expect(pressForce?.unit).toBe("kN"); + const torque = result.quantities.find((q) => q.key === "torqueCapacity"); + expect(torque?.unit).toBe("N·m"); + }); + + it("rejects an unknown material", () => { + setup(); + const response = handlers.press_fit({ + hubOuterDiameter: 0.05, + interfaceDiameter: 0.025, + hubLength: 0.03, + diametralInterference: 0.00005, + hubMaterial: "Unobtainium", + shaftMaterial: "Alloy steel 42CrMo4", + }); + expect(response.ok).toBe(false); + }); + + it("requires elastic moduli when no materials are given", () => { + setup(); + const response = handlers.press_fit({ + hubOuterDiameter: 0.05, + interfaceDiameter: 0.025, + hubLength: 0.03, + diametralInterference: 0.00005, + }); + expect(response.ok).toBe(false); + if (!response.ok) { + expect(response.error).toContain("hubElasticModulus"); + } + }); +}); + describe("von_mises tool", () => { it("computes cartesian stress with yield", () => { setup();