import { TAU, type KinematicValidationReport, type KinematicValidationSuite, type ScalarTrack, type ValidationRunOptions, angularTravelConstraint, bearingBallSpinRatio, bearingCageRatio, centerOrbitConstraint, clamp, constantAngularVelocityConstraint, crankSliderConstraint, crankSliderDisplacement, differentialAverageConstraint, finiteScalarTrackConstraint, formatKinematicValidationReport, genevaIndexConstraint, genevaOutputAngle, monotonicScalarConstraint, phaseRatioConstraint, positiveModulo, radialDistanceConstraint, runKinematicValidation, scalarPointConstraint, scalarRangeConstraint, smootherstep01, symmetryConstraint, willisPlanetaryConstraint } from '../../../animations/kinematicIntegrity'; export const PROCEDURAL_KINEMATIC_MACHINE_IDS = [ 'four-stroke-petrol-engine', 'planetary-gearbox', 'differential-gear', 'centrifugal-pump', 'geneva-drive', 'ball-bearing', 'disc-brake-caliper', 'wankel-rotary-engine' ] as const; export type ProceduralKinematicMachineId = (typeof PROCEDURAL_KINEMATIC_MACHINE_IDS)[number]; export const proceduralKinematicValidationSuites: KinematicValidationSuite[] = [ createFourStrokePetrolEngineSuite(), createPlanetaryGearboxSuite(), createDifferentialGearSuite(), createCentrifugalPumpSuite(), createGenevaDriveSuite(), createBallBearingSuite(), createDiscBrakeCaliperSuite(), createWankelRotaryEngineSuite() ]; export function runProceduralKinematicsValidation(options: ValidationRunOptions = {}): KinematicValidationReport { return runKinematicValidation(proceduralKinematicValidationSuites, { treatWarningsAsErrors: true, ...options }); } export { formatKinematicValidationReport }; function createFourStrokePetrolEngineSuite(): KinematicValidationSuite { const rpm = 900; const omega = rpmToRadiansPerSecond(rpm); const durationSeconds = (4 * Math.PI) / omega; const crankRadiusMeters = 0.039; const rodLengthMeters = 0.142; const valveLiftMeters = 0.009; const crank = track('engine.crankshaft.angle', (timeSeconds) => omega * timeSeconds, 'radians'); const cam = track('engine.camshaft.angle', (timeSeconds) => crank.sample(timeSeconds) * 0.5, 'radians'); const piston = track( 'engine.piston.position', (timeSeconds) => crankSliderDisplacement(crank.sample(timeSeconds), crankRadiusMeters, rodLengthMeters), 'meters' ); const intakeValve = track( 'engine.intake-valve.lift', (timeSeconds) => strokeWindowLift(crank.sample(timeSeconds), 0, Math.PI, valveLiftMeters), 'meters' ); const exhaustValve = track( 'engine.exhaust-valve.lift', (timeSeconds) => strokeWindowLift(crank.sample(timeSeconds), 3 * Math.PI, 4 * Math.PI, valveLiftMeters), 'meters' ); const spark = track( 'engine.spark-plug.energy', (timeSeconds) => gaussianPulse(crank.sample(timeSeconds), 2 * Math.PI, 0.08, 4 * Math.PI), 'unitless' ); return { id: 'four-stroke-petrol-engine.kinematics', machineId: 'four-stroke-petrol-engine', title: 'Four-stroke petrol engine cycle timing', durationSeconds, sampleCount: 181, metadata: { rpm, crankRadiusMeters, rodLengthMeters, valveLiftMeters, crankRevolutionsPerCycle: 2 }, constraints: [ ...finiteTracks(crank, cam, piston, intakeValve, exhaustValve, spark), constantAngularVelocityConstraint({ track: crank, radiansPerSecond: omega, tolerance: { absolute: 1e-8, relative: 1e-10 } }), phaseRatioConstraint({ driver: crank, driven: cam, ratio: 0.5, tolerance: { absolute: 1e-8, relative: 1e-10 } }), angularTravelConstraint({ track: crank, expectedDeltaRadians: 4 * Math.PI, tolerance: { absolute: 1e-8, relative: 1e-10 } }), angularTravelConstraint({ track: cam, expectedDeltaRadians: 2 * Math.PI, tolerance: { absolute: 1e-8, relative: 1e-10 } }), crankSliderConstraint({ crankAngle: crank, pistonPosition: piston, crankRadiusMeters, connectingRodLengthMeters: rodLengthMeters, tolerance: { absolute: 1e-8, relative: 1e-8 } }), scalarRangeConstraint(intakeValve, { min: 0, max: valveLiftMeters }, { tolerance: { absolute: 1e-10 } }), scalarRangeConstraint(exhaustValve, { min: 0, max: valveLiftMeters }, { tolerance: { absolute: 1e-10 } }), scalarRangeConstraint(spark, { min: 0, max: 1 }, { tolerance: { absolute: 1e-10 } }), scalarPointConstraint( intakeValve, [{ atFraction: 0.125, expected: valveLiftMeters, tolerance: { absolute: 1e-10 }, label: 'mid-intake' }], { id: 'engine.intake-event' } ), scalarPointConstraint( exhaustValve, [{ atFraction: 0.875, expected: valveLiftMeters, tolerance: { absolute: 1e-10 }, label: 'mid-exhaust' }], { id: 'engine.exhaust-event' } ), scalarPointConstraint( spark, [{ atFraction: 0.5, expected: 1, tolerance: { absolute: 1e-10 }, label: 'spark near TDC before power' }], { id: 'engine.spark-event' } ) ] }; } function createPlanetaryGearboxSuite(): KinematicValidationSuite { const sunTeeth = 24; const ringTeeth = 72; const planetTeeth = (ringTeeth - sunTeeth) / 2; const durationSeconds = 1; const inputOmega = TAU; const carrierRatio = sunTeeth / (sunTeeth + ringTeeth); const sun = track('planetary.sun.angle', (timeSeconds) => inputOmega * timeSeconds, 'radians'); const ring = track('planetary.ring.angle', () => 0, 'radians'); const carrier = track('planetary.carrier.angle', (timeSeconds) => sun.sample(timeSeconds) * carrierRatio, 'radians'); const planetSpin = track( 'planetary.planet.spin', (timeSeconds) => -((sunTeeth / planetTeeth) * (sun.sample(timeSeconds) - carrier.sample(timeSeconds))), 'radians' ); return { id: 'planetary-gearbox.kinematics', machineId: 'planetary-gearbox', title: 'Planetary gearbox Willis relation', durationSeconds, sampleCount: 145, metadata: { sunTeeth, ringTeeth, planetTeeth, carrierRatio }, constraints: [ ...finiteTracks(sun, ring, carrier, planetSpin), constantAngularVelocityConstraint({ track: sun, radiansPerSecond: inputOmega, tolerance: { absolute: 1e-8, relative: 1e-10 } }), willisPlanetaryConstraint({ sunAngle: sun, ringAngle: ring, carrierAngle: carrier, sunTeeth, ringTeeth, tolerance: { absolute: 1e-8, relative: 1e-10 } }), phaseRatioConstraint({ driver: sun, driven: carrier, ratio: carrierRatio, tolerance: { absolute: 1e-8, relative: 1e-10 } }), phaseRatioConstraint({ driver: sun, driven: planetSpin, ratio: -((sunTeeth / planetTeeth) * (1 - carrierRatio)), tolerance: { absolute: 1e-8, relative: 1e-10 } }), angularTravelConstraint({ track: carrier, expectedDeltaRadians: TAU * carrierRatio, tolerance: { absolute: 1e-8, relative: 1e-10 } }) ] }; } function createDifferentialGearSuite(): KinematicValidationSuite { const durationSeconds = 1; const carrierOmega = TAU; const turnBiasRadians = 0.45; const carrier = track('differential.carrier.angle', (timeSeconds) => carrierOmega * timeSeconds, 'radians'); const bias = (timeSeconds: number) => turnBiasRadians * Math.sin(Math.PI * (timeSeconds / durationSeconds)); const leftOutput = track( 'differential.left-output.angle', (timeSeconds) => carrier.sample(timeSeconds) + bias(timeSeconds), 'radians' ); const rightOutput = track( 'differential.right-output.angle', (timeSeconds) => carrier.sample(timeSeconds) - bias(timeSeconds), 'radians' ); return { id: 'differential-gear.kinematics', machineId: 'differential-gear', title: 'Open differential averaging and wheel-speed split', durationSeconds, sampleCount: 145, metadata: { turnBiasRadians }, constraints: [ ...finiteTracks(carrier, leftOutput, rightOutput), constantAngularVelocityConstraint({ track: carrier, radiansPerSecond: carrierOmega, tolerance: { absolute: 1e-8, relative: 1e-10 } }), differentialAverageConstraint({ leftOutputAngle: leftOutput, rightOutputAngle: rightOutput, carrierAngle: carrier, tolerance: { absolute: 1e-8, relative: 1e-10 } }), angularTravelConstraint({ track: leftOutput, expectedDeltaRadians: TAU, tolerance: { absolute: 1e-8, relative: 1e-10 } }), angularTravelConstraint({ track: rightOutput, expectedDeltaRadians: TAU, tolerance: { absolute: 1e-8, relative: 1e-10 } }), monotonicScalarConstraint({ track: leftOutput, tolerance: { absolute: 1e-8 } }), monotonicScalarConstraint({ track: rightOutput, tolerance: { absolute: 1e-8 } }) ] }; } function createCentrifugalPumpSuite(): KinematicValidationSuite { const rpm = 1200; const durationSeconds = 60 / rpm; const impellerOmega = TAU / durationSeconds; const impeller = track('pump.impeller.angle', (timeSeconds) => impellerOmega * timeSeconds, 'radians'); const flowMarker = track('pump.flow-marker.angle', (timeSeconds) => impeller.sample(timeSeconds) + Math.PI / 6, 'radians'); const pressure = track( 'pump.volute.pressure-normalized', (timeSeconds) => { const theta = impeller.sample(timeSeconds); return 0.55 + 0.16 * Math.sin(6 * theta + 0.3) + 0.05 * Math.sin(12 * theta); }, 'unitless' ); const flow = track( 'pump.discharge.flow-normalized', (timeSeconds) => { const theta = impeller.sample(timeSeconds); return 0.5 + 0.12 * Math.sin(6 * theta - 0.1); }, 'unitless' ); return { id: 'centrifugal-pump.kinematics', machineId: 'centrifugal-pump', title: 'Centrifugal pump impeller continuity and bounded pressure pulse', durationSeconds, sampleCount: 145, metadata: { rpm, bladeCount: 6 }, constraints: [ ...finiteTracks(impeller, flowMarker, pressure, flow), constantAngularVelocityConstraint({ track: impeller, radiansPerSecond: impellerOmega, tolerance: { absolute: 1e-7, relative: 1e-10 } }), angularTravelConstraint({ track: impeller, expectedDeltaRadians: TAU, tolerance: { absolute: 1e-8, relative: 1e-10 } }), phaseRatioConstraint({ driver: impeller, driven: flowMarker, ratio: 1, tolerance: { absolute: 1e-8, relative: 1e-10 } }), scalarRangeConstraint(pressure, { min: 0, max: 1 }, { tolerance: { absolute: 1e-8 } }), scalarRangeConstraint(flow, { min: 0, max: 1 }, { tolerance: { absolute: 1e-8 } }) ] }; } function createGenevaDriveSuite(): KinematicValidationSuite { const slots = 4; const durationSeconds = slots; const inputOmega = TAU; const driveStartFraction = 0.08; const driveWindowFraction = 0.34; const input = track('geneva.drive-wheel.angle', (timeSeconds) => inputOmega * timeSeconds, 'radians'); const output = track( 'geneva.star-wheel.angle', (timeSeconds) => genevaOutputAngle(input.sample(timeSeconds) / TAU, { slots, driveStartFraction, driveWindowFraction }), 'radians' ); return { id: 'geneva-drive.kinematics', machineId: 'geneva-drive', title: 'Geneva drive dwell and indexing profile', durationSeconds, sampleCount: 241, metadata: { slots, driveStartFraction, driveWindowFraction }, constraints: [ ...finiteTracks(input, output), constantAngularVelocityConstraint({ track: input, radiansPerSecond: inputOmega, tolerance: { absolute: 1e-8, relative: 1e-10 } }), genevaIndexConstraint({ inputAngle: input, outputAngle: output, slots, driveStartFraction, driveWindowFraction, tolerance: { absolute: 1e-8, relative: 1e-10 } }), angularTravelConstraint({ track: output, expectedDeltaRadians: TAU, tolerance: { absolute: 1e-8, relative: 1e-10 } }), monotonicScalarConstraint({ track: output, tolerance: { absolute: 1e-8 } }) ] }; } function createBallBearingSuite(): KinematicValidationSuite { const durationSeconds = 1; const innerOmega = TAU * 2; const ballDiameterMeters = 0.012; const pitchDiameterMeters = 0.065; const cageRatio = bearingCageRatio({ ballDiameterMeters, pitchDiameterMeters }); const ballSpinRatio = bearingBallSpinRatio({ ballDiameterMeters, pitchDiameterMeters }); const inner = track('bearing.inner-race.angle', (timeSeconds) => innerOmega * timeSeconds, 'radians'); const outer = track('bearing.outer-race.angle', () => 0, 'radians'); const cage = track('bearing.cage.angle', (timeSeconds) => cageRatio * inner.sample(timeSeconds), 'radians'); const ballSpin = track('bearing.ball.spin', (timeSeconds) => -ballSpinRatio * inner.sample(timeSeconds), 'radians'); return { id: 'ball-bearing.kinematics', machineId: 'ball-bearing', title: 'Ball bearing cage and ball-spin rolling relation', durationSeconds, sampleCount: 145, metadata: { ballDiameterMeters, pitchDiameterMeters, cageRatio, ballSpinRatio }, constraints: [ ...finiteTracks(inner, outer, cage, ballSpin), constantAngularVelocityConstraint({ track: inner, radiansPerSecond: innerOmega, tolerance: { absolute: 1e-8, relative: 1e-10 } }), angularTravelConstraint({ track: inner, expectedDeltaRadians: TAU * 2, tolerance: { absolute: 1e-8, relative: 1e-10 } }), angularTravelConstraint({ track: outer, expectedDeltaRadians: 0, tolerance: { absolute: 1e-8, relative: 1e-10 } }), phaseRatioConstraint({ driver: inner, driven: cage, ratio: cageRatio, tolerance: { absolute: 1e-8, relative: 1e-10 } }), phaseRatioConstraint({ driver: inner, driven: ballSpin, ratio: -ballSpinRatio, tolerance: { absolute: 1e-8, relative: 1e-10 } }) ] }; } function createDiscBrakeCaliperSuite(): KinematicValidationSuite { const durationSeconds = 2; const freeRotorOmega = TAU * 2.4; const brakingSlowdown = 0.58; const halfPadGapMeters = 0.014; const padTravelMeters = 0.01; const clampEnvelope = track( 'brake.caliper.clamp-normalized', (timeSeconds) => brakeClampEnvelope(timeSeconds / durationSeconds), 'unitless' ); const rotorSpeed = track( 'brake.rotor.angular-speed', (timeSeconds) => freeRotorOmega * (1 - brakingSlowdown * clampEnvelope.sample(timeSeconds)), 'radians/second' ); const rotorAngle = track( 'brake.rotor.angle', (timeSeconds) => { const phase = timeSeconds / durationSeconds; return freeRotorOmega * durationSeconds * (phase - brakingSlowdown * integrateBrakeClampEnvelope(phase)); }, 'radians' ); const leftPad = track( 'brake.left-pad.position', (timeSeconds) => -halfPadGapMeters + padTravelMeters * clampEnvelope.sample(timeSeconds), 'meters' ); const rightPad = track( 'brake.right-pad.position', (timeSeconds) => halfPadGapMeters - padTravelMeters * clampEnvelope.sample(timeSeconds), 'meters' ); return { id: 'disc-brake-caliper.kinematics', machineId: 'disc-brake-caliper', title: 'Disc brake caliper opposed-piston symmetry and rotor slowdown', durationSeconds, sampleCount: 181, metadata: { freeRotorOmega, brakingSlowdown, halfPadGapMeters, padTravelMeters }, constraints: [ ...finiteTracks(clampEnvelope, rotorSpeed, rotorAngle, leftPad, rightPad), scalarRangeConstraint(clampEnvelope, { min: 0, max: 1 }, { tolerance: { absolute: 1e-8 } }), scalarRangeConstraint( rotorSpeed, { min: freeRotorOmega * (1 - brakingSlowdown), max: freeRotorOmega }, { tolerance: { absolute: 1e-8 } } ), scalarRangeConstraint( leftPad, { min: -halfPadGapMeters, max: -halfPadGapMeters + padTravelMeters }, { tolerance: { absolute: 1e-8 } } ), scalarRangeConstraint( rightPad, { min: halfPadGapMeters - padTravelMeters, max: halfPadGapMeters }, { tolerance: { absolute: 1e-8 } } ), symmetryConstraint({ a: leftPad, b: rightPad, expectedSum: 0, tolerance: { absolute: 1e-10 } }), monotonicScalarConstraint({ track: rotorAngle, tolerance: { absolute: 1e-8 } }), scalarPointConstraint( clampEnvelope, [{ atFraction: 0.5, expected: 1, tolerance: { absolute: 1e-10 }, label: 'maximum clamp load' }], { id: 'brake.peak-clamp' } ) ] }; } function createWankelRotaryEngineSuite(): KinematicValidationSuite { const durationSeconds = 1; const shaftOmega = TAU; const eccentricityMeters = 0.018; const rotorRadiusMeters = 0.075; const shaft = track('wankel.eccentric-shaft.angle', (timeSeconds) => shaftOmega * timeSeconds, 'radians'); const rotor = track('wankel.rotor.angle', (timeSeconds) => shaft.sample(timeSeconds) / 3, 'radians'); const centerX = track( 'wankel.rotor-center.x', (timeSeconds) => eccentricityMeters * Math.cos(shaft.sample(timeSeconds)), 'meters' ); const centerY = track( 'wankel.rotor-center.y', (timeSeconds) => eccentricityMeters * Math.sin(shaft.sample(timeSeconds)), 'meters' ); const apex0X = track( 'wankel.apex-0.x', (timeSeconds) => centerX.sample(timeSeconds) + rotorRadiusMeters * Math.cos(rotor.sample(timeSeconds)), 'meters' ); const apex0Y = track( 'wankel.apex-0.y', (timeSeconds) => centerY.sample(timeSeconds) + rotorRadiusMeters * Math.sin(rotor.sample(timeSeconds)), 'meters' ); return { id: 'wankel-rotary-engine.kinematics', machineId: 'wankel-rotary-engine', title: 'Wankel eccentric shaft, rotor phase, and apex seal geometry', durationSeconds, sampleCount: 181, metadata: { eccentricityMeters, rotorRadiusMeters, shaftToRotorRatio: 3 }, constraints: [ ...finiteTracks(shaft, rotor, centerX, centerY, apex0X, apex0Y), constantAngularVelocityConstraint({ track: shaft, radiansPerSecond: shaftOmega, tolerance: { absolute: 1e-8, relative: 1e-10 } }), phaseRatioConstraint({ driver: shaft, driven: rotor, ratio: 1 / 3, tolerance: { absolute: 1e-8, relative: 1e-10 } }), centerOrbitConstraint({ centerX, centerY, orbitAngle: shaft, radius: eccentricityMeters, radiusTolerance: { absolute: 1e-10, relative: 1e-10 }, angleToleranceRadians: 1e-10 }), radialDistanceConstraint({ x: apex0X, y: apex0Y, centerX, centerY, radius: rotorRadiusMeters, tolerance: { absolute: 1e-10, relative: 1e-10 } }), angularTravelConstraint({ track: shaft, expectedDeltaRadians: TAU, tolerance: { absolute: 1e-8, relative: 1e-10 } }), angularTravelConstraint({ track: rotor, expectedDeltaRadians: TAU / 3, tolerance: { absolute: 1e-8, relative: 1e-10 } }) ] }; } function track(id: string, sample: (timeSeconds: number) => number, units: string): ScalarTrack { return { id, units, sample }; } function finiteTracks(...tracks: ScalarTrack[]) { return tracks.map((scalarTrack) => finiteScalarTrackConstraint(scalarTrack)); } function rpmToRadiansPerSecond(rpm: number): number { return (rpm * TAU) / 60; } function strokeWindowLift(crankAngle: number, openAngle: number, closeAngle: number, maxLift: number): number { const cycle = 4 * Math.PI; const span = closeAngle - openAngle; const phase = positiveModulo(crankAngle - openAngle, cycle); if (phase < 0 || phase > span) { return 0; } return maxLift * Math.pow(Math.sin((Math.PI * phase) / span), 2); } function gaussianPulse(angle: number, center: number, widthRadians: number, periodRadians: number): number { const error = positiveModulo(angle - center + periodRadians / 2, periodRadians) - periodRadians / 2; return Math.exp(-0.5 * Math.pow(error / widthRadians, 2)); } function brakeClampEnvelope(phase: number): number { const p = clamp(phase, 0, 1); if (p < 0.18) { return 0; } if (p < 0.42) { return smootherstep01((p - 0.18) / 0.24); } if (p < 0.72) { return 1; } if (p < 0.94) { return 1 - smootherstep01((p - 0.72) / 0.22); } return 0; } function integrateBrakeClampEnvelope(phase: number): number { const clampedPhase = clamp(phase, 0, 1); if (clampedPhase === 0) { return 0; } const steps = 128; const step = clampedPhase / steps; let accumulated = 0; for (let index = 0; index <= steps; index += 1) { const weight = index === 0 || index === steps ? 0.5 : 1; accumulated += weight * brakeClampEnvelope(index * step); } return accumulated * step; }