Skip to content
cfd-lab:~/en/posts/2026-08-27-coriolis-roth…● online

Translated from the author's Korean original. Read the original (한국어)

NOTE #141DAY THU 유체역학DATE 2026.08.27UPDATED 2026.10.19READ 8 min read#Rothalpy#Rotating-Frame#Coriolis#Turbomachinery#Historical

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. One centrifugal impeller is enough to rerun the ledger. The Coriolis acceleration reaches 290 g, the ledger still reads 10−1610^{-16} 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 what the numbers settle.

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. Flip the frame button from relative to absolute and only the orange path changes shape. The green line on the right stays flat.

Flip the frame switch: the orange trail changes shape completely, yet the green rothalpy line never tilts. At the current radius 50 mm, I = 0.0 J/kg against h0 = 0.0 J/kg. Across the whole channel h0 climbs by 1800.0 J/kg — exactly U2·c_theta2 − U1·c_theta1 = 1800.0. Raise omega and only the orange line responds.

The orange path bends with the camera. The green line does not. Raise omega and only the h0h_0 curve steepens.

A perpendicular force never reaches the ledger#

In a frame rotating at Ω⃗\vec{\Omega}, a fluid particle with relative velocity w⃗\vec{w} obeys

Dw⃗Dt=−∇pρ−2Ω⃗×w⃗−Ω⃗×(Ω⃗×r⃗)\frac{D\vec{w}}{Dt} = -\frac{\nabla p}{\rho} - 2\vec{\Omega}\times\vec{w} - \vec{\Omega}\times(\vec{\Omega}\times\vec{r})

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 w⃗\vec{w}. The Coriolis term dies on the spot.

(−2Ω⃗×w⃗)⋅w⃗=0(-2\vec{\Omega}\times\vec{w})\cdot\vec{w} = 0

A cross product is perpendicular to both of its arguments. This is an identity — no approximation, no condition attached. It holds for any Ω\Omega and any direction of w⃗\vec{w}.

The centrifugal term behaves differently. It equals Ω2r r^\Omega^2 r\,\hat{r}, so any radial velocity keeps the dot product alive. In exchange, it has a potential.

Ω2r r^=−∇(−U22),U=Ωr\Omega^2 r\,\hat{r} = -\nabla\left(-\frac{U^2}{2}\right), \qquad U = \Omega r

Having a potential means it can be moved to the left side and folded into a constant.

So the conserved quantity is II, not h0h_0#

Integrating that equation along a streamline under steady, inviscid conditions gives rothalpy — rotational plus enthalpy.

I=h+w22−U22=constI = h + \frac{w^2}{2} - \frac{U^2}{2} = \text{const}

hh is static enthalpy, ww the relative speed, and U=ΩrU=\Omega r 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 h0=h+c2/2h_0 = h + c^2/2 follows from c⃗=w⃗+U⃗\vec{c} = \vec{w} + \vec{U}:

h0=I+Ucθh_0 = I + U c_\theta

where cθc_\theta is the swirl component of the absolute velocity. So h0h_0 can rise while II stays put, and it rises by exactly Δ(Ucθ)\Delta(U c_\theta) — 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 Ω=300\Omega = 300 rad/s and 30 kg/s of water. Two backsweep angles, β=0°\beta = 0° and 30°30°. 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 m

Drag backsweep from 0° to 30° and only the purple blade work drops, from 1800.00 to 1737.98 J/kg.

The orange Coriolis arrow is the longest one on screen — up to 292 g — and its account still reads 0.0e+0 J/kg. Meanwhile the blade account fills to 0 of 1800 J/kg. Drag backsweep and only the purple bar moves: the perpendicular force never pays, it only decides where the blade has to push.

The orange Coriolis arrow is the longest mark on the screen, and its ledger stays at zero. Raise omega and that bar still does not grow.

The rothalpy drift is 10−1310^{-13} J/kg, which is double-precision round-off. The Coriolis account reads exactly 0.0 at zero backsweep and 2.2×10−162.2\times10^{-16} 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.

∫Ω2r wr dt=∫r1r2Ω2r dr=U22−U122\int \Omega^2 r\,w_r\,dt = \int_{r_1}^{r_2} \Omega^2 r\,dr = \frac{U_2^2 - U_1^2}{2}

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.

