The Coriolis Force Did Zero Work, Yet the Impeller Added 1800 J/kg — Energy in a Rotating Frame
Coriolis power in a rotating frame is exactly zero. Head still appears because that force is what the blade has to push against.
The man who defined work has a force that does none#
Gaspard-Gustave de Coriolis is the one who put "work" and "kinetic energy" into the form we still use. His 1829 book was titled Du calcul de l'effet des machines — on calculating the effect of machines. It was written by an engineer counting what went into a water wheel and what came out.
The force that carries his name does no work at all. Its magnitude is irrelevant. This post checks that claim on a single centrifugal impeller. The Coriolis acceleration reaches 290 g, the ledger still reads J/kg, and the fluid's total enthalpy climbs by 1800 J/kg. Where those 1800 come from — and what disappears when a rotating-frame solver drops the matching term — is the point.
1832: from water wheels to rotating coordinates#
The Korean serial Fluid Mechanics in History describes Paris in those years. The July Revolution of 1830 drove out the royalist Cauchy, and the republican Navier took his chair at the École Polytechnique. In 1832, Navier began working with Coriolis.
Coriolis had been chewing on water wheels. How do you count energy and work inside a rotating fluid machine? To do that arithmetic you have to sit on the rotating shaft. His work on rotating coordinates grew out of that, and his 1835 paper introduced what we now call the Coriolis force.
The order matters. The Coriolis force did not come from planetary rotation or meteorology. It came from trying to balance the energy ledger of a rotating machine. Play with that ledger directly in the simulation below.
Use the frame button to move the camera between the relative and the absolute view. The orange trajectory changes shape completely, while the green line on the right refuses to tilt. Push omega up and only the orange curve steepens.
A perpendicular force never reaches the ledger#
In a frame rotating at , a fluid particle with relative velocity obeys
The second term on the right is Coriolis, the third is centrifugal. Both are inertial terms, the price of spinning the coordinate system. How they enter the momentum equation was covered in rotating reference frames and MRF. Here only the energy side matters.
To get energy, dot each force with . The Coriolis term dies on the spot.
A cross product is perpendicular to both of its arguments. This is an identity — no approximation, no condition attached. It holds for any and any direction of .
The centrifugal term behaves differently. It equals , so any radial velocity keeps the dot product alive. In exchange, it has a potential.
Having a potential means it can be moved to the left side and folded into a constant.
So the conserved quantity is , not #
Integrating that equation along a streamline under steady, inviscid conditions gives rothalpy — rotational plus enthalpy.
is static enthalpy, the relative speed, and the blade speed. The minus sign on the last term is the centrifugal potential. The Coriolis term never appears at all, because its dot product was zero from the start.
Its link to the absolute total enthalpy follows from :
where is the swirl component of the absolute velocity. So can rise while stays put, and it rises by exactly — the Euler turbomachinery equation.
Three separate ledgers in Python#
Take a centrifugal impeller running from 50 mm to 150 mm in radius, with the channel height tapering from 20 mm to 8 mm. Set rad/s and 30 kg/s of water. Two backsweep angles, and . Continuity fixes the radial relative velocity, constant rothalpy fixes the pressure, and then the work done by the Coriolis, centrifugal, and blade reaction forces is integrated separately in time.
import numpy as np
OMEGA = 300.0 # angular speed [rad/s]
RHO = 1000.0 # density [kg/m^3]
MDOT = 30.0 # mass flow rate [kg/s]
R1, R2 = 0.05, 0.15 # inlet / outlet radius [m]
B1, B2 = 0.020, 0.008 # inlet / outlet channel height [m]
GRID = np.linspace(R1, R2, 4001)
def channel_state(r, beta_deg):
"""At radius r: relative components (w_r, w_t), blade speed U, absolute swirl c_t."""
b = B1 + (B2 - B1) * (r - R1) / (R2 - R1)
w_r = MDOT / (RHO * 2.0 * np.pi * r * b) # radial part set by continuity
w_t = -w_r * np.tan(np.radians(beta_deg)) # backswept blade -> against rotation
u = OMEGA * r
return w_r, w_t, u, u + w_t
def march_channel(beta_deg):
"""Hold rothalpy constant and build p/rho and absolute total enthalpy h0 along the channel."""
w_r, w_t, u, _ = channel_state(R1, beta_deg)
i_const = 0.5 * (w_r**2 + w_t**2) - 0.5 * u**2 # taking p1/rho = 0 as datum
cols = []
for r in GRID:
w_r, w_t, u, c_t = channel_state(r, beta_deg)
p = i_const - 0.5 * (w_r**2 + w_t**2) + 0.5 * u**2
cols.append((u, c_t, p + 0.5 * (w_r**2 + c_t**2),
p + 0.5 * (w_r**2 + w_t**2) - 0.5 * u**2))
return np.array(cols) # U, c_t, h0, I
def power_ledger(beta_deg):
"""Integrate Coriolis, centrifugal and blade-reaction power separately along the path."""
om = np.array([0.0, 0.0, OMEGA])
st = np.array([channel_state(r, beta_deg) for r in GRID])
w_r, w_t = st[:, 0], st[:, 1]
dwt_dr = np.gradient(w_t, GRID)
p_cor = np.zeros_like(GRID)
p_cen = np.zeros_like(GRID)
p_bld = np.zeros_like(GRID)
for k, r in enumerate(GRID):
w = np.array([w_r[k], w_t[k], 0.0]) # local (r, theta, z) orthonormal
f_cor = -2.0 * np.cross(om, w)
f_cen = -np.cross(om, np.cross(om, np.array([r, 0.0, 0.0])))
acc_t = w_r[k] * dwt_dr[k] + w_r[k] * w_t[k] / r # cylindrical curvature term included
f_bld_t = acc_t - f_cor[1] # leftover swirl accel belongs to the blade
p_cor[k] = f_cor @ w
p_cen[k] = f_cen @ w
p_bld[k] = OMEGA * r * f_bld_t # torque x angular speed = lab-frame power
dt = 1.0 / w_r # dt = dr / w_r
ints = [np.trapezoid(p * dt, GRID) for p in (p_cor, p_cen, p_bld)]
return ints + [np.max(np.abs(p_cor))]
def transported_h0(beta_deg, with_source):
"""Integrate the h0 transport equation directly. Source term is Omega * d(r c_theta)/dr."""
st = np.array([channel_state(r, beta_deg) for r in GRID])
src = OMEGA * np.gradient(GRID * st[:, 3], GRID) if with_source else np.zeros_like(GRID)
return np.trapezoid(src, GRID)
for beta in (0.0, 30.0):
tab = march_channel(beta)
d_h0 = tab[-1, 2] - tab[0, 2]
euler = tab[-1, 0] * tab[-1, 1] - tab[0, 0] * tab[0, 1]
drift = np.max(np.abs(tab[:, 3] - tab[0, 3]))
cor, cen, bld, cor_peak = power_ledger(beta)
ok = transported_h0(beta, True)
bad = transported_h0(beta, False)
print(f"beta = {beta:4.1f} deg U1 = {tab[0,0]:4.1f} m/s U2 = {tab[-1,0]:4.1f} m/s")
print(f" delta h0 from rothalpy = {d_h0:9.2f} J/kg")
print(f" U*c_theta (Euler) = {euler:9.2f} J/kg gap {abs(d_h0-euler):.1e}")
print(f" rothalpy max drift = {drift:9.1e} J/kg")
print(f" work by Coriolis = {cor:9.1e} J/kg (peak power {cor_peak:.1e} W/kg)")
print(f" work by centrifugal = {cen:9.2f} J/kg")
print(f" work by blade torque = {bld:9.2f} J/kg")
print(f" h0 transport, source = {ok:9.2f} J/kg head {ok/9.81:5.1f} m")
print(f" h0 transport, dropped = {bad:9.2f} J/kg head {bad/9.81:5.1f} m")beta = 0.0 deg U1 = 15.0 m/s U2 = 45.0 m/s
delta h0 from rothalpy = 1800.00 J/kg
U*c_theta (Euler) = 1800.00 J/kg gap 0.0e+00
rothalpy max drift = 1.1e-13 J/kg
work by Coriolis = 0.0e+00 J/kg (peak power 0.0e+00 W/kg)
work by centrifugal = 900.00 J/kg
work by blade torque = 1800.00 J/kg
h0 transport, source = 1800.00 J/kg head 183.5 m
h0 transport, dropped = 0.00 J/kg head 0.0 m
beta = 30.0 deg U1 = 15.0 m/s U2 = 45.0 m/s
delta h0 from rothalpy = 1737.98 J/kg
U*c_theta (Euler) = 1737.98 J/kg gap 0.0e+00
rothalpy max drift = 7.1e-14 J/kg
work by Coriolis = 2.2e-16 J/kg (peak power 1.4e-12 W/kg)
work by centrifugal = 900.00 J/kg
work by blade torque = 1737.98 J/kg
h0 transport, source = 1737.98 J/kg head 177.2 m
h0 transport, dropped = 0.00 J/kg head 0.0 mThe rothalpy drift is J/kg, which is double-precision round-off. The Coriolis account reads exactly 0.0 at zero backsweep and at 30°. The second number is floating-point residue, not physics.
900 and 1800 — who paid the other half#
Two numbers stand out. The centrifugal force did 900 J/kg of work, yet total enthalpy rose by 1800 J/kg. Exactly twice.
Those 900 are money circulating inside the relative frame only. They split into pressure and relative kinetic energy. That is a different account from the 1800 the fluid actually received in the lab frame.
The blade paid the other 900. On a radial blade the relative velocity has no swirl component. Keeping it that way requires something to cancel the Coriolis force exactly, and that something is the blade's pressure side. By reaction the fluid feels a force of equal magnitude, whose torque is .
Here is the split. In the relative frame that force is perpendicular to , so it does no work. In the absolute frame the blade is turning at , so torque times angular speed is power. Integrate and you get J/kg — the work by blade torque line in the output.
Put plainly: the Coriolis force hands the fluid nothing. It makes the fluid push on the blade, and shaft work enters through that reaction path. The force picks the direction; the shaft pays the bill.
The orange Coriolis arrow is the longest one on screen, and its account stays pinned at zero. Drag the backsweep slider and only the purple blade account changes. Raise omega and the orange bar still refuses to grow.
When a rotating-frame solver transports as is#
That was the physics. In code, the accident happens in a predictable place.
It is about which variable you transport in the energy equation over a rotating zone. What is carried by the relative velocity and conserved without a source is . If you want to carry on the relative velocity, a source term has to come with it.
with the streamwise coordinate. Drop that term and you get the last two lines of the output. With the source: 1800 J/kg, a head of 183.5 m. Without it: 0.00 J/kg, a head of 0 m. The fluid crosses the impeller and nothing happens to it.
The symptom is confusing because the residuals look fine. The equation converges nicely — it just converges to the answer with zero head. Refining the grid does not help; zero stays zero.
In OpenFOAM-family solvers, this shape appears when the total-energy definition on the rhothermo side and the MRF correction fall out of step. As a missing term in a coordinate transformation, it shares a root with the mistake in curvature terms in polar FVM. That is also why the acc_t line above carries the curvature term . Remove it and the blade ledger at 30° backsweep drifts to 1802.06.
Three questions to ask when you meet a rotating zone#
First, in this case, which one is constant — or ? Inside the rotating zone it is . Cross into the stationary zone and it is again. Check what is being matched at the interface.
Second, is the source term in the energy equation actually wired up? If the head is oddly low, or exactly zero, look there first.
Third, are you "adding" the Coriolis term as an energy source? Its power is identically zero. If it feels like something belongs there, the thing you need is not Coriolis but .
Coriolis arrived at rotating coordinates while counting the efficiency of water wheels. In the ledger he invented, the force bearing his name always reads zero. Keeping that zero at zero when we compute rotating machinery is how we inherit his bookkeeping, 190 years on.
Related
Share if you found it helpful.