Concepts Primer
Top-level map: Field Guide
A standalone reference for the signal-processing and constrained-dynamics concepts that the slider-crank / multi-cylinder chapters lean on. Each section is short, concrete, and grounded in the code and numbers we’ve actually produced in this project — not a textbook retread.
Intended audience: a mechanical engineer who’s comfortable with rigid-body mechanics but may want a quick refresher on the maths side before (or after) reading the analysis chapters.
Jump table:
- FFT (Fast Fourier Transform)
- Phasors and harmonics (incl. 2.1 2D phasors · 2.2 Firing-cycle phasors)
- Vibration spectrum
- DC component
- Saddle-point system
- Signal processing
- Transmissibility
- Transfer function vs FRF (and Nyquist)
- LMS adaptive cancellation
- Glossary of symbols used in the series
1. FFT (Fast Fourier Transform)
What it is. An algorithm that takes a time-domain signal (a list of
numbers sampled at uniform intervals) and returns the complex
amplitudes of each sinusoidal frequency that adds up to it. The
mathematical object is the discrete Fourier transform (DFT); the “F”
in FFT just means the clever O(N log N) algorithm for computing it.
Input: sampled at N points with spacing . Output: N complex numbers where each is the amplitude + phase of the sinusoidal component at frequency .
Reconstruction (inverse):
For real-valued signals (everything we deal with), and
are complex conjugates — they encode the same physical
information — so numpy provides rfft which returns only the first
values.
Where we use it. In fft_amplitude() in
_common.py:
def fft_amplitude(signal, N):
"""One-sided amplitude spectrum: |rfft(x)| * 2/N with DC un-doubled."""
raw = np.abs(rfft(signal))
amp = raw * 2.0 / N
amp[0] *= 0.5
return amp
Three things worth knowing about this formula:
- The
2/Nscaling converts the raw FFT output into peak amplitudes of the sinusoidal components. If , thenfft_amplitude(x)[k]at the bin corresponding to . Without this scaling, you’d get instead — correct but unphysical. - DC is un-doubled (
amp[0] *= 0.5) because a DC component isn’t a “cosine peak” that’s spread across positive and negative frequencies — it’s just itself. It should read as the constant offset, not twice the offset. - Sampling must be exact. In our scripts we use
n_samples = 1024overn_rev = 10full crank revolutions withendpoint=False. Theendpoint=Falsemakes the sampled grid perfectly periodic with period , so harmonics of fall on exact FFT bins (). No spectral leakage, no windowing needed. If sampling isn’t exact, harmonics smear across neighbouring bins and you’d need a Hanning or similar window to fix it.
Concrete example. Feed the single-cylinder driving torque
into fft_amplitude() and read bin :
That’s the 2nd-harmonic amplitude — the “secondary” component of the driving torque.
This is the number that we then multiply by a phasor-sum factor when building multi-cylinder engines.
2. Phasors and harmonics
Harmonics. A periodic signal with fundamental frequency can always be written as a sum of sinusoids at (the harmonics of the fundamental, also called 1×, 2×, 3× in shorthand).
For our engine running at 10 RPM, the fundamental is Hz. Every vibration signal the engine produces can be expanded as
Each term has two numbers attached: an amplitude (how strong it is) and a phase (when its peak occurs within the cycle).
Phasors. Packaging those two numbers into a single complex number is the phasor trick:
Using Euler’s formula , this gets us . The phasor absorbs the amplitude and phase; the rotates at the harmonic’s angular speed.
Why phasors are amazing for multi-cylinder engines. Each cylinder in a multi-cylinder engine contributes the same waveform but shifted in time by its crank-phase offset . In phasor land, a time shift of turns into a multiplication by — so summing across cylinders at harmonic is
The spatial/timing geometry of the engine condenses into a single complex number per harmonic: . Magnitude zero = cancellation, magnitude 4 = four-fold reinforcement. No trigonometry required.
This is exactly what phasor_sum()
computes:
def phasor_sum(phases_rad, harmonic, signs=None):
"""sum_i s_i · exp(j·n·phi_i) — the complex multiplier for harmonic n."""
...
And its moment counterpart phasor_moment_sum() adds a position
weight for rocking-couple analysis.
Concrete example: the I4 at 1× vs 2×. With kinematic phases :
- At 1×: . Cancellation.
- At 2×: . 4× reinforcement.
Two additions, four complex numbers — and that’s the entire story of I4 primary balance and secondary imbalance.
2.1 2D phasors (bank-angle extension)
The basic phasor sum collapses cylinders that all push along the same world axis — fine for inline and flat layouts where the bore axes are parallel. V-engines have banks at different angles, so each cylinder’s force is a 2D vector with a different direction per cylinder. The generalisation introduces a bank angle (the orientation of cylinder i’s bore axis, measured from world +x):
Two independent phasor sums per harmonic, one per world axis. The
scalar-sign case is recovered when all (then
and ). Implemented in phasor_sum_2d()
and phasor_moment_sum_2d() in
_common.py,
used from the V-engines chapter onward.
Why this is still 2D in the simulator’s sense: every lies in the simulator’s working plane (the (x, y) cross- section perpendicular to the crankshaft). The framework never needs to leave that plane — see the Dimensional Reduction reference chapter for why this is exact under the rigid-crankshaft / identical-cylinder hypotheses.
2.2 Firing-cycle phasors (720° vs 360°)
All the previous phasor sums use the inertial cycle of 360° (one crank revolution). For combustion analysis the natural cycle is 720° (one full four-stroke cycle: intake-compression- power-exhaust). The phasor structure is identical, but with three re-interpretations:
| Inertial (360°) | Combustion (720°) |
|---|---|
| crank phase (mod 360°) | firing angle (mod 720°) |
| harmonic index of crank | harmonic index of 720° cycle of crank |
| per-cyl source | per-cyl pulse |
| DC, 1×, 2×, 3×, … | DC, ½×, 1×, 1½×, 2×, … of crank |
The new feature is half-integer crank harmonics (½×, 1½×, 2½×,
…) that the inertial analysis cannot see — they exist because the
forcing function repeats only every two revolutions. The same
phasor_sum_2d() helper handles both: just feed firing angles
scaled to over 720°. See the
combustion chapter
for the worked example.
3. Vibration spectrum
The spectrum of a signal is the set of amplitudes (or complex phasors) of all its harmonic components, plotted or tabulated against frequency. It’s the frequency-domain picture of what the time-domain picture is doing.
For a periodic signal sampled over an integer number of periods, the
spectrum is discrete — amplitude only at the harmonic frequencies,
zero between them. That’s why we plot it as a bar chart
(vibration_slider_crank_spectrum.png, boxer4_spectrum.png, etc.)
rather than as a continuous curve.
Why it matters. Every mechanical structure has its own resonances — natural frequencies where even a small forcing input produces a large response.
If the engine’s vibration spectrum has significant energy at the same frequency as a vehicle chassis mode or a cabin panel mode, you get an audible hum or a felt vibration disproportionate to the input amplitude.
Understanding which harmonics carry energy is therefore the prerequisite for everything downstream: mount design, balance-shaft sizing, chassis stiffness tuning.
A designer can’t tell you “we need to isolate 12 Hz” without first having the spectrum and knowing that 12 Hz is where the engine’s 2× at idle sits.
Concrete example. Our I4 at 10 RPM:
| Harmonic | [N·m] | [N] | [N] |
|---|---|---|---|
| DC | 0.00 | 0.00 | 39.24 |
| 1× | 0.03 | 0.24 | 2.22 |
| 2× | 17.44 | 16.44 | 2.06 |
| 3× | 0.24 | 0.00 | 1.07 |
| 4× | 1.90 | 1.18 | 0.07 |
The engine’s entire vibration signature lives in this table.
Every downstream engineering decision — motor sizing, bearing loads, mount compliance, balance-shaft mass — is a reaction to a specific cell in a table like this, at a specific operating RPM.
4. DC component
DC stands for “direct current” — a term from electrical engineering that’s been co-opted into signal processing to mean the zero-frequency (constant) component of a signal. Despite the name, it has nothing to do with electricity in our context.
Mathematically, the DC component is the time average over one period:
In the FFT it’s the bin (frequency = 0).
What DC means in our engine analysis:
-
DC of = the average driving torque over the cycle. For a dynamic system with only inertial reactions (gravity off), this should be zero — if it weren’t, the engine would gain or lose rotational kinetic energy every cycle. We see for every engine configuration, as expected.
-
DC of (horizontal bearing force) = the average sideways force on the main bearing. Always zero in our setups, because no horizontal body force exists in a gravity-off inline layout.
-
DC of (vertical bearing force) = the static weight the bearing must support. This is the only column where DC is non-zero in a gravity-on single-cylinder run: . The DC literally reports the static-statics sanity-check of the model.
Why DC is important enough to call out separately: engine NVH engineers generally don’t care about DC (it’s a static load, not a vibration), but they do care that DC is correct, because a wrong DC is always a sign that the mass matrix or gravity vector is wrong. “Does my simulated DC match the static weight hand-calc?” is the single cheapest sanity check you can do on a multibody model.
When we subtract DC vs keep it:
- For vibration analysis (spectra, harmonic amplitudes): we treat DC as informational only and focus on the AC content.
- For force/load analysis (sizing bearings, mounts): DC matters because it contributes to the total load on the hardware.
In the plots in this series, DC is shown as the “0×” bar on the bar charts, usually smaller than or comparable to 1×. The interesting physics is almost always in the non-DC harmonics.
5. Saddle-point system
A specific flavour of linear system that pops up anywhere you have constrained optimisation or, equivalently, constrained dynamics. It looks like this:
- is a square matrix (positive semi-definite in our case).
- is a rectangular matrix (the constraints).
- The zero in the lower-right block is the defining feature.
- is the “primary” unknown; is the Lagrange multipliers.
The “saddle point” name comes from the fact that if you rewrite the problem as finding a stationary point of the Lagrangian , the solution is a minimum in and a maximum in — i.e. a saddle of .
Where this shows up in our multibody solver. The constrained equations of motion are
Rearranged into a single block:
- is the mass matrix (from body masses and inertias). Positive semi-definite.
- is the constraint Jacobian. One row per constraint equation, one column per coordinate.
- is the vector of generalised accelerations — this is what we want for the dynamics.
- is the vector of Lagrange multipliers. Each one is a constraint reaction force — e.g. the pair at a revolute joint, or the minus-driving-torque at a user constraint.
- is the sum of applied generalised forces (gravity, spring, contact, user).
- is the constraint’s acceleration- level right-hand side.
Solving this system IS the multibody dynamics step. Every call to
solve_dynamics_scipy() invokes the saddle-point solve internally at
every RHS evaluation. Every call to solve_inverse_dynamics() does
the same. The function that actually does it is
solve_acceleration_with_lagrange() in
2D-Kinematics/solver.py, which just
assembles the block matrix and calls np.linalg.solve.
Why this framework is powerful. The same saddle-point system handles:
- Forward dynamics — given forces, solve for accelerations.
- Inverse dynamics — given motion (via user constraints making DOF = 0), solve for the Lagrange multipliers that enforce it. Those multipliers include the driving torque and bearing reactions we’ve been reporting.
- Constrained optimisation — same math, different interpretation (accelerations become design variables, multipliers become shadow prices).
Connection to vibration analysis. Each call to the saddle-point solver gives us one time-step’s worth of . Collecting over time and running it through the FFT produces the vibration spectrum we’ve been plotting. The saddle-point solver is the source that feeds the FFT pipeline.
6. Signal processing (in this project)
“Signal processing” is an enormous discipline; we use a small, well-chosen subset.
What we use
-
Sampling — converting a continuous-time function into a finite list of values by picking evaluations at uniformly spaced times . Our inverse-dynamics solver samples at the
tspanwe hand it. -
FFT — decomposing the sampled signal into harmonic amplitudes and phases. Covered in §1.
-
Phasor arithmetic — complex-number algebra on the Fourier components. Covered in §2. This is how we superpose cylinders at different phases without ever re-running a simulation.
-
Circular time shift —
np.roll(signal, -k)shifts a periodic signal forward in time by samples. We use this to simulate the effect of each cylinder’s crank-phase offset in the time domain, without recomputing its trajectory. Mathematically equivalent to multiplying the FFT by . -
Peak-amplitude normalisation — the
2/Nscaling infft_amplitude()so the FFT output reads as physical force amplitudes in N, not abstract FFT units.
What we carefully avoid (and why)
-
Windowing — a common FFT trick where you multiply the signal by a smooth taper (Hanning, Hamming, Blackman) before the FFT to reduce “spectral leakage” when the sample doesn’t cover exact integer periods. We don’t need it because our sampling is deliberately exact (
endpoint=Falseover integer revolutions), so every harmonic lands on an exact bin. If we used arbitrary sampling lengths, we’d need windowing and the spectrum would have small smearing artefacts. -
Anti-aliasing / low-pass filters — matter only if the signal has content above the Nyquist frequency (). Our single-cylinder content decays as beyond the 2nd harmonic, and our sample rate is more than enough, so nothing above Nyquist exists to alias.
-
Convolution / digital filters — we don’t need to apply any transfer function to the signals; we’re measuring the source. When the project grows into chassis response, we’ll need them.
A mental model: time ↔ frequency
For any periodic signal, these three views contain the same information:
- Time-domain samples: . A list of numbers. Plotted as a wiggly curve.
- Frequency-domain phasors: . A list of complex numbers, one per harmonic. Plotted as a bar chart (amplitudes) with optional phase annotations.
- Fourier-series coefficients: . The “humanly readable” form of (2).
All three are isomorphic — you can convert between them without loss of information. Which one you pick depends on what question you’re asking:
| Question | Best view |
|---|---|
| What is the force right now at crank angle 90°? | Time domain |
| What is the peak shake force over the cycle? | Time domain |
| What frequency is the engine humming at? | Frequency domain |
| How does vibration change if I add a balance shaft? | Frequency domain |
| How do 4 cylinders superpose to cancel at 1×? | Phasor (frequency domain) |
| What does the time history look like if I shift phase by 180°? | Back to time domain for plotting, frequency domain for shifting |
We use all three. The FFT is just the bridge between views.
7. Transmissibility
What it is. The dimensionless ratio of the force a mount transmits to the chassis to the force the engine applies to the mount, as a function of frequency. The textbook 1-DOF result for a mass-spring-damper:
with three parameters:
| Symbol | Meaning |
|---|---|
| frequency ratio — excitation frequency / mount natural frequency | |
| natural frequency — set by mount stiffness and engine mass | |
| damping ratio — set by the mount’s loss factor |
Three operating regions:
| Interpretation | ||
|---|---|---|
| quasi-static; the mount just transmits | ||
| up to several | resonance — amplification, not isolation | |
| exactly 1 | crossover | |
| falls as | isolation regime — the design target |
Three-DOF generalisation. A real engine block on N mounts is not a 1-DOF system — it has three planar DOF and multiple mount points. Replace the scalars with 3×3 matrices and solve in the frequency domain:
Same shape of curve per excitation axis, with cross-coupling between translation and rotation through mount geometry (an off-CG mount couples to , and so on).
Where this lives in the project. Implemented in
engine_mount_transmissibility.py
and used in the
engine mounts chapter
to demonstrate the soft-vs-stiff mount tradeoff numerically.
Connection to phasors. The phasor framework produces source phasors ; transmissibility filters them into chassis phasors . Same complex- number language, one extra multiplier per harmonic.
8. Transfer function vs FRF (and where Nyquist comes in)
Same math, two engineering vocabularies. The distinction matters because the e-book uses both — transmissibility and cabin transfer function are FRFs evaluated analytically, while LMS stability is a control-systems Nyquist-style argument.
8.1 The mathematical relationship
In Laplace-transform language, the transfer function of a linear system is the ratio of output to input in the complex -plane:
The Frequency Response Function (FRF) is restricted to the imaginary axis — i.e., assuming the transient parts have died out and we’re observing only the steady-state sinusoidal response:
is the “master equation” (transients + steady state, stability
- response). is the slice that tells you how the system vibrates at each steady-state frequency.
8.2 Two engineering cultures
Mechanical / NVH engineers and control engineers grew up with the same math but different vocabularies. The terminology in practice:
| FRF (mechanical / NVH) | Transfer function (control) | |
|---|---|---|
| Domain | only (steady state) | full -plane (transient + steady) |
| Typical origin | experimental — hammer test, shaker, dyno | analytical — derived from differential equations |
| What you study | resonances, modal damping, frequency content | stability, gain margin, transient response |
| Common plot | Bode (magnitude / phase vs frequency) | Bode, Nyquist, root locus |
Slight nuance worth being honest about: “FRF = experimental” is the industry default (NVH engineers measure them in the lab) but not a rule. Our and are FRFs derived analytically from linear models — same mathematical object as a measured FRF, just a different origin. Conversely, control engineers do measure transfer functions experimentally too (system identification).
8.3 Where the e-book uses each
| Place | Object | Why this name |
|---|---|---|
| §7 Transmissibility | mount-side FRF | steady-state harmonic source |
| Engine Mounts chapter | 3-DOF block FRF | steady-state, evaluated per harmonic |
| Chassis Response chapter | cabin SDOF FRF | analytical, steady-state |
| §9 LMS closed-loop stability | open-loop transfer function | feedback loop — stability matters, transients matter |
The first three live happily in FRF land. The moment you close a feedback loop (LMS, FXLMS, active mounts), you’re in control-systems territory and need the full -plane view to talk about stability.
8.4 Where Nyquist comes in
Nyquist’s stability criterion answers: will this closed-loop system blow up?
A unity-feedback closed-loop system is stable iff the polar plot of the open-loop FRF doesn’t encircle the point in the complex plane.
In the Active Damping chapter we showed that actuator delay drives naive LMS unstable above ~2.5 ms. That is a Nyquist-family failure: delay introduces a per-cycle phase shift that pushes the open-loop polar plot around the critical point, and the closed-loop “canceller” turns into a closed-loop “amplifier”.
Honest nuance: the analysis we actually used in the chapter was the simpler statement “the LMS gradient direction rotates by radians per update; above the step points uphill”. That is the same family of phase-margin reasoning that Nyquist captures rigorously, just expressed without drawing the polar plot. Filtered-X LMS, the standard fix, can be derived either way: pre-filter the reference through the actuator response model, and the open-loop polar plot stays clear of the critical point regardless of delay.
8.5 Three-line summary
- Transfer function : full -plane, captures transient + steady state.
- FRF : -axis slice, captures steady-state vibration.
- Nyquist: the test you apply to (or its open-loop FRF) to make sure your active controller doesn’t turn the engine into a self-oscillating mechanical disaster.
Same underlying maths, three lenses. The e-book uses the first passively (linear analysis, no feedback), the second pervasively (transmissibility, mount + cabin response), and the third implicitly in the LMS-delay story.
9. LMS adaptive cancellation
What it is. The Least-Mean-Squares (Widrow & Hoff, 1960) algorithm is a gradient-descent law for finding the linear combination of reference signals that best cancels a target signal in the mean-square sense. In our context: closed-loop active vibration cancellation at a single harmonic.
The setup. Pick a cancellation harmonic and grab two reference signals from the crank-angle sensor:
Define the actuator output as
The weights are a single phasor in disguise: amplitude , phase . They are updated continuously to drive the residual
toward zero by gradient descent on :
The single tunable parameter (the step size) sets the trade between convergence speed and steady-state ripple.
Time constant. With unit-amplitude references, and the expected weight update is
giving a first-order exponential approach to the steady-state weight , with time constant
At and that’s — convergence to within 1 % takes .
Stability under actuator delay. Naive LMS uses the current reference in its update law. If the actuator has delay samples, the gradient direction rotates by radians per cycle. Above of rotation the update step starts pointing uphill and the system becomes unstable:
At (100 Hz) and that’s . Filtered-X LMS (FXLMS) extends the stable range by pre-filtering the references through an identified model of the actuator response, handling arbitrary delays. Three lines of code change.
Where this lives in the project. Implemented as the
ActiveDamper class in
active_mass_damping.py,
with four demos (steady-state convergence, throttle step, RPM
step, delay-induced instability) covered in the
active damping chapter.
Connection to balance shafts. A passive Lanchester balancer is the open-loop limit of this scheme: weights are designed once and frozen, with no feedback. Active damping replaces the design-time decision with a continuously-adapted one, which is what lets it track load, RPM, manufacturing tolerances, and cylinder deactivation — none of which a fixed mechanical balance shaft can do.
10. Glossary of symbols used in the series
| Symbol | Meaning |
|---|---|
| Generalised coordinates of the mechanism (positions, angles) | |
| Generalised velocities () | |
| Generalised accelerations () | |
| Mass matrix (position-dependent) | |
| Constraint equations ( on the manifold) | |
| Constraint Jacobian () | |
| Lagrange multipliers = constraint reaction forces | |
| Total generalised applied force (gravity + springs + contact + user) | |
| Constraint acceleration RHS () | |
| Bearing reaction or driving torque time series | |
| th harmonic amplitude of from FFT | |
| Crank angular velocity (rad/s) | |
| Crank frequency in Hz () | |
| Shorthand for “nth harmonic of crank frequency” | |
| Kinematic phase of cylinder on the crankshaft (radians) | |
| sign flip on cylinder for mirror-opposed banks (boxer, flat) | |
| Position of cylinder along the crankshaft axis (metres) | |
| Crank throw length | |
| Connecting rod length | |
| Crank-to-rod ratio; controls secondary imbalance strength | |
| DC | Zero-frequency / time-average component |
| Driving torque at the crank (N·m) | |
| Crankshaft bearing reaction forces in world x, y (N) | |
| TDC / BDC | Top dead centre / bottom dead centre (slider extremes) |
| primary / secondary | Engine-NVH shorthand for 1× / 2× harmonic shake |
| Crankpin offset in a boxer engine (metres) | |
| 2D phasors / V-engines | (added in §2.1) |
| Bank angle of cylinder (bore-axis direction from world +x) | |
| Per-harmonic 2D force phasors (world x, world y) | |
| Per-harmonic 3D moment phasors (about y, x axes) | |
| Per-harmonic torque ripple about the crankshaft axis | |
| Combustion | (added in §2.2) |
| Firing angle of cylinder in the 720° four-stroke cycle | |
| Per-cylinder combustion pressure-pulse force | |
| -th harmonic of the pulse over the 720° cycle | |
| , duty | Pulse amplitude (N) and active fraction of the 720° cycle |
| Balance shafts | |
| Balance-shaft spin frequency as integer multiple of crank | |
| Balance-shaft mass × eccentricity (kg·m) | |
| Engine mounts and transmissibility | (added in §7) |
| Engine-block mass and in-plane inertia | |
| Per-mount translational stiffness in N/m | |
| Per-mount translational damping in N·s/m | |
| Mount natural frequency in rad/s and Hz | |
| Damping ratio (dimensionless) | |
| Frequency ratio (dimensionless) | |
| Force transmissibility ratio | |
| LMS / active damping | (added in §9) |
| Cosine / sine reference signals at harmonic | |
| LMS weight pair (cosine and sine components) | |
| LMS step size (gain) | |
| Residual / error signal in a closed-loop canceller | |
| Actuator delay in samples | |
| LMS convergence time constant in samples | |
| Chassis modal response & perception | (chassis-response chapter) |
| Effective modal mass / natural frequency / damping ratio of a body-shell mode | |
| Transfer function from chassis-side force to cabin acceleration (m/s²/N) | |
| ISO 2631-1 weighting curve for vertical seated whole-body vibration | |
| Weighted r.m.s. cabin acceleration (m/s²); compared to ISO 2631 comfort thresholds |
Cross-links
These ebook chapters each exercise a subset of the concepts above. Use this primer as the dictionary when any of them seems unclear.
Multi-cylinder series
- The Slider-Crank, Three Ways — saddle-point dynamics, DC vs AC, first FFT (§§ 1, 4, 5).
- Multi-Cylinder I4 — phasor framework, first non-trivial application (§ 2).
- Boxer-4 — phasors with signs, sign-flip cancellation (§ 2).
- Rocking Couples — moment-sum extension, (F, M) outcome tables (§ 2).
- Non-boxer Flat-4 — third corner of the (F, M) classification (§ 2).
- Series Summary — master comparison, R/L sweep, FAQ recap.
- V-Engines — bank-angle generalisation, 2D phasors (§ 2.1).
- Combustion — 720° cycle, half-integer harmonics, firing-cycle phasors (§ 2.2).
- Balance Shafts — Lanchester pair, narrow-band cancellation (open loop).
- Engine Mounts — 1-DOF and 3-DOF transmissibility (§ 7).
- Active Damping — closed-loop cancellation by LMS, FXLMS for delay (§ 9).
- Chassis Modal Response — SDOF body-shell + ISO 2631 weighting; force-to-feel.
Reference companions
- The Physics of 2D Multibody Systems — rigid-body, generalised coordinates, Lagrangian mechanics.
- The Computational Machinery — how the physics maps to the Python code.
- Dimensional Reduction — why 2D simulation is exact for 3D engine-NVH problems under the rigid-crank / identical-cylinder hypotheses.