Inlet blade speed is 15.0 m/s and outlet speed is 45.0 m/s. Drop those into (U22−U12)/2(U_2^2 - U_1^2)/2: 45 squared is 2025, 15 squared is 225, and half the difference is 900. The printed centrifugal work, 900.00 J/kg, matches that expression to the hundredth. At zero backsweep the blade-torque line is 1800.00, exactly twice the centrifugal work. Lab-frame power is torque times Ω\Omega, so the 900 in the relative ledger is counted twice. At 30° the blade work falls to 1737.98 J/kg and the Euler term UcθU c_\theta falls to the same 1737.98. The gap prints 0.0e+00. Centrifugal work stays 900.00. Only swirl moved. wtw_t points against rotation, so exit cθc_\theta is smaller than UU, and Δ(Ucθ)\Delta(U c_\theta) sits below 1800. Rothalpy drift is 1.1e-13 J/kg at 0° and 7.1e-14 at 30°. That is II holding as a constant inside double precision, not a new loss. Head 183.5 m is 1800.00 divided by 9.81. Head 177.2 m is 1737.98 divided by the same 9.81. Kill the source and both heads print 0.0 m, backsweep or not. The 30° Coriolis residue, 2.2e-16 J/kg, cannot budge that 0.0 m. The 290 g in the opening is an acceleration, not a power. (−2Ω⃗×w⃗)⋅w⃗=0(-2\vec{\Omega}\times\vec{w})\cdot\vec{w} = 0 stays an identity at Ω=300\Omega = 300 rad/s, even when the acceleration is 290 times gravity. Thirty kilograms per second through a channel that narrows from 20 mm to 8 mm only raises wrw_r. The arrow gets longer. The work stays 0.0e+00. At 50 mm, U=Ωr=15.0U = \Omega r = 15.0 m/s. At 150 mm it is 45.0 m/s. The centrifugal potential closes on those two radii.

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 2Ωwr2\Omega w_r exactly, and that something is the blade's pressure side. By reaction the fluid feels a force of equal magnitude, whose torque is 2Ωrwr2\Omega r w_r.

Here is the split. In the relative frame that force is perpendicular to w⃗\vec{w}, so it does no work. In the absolute frame the blade is turning at Ω\Omega, so torque times angular speed is power. Integrate and you get 2×900=18002\times 900 = 1800 J/kg — the work by blade torque line in the output.

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.

When a rotating-frame solver transports h0h_0 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 II. If you want to carry h0h_0 on the relative velocity, a source term has to come with it.

w⃗⋅∇h0=Ω ∂(rcθ)∂s\vec{w}\cdot\nabla h_0 = \Omega\,\frac{\partial (r c_\theta)}{\partial s}

with ss 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. Drop one term in the coordinate change and the ledger slips. That is why the acc_t line above carries the curvature term wrwθ/rw_r w_\theta / r. Remove it and the blade ledger at 30° backsweep drifts to 1802.06. Along a streamline II is the constant and h0=I+Ucθh_0 = I + U c_\theta is not. The non-rotating cousin, where total pressure fails between streamlines, is Bernoulli's constant crossing a streamline.

A printed head of 0.0 m means h0h_0 lost its source#

Inside the rotating zone the constant is II. Cross into the stationary zone and it is h0h_0 again. Carrying h0h_0 on the relative velocity needs Ω ∂(rcθ)/∂s\Omega\,\partial(rc_\theta)/\partial s attached. Turn that source off and 1800.00 J/kg becomes 0.00, while head falls from 183.5 m to 0.0 m. Residuals look clean. They converged to zero. Refining the mesh leaves the zero where it is.

Adding Coriolis as an energy source is the wrong repair. Its power prints 0.0e+00, and at 30° it is still 2.2×10−162.2\times10^{-16} J/kg. The term that belongs there is the swirl source, not Coriolis. He reached rotating coordinates while counting water-wheel efficiency. In the ledger he built, the force with his name is always zero. A hundred and ninety years later, leaving that zero alone is what keeps the shaft work at 1800 J/kg.

Share if you found it helpful.