{ "cells": [ { "cell_type": "markdown", "id": "f61d8fbc", "metadata": {}, "source": [ "# AirfRANS finite-volume hand simulation\n", "\n", "Purpose: learn the computation, not the Python stepper or OpenFOAM object graph.\n", "\n", "This notebook reduces the AirfRANS/OpenFOAM momentum loop to the smallest useful stencil:\n", "\n", "1. one internal face shared by two cells;\n", "2. one boundary face on one of those cells.\n", "\n", "Repo grounding:\n", "\n", "- AirfRANS case scale: $U_\\infty=(93.009,6.161,0)$ m/s, $\\nu=1.56\\times10^{-5}$ m²/s, fields `U`, `p`, `phi`, `nut`, `k`, `omega`.\n", "- OpenFOAM v14 momentum predictor source: `OpenFOAM-14/applications/modules/incompressibleFluid/momentumPredictor.C`.\n", "- The real predictor assembles\n", "\n", "$$\n", "\\texttt{fvm::ddt(U)} + \\texttt{fvm::div(phi,U)} + \\texttt{momentumTransport->divDevSigma(U)}\n", "= \\texttt{fvModels().source(U)}\n", "$$\n", "\n", "then solves\n", "\n", "$$\n", "\\texttt{UEqn == -fvc::grad(p)}.\n", "$$\n", "\n", "For the hand calculation, keep one velocity component, ignore time/MRF/source/non-orthogonal details, and replace the viscous/turbulent stress term by scalar diffusion with\n", "\n", "$$\n", "\\Gamma = \\nu + \\nu_t.\n", "$$\n", "\n", "For a cell $P$, use the residual convention\n", "\n", "$$\n", "R_P =\n", "\\sum_f \\underbrace{\\phi_f U_f}_{\\text{convection leaving cell}}\n", "-\n", "\\sum_f \\underbrace{\\Gamma_f \\nabla U_f\\cdot S_f}_{\\text{diffusion leaving cell}}\n", "+\n", "\\sum_f \\underbrace{p_f S_{f,x}}_{\\text{pressure-gradient contribution}}.\n", "$$\n", "\n", "The equation wants $R_P=0$. A face loop is bookkeeping for these three numbers.\n" ] }, { "cell_type": "code", "execution_count": 1, "id": "176670ce", "metadata": { "execution": { "iopub.execute_input": "2026-07-24T06:22:08.071527Z", "iopub.status.busy": "2026-07-24T06:22:08.071431Z", "iopub.status.idle": "2026-07-24T06:22:08.076755Z", "shell.execute_reply": "2026-07-24T06:22:08.076272Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "internal face: P -> N\n", "AirfRANS anchor: U_inf=(93.009, 6.161, 0.0) m/s, nu=1.560e-05 m^2/s\n", "face flux phi_f = 91\n", "upwind U_f = 92\n", "\n", "convection owner P 8372 | neighbour N -8372\n", "diffusion owner P 0.0400312 | neighbour N -0.0400312\n", "pressure owner P 0.15 | neighbour N -0.15\n", "residual owner P 8372.19 | neighbour N -8372.19\n", "\n", "conservation check: R_P + R_N = 0\n" ] } ], "source": [ "import math\n", "\n", "# Real AirfRANS scale, kept only to anchor the toy numbers.\n", "U_INF = (93.00914503999995, 6.161356013756317, 0.0)\n", "NU = 1.56e-5\n", "\n", "# Toy two-cell state. P is the OpenFOAM owner, N is the neighbour.\n", "# Face area vector S points from P to N.\n", "U_P = 92.0 # x-velocity in owner cell P [m/s]\n", "U_N = 90.0 # x-velocity in neighbour cell N [m/s]\n", "p_P = 0.20 # incompressible pressure p/rho in P [m^2/s^2]\n", "p_N = 0.10 # incompressible pressure p/rho in N [m^2/s^2]\n", "S = 1.0 # face area vector x-component, owner-outward [m^2]\n", "d = 1.0 # owner-to-neighbour center distance [m]\n", "Gamma = NU + 2e-2 # nu + toy turbulent viscosity [m^2/s]\n", "\n", "# 1) Face flux: surfaceScalarField phi = U_f · S_f.\n", "U_for_phi = 0.5 * (U_P + U_N)\n", "phi_f = U_for_phi * S\n", "\n", "# 2) Convection: upwind momentum carried through the face.\n", "U_f = U_P if phi_f >= 0 else U_N\n", "convection_P = phi_f * U_f\n", "convection_N = -convection_P\n", "\n", "# 3) Diffusion: - Gamma * grad(U) · S.\n", "grad_U_dot_S_P = (U_N - U_P) / d * S\n", "diffusion_P = -Gamma * grad_U_dot_S_P\n", "diffusion_N = -diffusion_P\n", "\n", "# 4) Pressure-gradient contribution: p_f * S.\n", "p_f = 0.5 * (p_P + p_N)\n", "pressure_P = p_f * S\n", "pressure_N = -pressure_P\n", "\n", "# 5) Residual contribution from this single internal face.\n", "R_P_internal = convection_P + diffusion_P + pressure_P\n", "R_N_internal = convection_N + diffusion_N + pressure_N\n", "\n", "assert math.isclose(R_P_internal + R_N_internal, 0.0, abs_tol=1e-12)\n", "\n", "def row(label, owner, neighbour):\n", " print(f\"{label:<13} owner P {owner:>12.6g} | neighbour N {neighbour:>12.6g}\")\n", "\n", "print(\"internal face: P -> N\")\n", "print(f\"AirfRANS anchor: U_inf=({U_INF[0]:.3f}, {U_INF[1]:.3f}, {U_INF[2]:.1f}) m/s, nu={NU:.3e} m^2/s\")\n", "print(f\"face flux phi_f = {phi_f:.6g}\")\n", "print(f\"upwind U_f = {U_f:.6g}\")\n", "print()\n", "row(\"convection\", convection_P, convection_N)\n", "row(\"diffusion\", diffusion_P, diffusion_N)\n", "row(\"pressure\", pressure_P, pressure_N)\n", "row(\"residual\", R_P_internal, R_N_internal)\n", "print()\n", "print(f\"conservation check: R_P + R_N = {R_P_internal + R_N_internal:.6g}\")\n" ] }, { "cell_type": "code", "execution_count": 2, "id": "7325d390", "metadata": { "execution": { "iopub.execute_input": "2026-07-24T06:22:08.077905Z", "iopub.status.busy": "2026-07-24T06:22:08.077807Z", "iopub.status.idle": "2026-07-24T06:22:08.081575Z", "shell.execute_reply": "2026-07-24T06:22:08.080994Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "boundary face on P: noSlip wall\n", "patch algebra: U_b=0, p_b=p_P, phi_b=0\n", "\n", "convection owner P 0\n", "diffusion owner P 3.68287\n", "pressure owner P -0.2\n", "wall total owner P 3.48287\n", "\n", "R_P before boundary = 8372.19\n", "R_P after boundary = 8375.67\n", "R_N unchanged = -8372.19\n" ] } ], "source": [ "# Add one boundary face on the left side of owner cell P.\n", "# This mimics the airfoil wall idea: noSlip velocity, zeroGradient pressure, no normal flux.\n", "\n", "U_wall = 0.0 # noSlip\n", "p_wall = p_P # zeroGradient pressure -> boundary value equals owner value\n", "S_wall = -1.0 # owner-outward area vector points left\n", "area_wall = 1.0\n", "d_wall = 0.5 # distance from cell center to wall face\n", "phi_wall = 0.0 # impermeable wall\n", "\n", "# Convection through an impermeable wall is zero.\n", "convection_wall_P = phi_wall * U_wall\n", "\n", "# Boundary diffusion uses the patch value instead of a neighbour value.\n", "grad_U_dot_S_wall = (U_wall - U_P) / d_wall * area_wall\n", "diffusion_wall_P = -Gamma * grad_U_dot_S_wall\n", "\n", "# Pressure term uses the boundary pressure and the boundary area vector.\n", "pressure_wall_P = p_wall * S_wall\n", "\n", "R_wall_P = convection_wall_P + diffusion_wall_P + pressure_wall_P\n", "R_P_after_wall = R_P_internal + R_wall_P\n", "R_N_after_wall = R_N_internal\n", "\n", "assert R_N_after_wall == R_N_internal\n", "assert diffusion_wall_P > 0\n", "\n", "def one(label, value):\n", " print(f\"{label:<13} owner P {value:>12.6g}\")\n", "\n", "print(\"boundary face on P: noSlip wall\")\n", "print(\"patch algebra: U_b=0, p_b=p_P, phi_b=0\")\n", "print()\n", "one(\"convection\", convection_wall_P)\n", "one(\"diffusion\", diffusion_wall_P)\n", "one(\"pressure\", pressure_wall_P)\n", "one(\"wall total\", R_wall_P)\n", "print()\n", "print(f\"R_P before boundary = {R_P_internal:.6g}\")\n", "print(f\"R_P after boundary = {R_P_after_wall:.6g}\")\n", "print(f\"R_N unchanged = {R_N_after_wall:.6g}\")\n" ] }, { "cell_type": "markdown", "id": "ec2cb6d6", "metadata": {}, "source": [ "## What this teaches\n", "\n", "The OpenFOAM loop shape is now visible:\n", "\n", "| notebook arithmetic | OpenFOAM idea |\n", "|---|---|\n", "| `convection_*` | `fvm::div(phi,U)` moves momentum with face flux |\n", "| `diffusion_*` | `momentumTransport->divDevSigma(U)` / diffusion uses face area and cell spacing |\n", "| `pressure_*` | `-fvc::grad(p)` appears as face pressure times area vector |\n", "| internal equal/opposite signs | owner/neighbour addressing conserves internal face transfers |\n", "| wall-only contribution | a boundary patch replaces the missing neighbour with boundary-condition algebra |\n", "\n", "This is not yet SIMPLE, turbulence, pressure correction, or a real airfoil mesh. It is the smallest executable picture of what every finite-volume face loop contributes to the residual.\n", "\n", "Good use: change one value at a time — `U_N`, `p_N`, `Gamma`, `U_wall`, or `phi_wall` — predict the sign of the residual change, then rerun.\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.12" } }, "nbformat": 4, "nbformat_minor": 5 }