Skip to content
cfd-lab:~/en/posts/2026-08-27-coriolis-roth…online
NOTE #142DAY THU 유체역학DATE 2026.08.27READ 7 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. This post checks that claim on a single centrifugal impeller. The Coriolis acceleration reaches 290 g, the ledger still reads 101610^{-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 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.

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.

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 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

DwDt=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 Ω2rr^\Omega^2 r\,\hat{r}, so any radial velocity keeps the dot product alive. In exchange, it has a potential.

Ω2rr^=(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+w22U22=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

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

Ω2rwrdt=r1r2Ω2rdr=U22U122\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.

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.

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 — 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 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 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.

wh0=Ω(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. 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 wrwθ/rw_r w_\theta / r. 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 — h0h_0 or II? Inside the rotating zone it is II. Cross into the stationary zone and it is h0h_0 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 Ω(rcθ)/s\Omega\,\partial(rc_\theta)/\partial s.

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.

Share if you found it helpful.