Tuesday, December 2, 2025

Potential flow simulator

The theory of potential flows is something that I got to learn in the course CIVE 6370 - Environmental Fluid Mechanics and Turbulence - that I had taken this semester, which is the second part of my first year in this PhD program. Fourier transforms and Finite Difference Methods were two other really interesting topics for me in the other course, CIVE 7397 -  Applied Mathematics for Engineers, this very semester as well but I didn't make anything with a GUI attached to the project work for it (it's just the solution to the Advection-Diffusion Equation using two Finite Difference Schemes implemented in Python - pretty standard stuff). For the former though, I thought it deserved a write-up of its own.

This is a JavaScript web application that allows a user to add in, modify and remove any number of flow elements and simulate the resulting flow field. Currently the available flow elements includes uniform flow, source/sink and free vortex flows. The supported output quantities are the velocity potential (Φ), stream function (Ψ), velocity (V) and coefficient of pressure (Cp).

The basic method this thing works is this: calculate the selected quantity for each element inserted in the domain, add up their individual contributions, display the resulting quantity.

From theory, the two fundamental quantities - velocity potential and stream function for the three supported flow elements are:


If the user selected stream function or velocity potential, it is directly calculated for each grid point and a contour plot is generated. That is the end of it. If the user selected velocity, the velocity potential is calculated for each grid point still and then a sort of numerical differentiation is performed at each location to calculate the horizontal velocity field (u) and vertical velocity field (v) separately (except at the last row and column). These two fields are then used to construct a scatter plot in Plotly.js, the plotting library used here, to depict the vector field of velocities. This turned out to be the most time-consuming part in terms of compute/rendering requirements. Since the library doesn't natively support drawing vector arrows, I found out that scatter plots could do the job, with two ends of a vector drawn as a line segment joining two points - their exact locations being determined by the relative magnitudes of u and v at that point, followed by a null data point (so that all the points wouldn't be connected together). Similar story for the arrow tips and their rotation angles. Sampling a small enough number of grid points to draw these vector arrows on was another non-trivial issue. The default domain parameters being -50 to +50 along both axes with delX and delY of 0.1 each would mean a million grid points. Plotly chokes at such numbers and the resulting vector field would be too dense to make anything out anyway. So, picking a reasonable fraction of those points fast enough took some time as well. At any rate, as things stand, retrieving 2000 points and plotting their vector representations still takes over 10s, during which the whole page freezes. Maybe Web Worker threads could be used to better handle this but the real solution would be to find a better plotting library. Funny thing is, before realizing that this rendering process was the bottleneck, I blindly assumed it must be the numerous loops used in this process that were slowing down the application instead and quickly jumped to port them to GPU kernels using GPU.js (an awesome library that I had already tried in the past). Only after I had finished that I understood it didn't make any noticeable difference where the loops ran for the default domain parameters.

Another issue plaguing the application right from the beginning was that of phase jumps. In short, in contour plots involving phase values - basically angles points make with the origin from the positive x-axis - there is a discontinuity between phases of points from just below the x-axis to just above it (if the angle is a direct result of an atan2() function), either on both sides (positive and negative) or on the positive side only (if the angle is corrected to always report positive, usually clockwise from positive x-axis). This manifests in contour plots as a horizontal strip of dark lines drawn very close to one another due to the large difference in the phase values over a very short distance.


Initially, I thought I must have made some error in the code that would only show in non-symmetric flows (e.g. a source-sink pair not perfectly along the x-axis). I later realized that this problem is actually a pretty common occurrence in contour plots and a field of study of its own. I didn't bother to go too deep into it once I learned about its ubiquity (and in equal part its stubbornness to go away) as long as the contour lines could be made out (the curious heatmap values notwithstanding). I have included an attempt to correct this issue in the code but later decided to have it jumped over with a flag after it became clear it wasn't going to help much.

That's about the breadth of what I wanted to write here. Other than the above, the general looseness of JavaScript and the spaghettification of code that usually follows when a project starts out as a quick and dirty snippet to get a thing done doesn't need much expounding upon. GPU.js was a delight to work with and to see it in action still was a great feeling.

Here's the GUI of the application that I prepared for my presentation:


Here's the GitHub repo

Here's the live webpage