├── .gitattributes ├── .gitignore ├── README.md ├── derivations ├── linesource.mlx └── linevortex.mlx ├── examples ├── __init__.py ├── demo.py ├── doublet.py ├── freestream_source_sink.py ├── lifting_cylinder_flow.py └── nonlifting_approximate_naca0020.py ├── media ├── demo.png ├── freestream_source_sink.png ├── lifting_cylinder_flow.png ├── nonlifting_NACA0020.png └── nonlifting_NACA0020_at_angle.png ├── potentialflowvisualizer ├── __init__.py ├── flowfield.py └── objects.py ├── requirements.txt └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | .idea/ 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | .idea/workspace.xml 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PotentialFlowVisualizer (PFV) 2 | **By Peter Sharpe** 3 | 4 | *A fun, lightweight tool to visualize potential flows quickly!* 5 | 6 | ## PFV Description 7 | PotentialFlowVisualizer is a convenient Python library that can be used to quickly visualize various potential flow fields. If you're new to potential flows, I've written a short intro at the bottom of this document. 8 | 9 | PFV syntax is explicitly designed to be concise and readable. 10 | 11 | ## Installation 12 | 13 | Install with: `pip install potentialflowvisualizer` 14 | 15 | ## Examples 16 | 17 | Here are a few examples to show you how you might use PFV to look at basic flowfields: 18 | 19 | #### Freestream, Source, and Sink (Rankine Oval) 20 | ```python 21 | from potentialflowvisualizer import * 22 | 23 | field = Flowfield([ 24 | Freestream(1, 0), 25 | Source(10, -3, 0), 26 | Source(-10, 3, 0), 27 | ]) 28 | 29 | field.draw("streamfunction") 30 | ``` 31 | ![Freestream Source Sink](media/freestream_source_sink.png) 32 | 33 | #### Lifting Cylinder Flow 34 | ```python 35 | from potentialflowvisualizer import * 36 | 37 | field = Flowfield([ 38 | Freestream(1, 0), 39 | Vortex(20, 0, 0), 40 | Doublet(-100, 0, 0, 0) 41 | ]) 42 | 43 | field.draw("streamfunction") 44 | ``` 45 | ![Lifting Cylinder Flow](media/lifting_cylinder_flow.png) 46 | 47 | #### Nonlifting NACA0020 48 | Here, we use some key results of slender body theory to (approximately) model a thin streamlined body. 49 | ```python 50 | from potentialflowvisualizer import * 51 | 52 | # Body Geometry definition 53 | x = np.linspace(-5, 5, 21) 54 | c = np.max(x) - np.min(x) 55 | x0 = np.min(x) 56 | y = 0.2 * c * 10 * ( 57 | + 0.2969 * ((x - x0) / c) ** 0.5 58 | - 0.1260 * ((x - x0) / c) 59 | - 0.3516 * ((x - x0) / c) ** 2 60 | + 0.2843 * ((x - x0) / c) ** 3 61 | - 0.1036 * ((x - x0) / c) ** 4 62 | ) 63 | dy = np.diff(y) 64 | 65 | # Freestream properties 66 | V = 1 67 | 68 | # Set up the flowfield 69 | field = Flowfield([ 70 | Freestream(V, 0) 71 | ]) 72 | 73 | field.objects.extend( # Add line sources to model the thickness 74 | [LineSource(V * dy[i], x[i], 0, x[i + 1], 0) for i in range(len(x) - 1)] 75 | ) 76 | field.draw("streamfunction") # And visualize it 77 | ``` 78 | ![Nonlifting NACA0020 Flow](media/nonlifting_NACA0020.png) 79 | 80 | 81 | #### Nonlifting NACA0020 at an Angle 82 | To first order, we can model the flow around a NACA0020 section at an angle by adding doublets. (Note that the lack of net vorticity means that this is a nonlifting flow.) As we can see, slender body theory starts to break down a bit in this case. 83 | ```python 84 | from potentialflowvisualizer import * 85 | 86 | # Body Geometry definition 87 | x = np.linspace(-5, 5, 51) 88 | c = np.max(x) - np.min(x) 89 | x0 = np.min(x) 90 | y = 0.2 * c * 10 * ( 91 | + 0.2969 * ((x - x0) / c) ** 0.5 92 | - 0.1260 * ((x - x0) / c) 93 | - 0.3516 * ((x - x0) / c) ** 2 94 | + 0.2843 * ((x - x0) / c) ** 3 95 | - 0.1036 * ((x - x0) / c) ** 4 96 | ) 97 | dy = np.diff(y) 98 | 99 | # Freestream properties 100 | V = 1 101 | alpha = 10 102 | alpha_rad = np.radians(alpha) 103 | 104 | # Set up the flowfield 105 | field = Flowfield([ 106 | Freestream(V * np.cos(alpha_rad), V * np.sin(alpha_rad)) 107 | ]) 108 | 109 | field.objects.extend( # Add line sources to model the thickness 110 | [LineSource(V * dy[i], x[i], 0, x[i + 1], 0) for i in range(len(x) - 1)] 111 | ) 112 | field.objects.extend( # Add doublets to model crossflow 113 | [Doublet(2 * V * y[i] * alpha_rad, x[i], 0, np.radians(90)) for i in range(len(x))] 114 | ) 115 | 116 | field.draw("streamfunction") # And visualize it 117 | ``` 118 | ![Nonlifting NACA0020 at angle](media/nonlifting_NACA0020_at_angle.png) 119 | 120 | And, mix and match elements however you want! 121 | 122 | ![Demo](media/demo.png) 123 | 124 | ## Potential Flow: A 30-second Crash Course 125 | Potential flow is a model of how fluids behave under certain mathematically-convenient assumptions. Potential flows can be described by a short, elegant equation: 126 | 127 | $$\nabla^2\phi=0$$ 128 | 129 | Here, $\phi$ is a scalar that represents the "velocity potential" at a given point - we use the word "potential", because it's sort of analogous to potential energy or a voltage potential. To find the fluid's velocity at a point, we look at the derivatives of the potential at that point: 130 | 131 | $$\nabla\phi=\vec{V}$$ 132 | 133 | There are a few "fundamental solutions" to this governing equation - a few of these are shown below. The governing equation is linear, so we can superimpose various fundamental solutions until we obtain a flowfield that describes some physically-relevant situation. 134 | 135 | Because of this, potential flow is applicable to many common problems in aerodynamics and hydrodynamics. Furthermore, the ability to break complex flow fields into simple "fundamental solutions" allows engineers to gain an intuitive understanding of the physics at play - something that can be much more difficult with other, more complicated flow models. 136 | 137 | ## License 138 | MIT License 139 | 140 | Copyright 2020 Peter Sharpe 141 | 142 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 143 | 144 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 145 | 146 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 147 | -------------------------------------------------------------------------------- /derivations/linesource.mlx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterdsharpe/PotentialFlowVisualizer/fae73583c0f16f45cfd9fec13263c6b603d6d900/derivations/linesource.mlx -------------------------------------------------------------------------------- /derivations/linevortex.mlx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterdsharpe/PotentialFlowVisualizer/fae73583c0f16f45cfd9fec13263c6b603d6d900/derivations/linevortex.mlx -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterdsharpe/PotentialFlowVisualizer/fae73583c0f16f45cfd9fec13263c6b603d6d900/examples/__init__.py -------------------------------------------------------------------------------- /examples/demo.py: -------------------------------------------------------------------------------- 1 | from potentialflowvisualizer import * 2 | 3 | field = Flowfield( 4 | objects=[ 5 | Freestream(u=1, v=0), 6 | Source(strength=5, x=-5, y=0), 7 | Vortex(strength=5, x=0, y=5), 8 | Doublet(strength=5, x=0, y=0, alpha=0), 9 | LineVortex(strength=-5, x1 = 0, y1 = -5, x2 = 5, y2 = 0), 10 | LineSource(strength=-5, x1 = 0, y1 = 5, x2 = 5, y2 = 0), 11 | ] 12 | ) 13 | 14 | field.draw("potential") 15 | field.draw("streamfunction") 16 | field.draw("xvel") 17 | field.draw("yvel") 18 | field.draw("velmag") 19 | -------------------------------------------------------------------------------- /examples/doublet.py: -------------------------------------------------------------------------------- 1 | from potentialflowvisualizer import * 2 | 3 | field = Flowfield([ 4 | Doublet(1, 0, 0, 0) 5 | ]) 6 | 7 | # field.draw("potential") 8 | # field.draw("streamfunction") 9 | field.draw("xvel") 10 | field.draw("yvel") 11 | # field.draw("velmag") 12 | -------------------------------------------------------------------------------- /examples/freestream_source_sink.py: -------------------------------------------------------------------------------- 1 | from potentialflowvisualizer import * 2 | 3 | field = Flowfield([ 4 | Freestream(1, 0), 5 | Source(10, -3, 0), 6 | Source(-10, 3, 0), 7 | ]) 8 | 9 | field.draw("potential") 10 | field.draw("streamfunction") 11 | field.draw("xvel") 12 | field.draw("yvel") 13 | field.draw("velmag") 14 | -------------------------------------------------------------------------------- /examples/lifting_cylinder_flow.py: -------------------------------------------------------------------------------- 1 | from potentialflowvisualizer import * 2 | 3 | field = Flowfield( 4 | objects=[ 5 | Freestream(1, 0), 6 | # Source(1, 0, 0), 7 | Vortex(-20, 0, 0), 8 | Doublet(-100, 0, 0, 0), 9 | # LineSource(1, 0, 0, 0, 5), 10 | # LineVortex(1, 0, 0, 0, 5), 11 | ] 12 | ) 13 | 14 | # field.draw("potential") 15 | field.draw("streamfunction") 16 | # field.draw("xvel") 17 | # field.draw("yvel") 18 | # field.draw("velmag") 19 | field.draw_streamlines() -------------------------------------------------------------------------------- /examples/nonlifting_approximate_naca0020.py: -------------------------------------------------------------------------------- 1 | from potentialflowvisualizer import * 2 | 3 | 4 | def cosspace(min=0, max=1, n_points=50): 5 | mean = (max + min) / 2 6 | amp = (max - min) / 2 7 | 8 | return mean + amp * np.cos(np.linspace(np.pi, 0, n_points)) 9 | 10 | 11 | # Body Geometry definition 12 | x = cosspace(-5, 5, 41) 13 | c = np.max(x) - np.min(x) 14 | x0 = np.min(x) 15 | y = 0.2 * c * 10 / 2 * ( 16 | + 0.2969 * ((x - x0) / c) ** 0.5 17 | - 0.1260 * ((x - x0) / c) 18 | - 0.3516 * ((x - x0) / c) ** 2 19 | + 0.2843 * ((x - x0) / c) ** 3 20 | - 0.1036 * ((x - x0) / c) ** 4 21 | ) 22 | dx = np.diff(x) 23 | dy = np.diff(y) 24 | 25 | # Flow properties 26 | V = 100 27 | alpha = 10 28 | alpha_rad = np.radians(alpha) 29 | 30 | field = Flowfield([ 31 | Freestream(V * np.cos(alpha_rad), V * np.sin(alpha_rad)) 32 | ]) 33 | 34 | # field.objects.extend( 35 | # [Source(V * dy[i], (x[i]+x[i+1])/2, 0) for i in range(len(x)-1)] 36 | # ) 37 | 38 | field.objects.extend( 39 | [LineSource(2 * V * dy[i], x[i], 0, x[i + 1], 0) for i in range(len(x) - 1)] 40 | ) 41 | # field.objects.extend( 42 | # [Vortex(V * np.pi * alpha_rad, 0, 0)] 43 | # ) 44 | # field.objects.extend( 45 | # [Vortex(V * np.pi * alpha_rad * dx[i] / c, (x[i] + x[i+1])/2, 0) for i in range(len(x)-1)] 46 | # ) 47 | 48 | # field.objects.extend( 49 | # [Doublet(V * (y[i] + y[i+1])/2 * dx[i], (x[i] + x[i+1])/2, 0, np.radians(180)) for i in range(len(x) - 1)] 50 | # ) 51 | field.objects.extend( 52 | [Doublet(2 * V * (2 * y[i]) * alpha_rad * dx[i], (x[i] + x[i+1])/2, 0, np.radians(90+alpha)) for i in range(len(x) - 1)] 53 | ) 54 | 55 | field.draw("streamfunction") 56 | -------------------------------------------------------------------------------- /media/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterdsharpe/PotentialFlowVisualizer/fae73583c0f16f45cfd9fec13263c6b603d6d900/media/demo.png -------------------------------------------------------------------------------- /media/freestream_source_sink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterdsharpe/PotentialFlowVisualizer/fae73583c0f16f45cfd9fec13263c6b603d6d900/media/freestream_source_sink.png -------------------------------------------------------------------------------- /media/lifting_cylinder_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterdsharpe/PotentialFlowVisualizer/fae73583c0f16f45cfd9fec13263c6b603d6d900/media/lifting_cylinder_flow.png -------------------------------------------------------------------------------- /media/nonlifting_NACA0020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterdsharpe/PotentialFlowVisualizer/fae73583c0f16f45cfd9fec13263c6b603d6d900/media/nonlifting_NACA0020.png -------------------------------------------------------------------------------- /media/nonlifting_NACA0020_at_angle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterdsharpe/PotentialFlowVisualizer/fae73583c0f16f45cfd9fec13263c6b603d6d900/media/nonlifting_NACA0020_at_angle.png -------------------------------------------------------------------------------- /potentialflowvisualizer/__init__.py: -------------------------------------------------------------------------------- 1 | from .objects import * 2 | from .flowfield import * -------------------------------------------------------------------------------- /potentialflowvisualizer/flowfield.py: -------------------------------------------------------------------------------- 1 | import aerosandbox as asb 2 | import aerosandbox.numpy as np 3 | from potentialflowvisualizer.objects import Singularity 4 | from typing import List 5 | 6 | 7 | class Flowfield: 8 | def __init__(self, 9 | objects: List[Singularity] = None 10 | ): 11 | if objects is None: 12 | objects = [] 13 | 14 | self.objects = objects 15 | 16 | def get_potential_at(self, points: np.ndarray): 17 | return sum([object.get_potential_at(points) for object in self.objects]) 18 | 19 | def get_streamfunction_at(self, points: np.ndarray): 20 | return sum([object.get_streamfunction_at(points) for object in self.objects]) 21 | 22 | def get_x_velocity_at(self, points: np.ndarray): 23 | return sum([object.get_x_velocity_at(points) for object in self.objects]) 24 | 25 | def get_y_velocity_at(self, points: np.ndarray): 26 | return sum([object.get_y_velocity_at(points) for object in self.objects]) 27 | 28 | def draw(self, 29 | scalar_to_plot: str = "potential", # "potential", "streamfunction", "xvel", "yvel", "velmag", "Cp" 30 | x_points: np.ndarray = np.linspace(-10, 10, 400), 31 | y_points: np.ndarray = np.linspace(-10, 10, 300), 32 | percentiles_to_include: float = 99.7, 33 | set_equal: bool = True, 34 | show: bool = True, 35 | ): 36 | import matplotlib.pyplot as plt 37 | import aerosandbox.tools.pretty_plots as p 38 | 39 | X, Y = np.meshgrid(x_points, y_points) 40 | X_r = np.reshape(X, -1) 41 | Y_r = np.reshape(Y, -1) 42 | points = np.vstack((X_r, Y_r)).T 43 | 44 | if scalar_to_plot == "potential": 45 | scalar_to_plot_value = sum([object.get_potential_at(points) for object in self.objects]) 46 | elif scalar_to_plot == "streamfunction": 47 | scalar_to_plot_value = sum([object.get_streamfunction_at(points) for object in self.objects]) 48 | elif scalar_to_plot == "xvel": 49 | scalar_to_plot_value = sum([object.get_x_velocity_at(points) for object in self.objects]) 50 | elif scalar_to_plot == "yvel": 51 | scalar_to_plot_value = sum([object.get_y_velocity_at(points) for object in self.objects]) 52 | elif scalar_to_plot == "velmag": 53 | x_vels = sum([object.get_x_velocity_at(points) for object in self.objects]) 54 | y_vels = sum([object.get_y_velocity_at(points) for object in self.objects]) 55 | scalar_to_plot_value = np.sqrt(x_vels ** 2 + y_vels ** 2) 56 | elif scalar_to_plot == "Cp": 57 | x_vels = sum([object.get_x_velocity_at(points) for object in self.objects]) 58 | y_vels = sum([object.get_y_velocity_at(points) for object in self.objects]) 59 | V = np.sqrt(x_vels ** 2 + y_vels ** 2) 60 | scalar_to_plot_value = 1 - V ** 2 61 | else: 62 | raise ValueError("Bad value of `scalar_to_plot`!") 63 | 64 | min = np.nanpercentile(scalar_to_plot_value, 50 - percentiles_to_include / 2) 65 | max = np.nanpercentile(scalar_to_plot_value, 50 + percentiles_to_include / 2) 66 | 67 | p.contour( 68 | x_points, y_points, scalar_to_plot_value.reshape(X.shape), 69 | levels=np.linspace(min, max, 80), 70 | linelabels=False, 71 | cmap=plt.get_cmap("rainbow"), 72 | contour_kwargs={ 73 | "linestyles": 'solid', 74 | "alpha" : 0.4 75 | } 76 | ) 77 | 78 | if set_equal: 79 | p.equal() 80 | 81 | p.show_plot( 82 | f"Potential Flow: {scalar_to_plot}", 83 | "$x$", 84 | "$y$", 85 | show=show 86 | ) 87 | 88 | def draw_streamlines(self, 89 | x_points: np.ndarray = np.linspace(-10, 10, 400), 90 | y_points: np.ndarray = np.linspace(-10, 10, 300), 91 | cmap=None, 92 | norm=None, 93 | set_equal: bool = True, 94 | show: bool = True, 95 | ): 96 | 97 | X, Y = np.meshgrid(x_points, y_points) 98 | U = np.reshape( 99 | self.get_x_velocity_at(np.stack([ 100 | X.flatten(), 101 | Y.flatten() 102 | ], axis=1)), 103 | X.shape 104 | ) 105 | V = np.reshape( 106 | self.get_y_velocity_at(np.stack([ 107 | X.flatten(), 108 | Y.flatten() 109 | ], axis=1)), 110 | X.shape 111 | ) 112 | 113 | velmag = np.sqrt(U ** 2 + V ** 2) 114 | 115 | import matplotlib.pyplot as plt 116 | import aerosandbox.tools.pretty_plots as p 117 | 118 | if cmap is None: 119 | cmap = "coolwarm_r" 120 | 121 | if norm is None: 122 | from matplotlib.colors import LogNorm 123 | norm = LogNorm( 124 | np.quantile(velmag, 0.05), 125 | np.quantile(velmag, 0.95) 126 | ) 127 | 128 | plt.streamplot( 129 | X, Y, U, V, 130 | color=velmag, 131 | linewidth=1, 132 | minlength=0.02, 133 | cmap=cmap, 134 | norm=norm, 135 | broken_streamlines=False 136 | ) 137 | 138 | plt.colorbar(label="Velocity Magnitude [m/s]") 139 | 140 | if set_equal: 141 | p.equal() 142 | 143 | p.show_plot( 144 | f"Potential Flow: Streamlines", 145 | "$x$", 146 | "$y$", 147 | show=show 148 | ) 149 | -------------------------------------------------------------------------------- /potentialflowvisualizer/objects.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from abc import ABC, abstractmethod 3 | 4 | 5 | class Singularity(ABC): 6 | 7 | @abstractmethod 8 | def get_potential_at(self, points: np.ndarray): 9 | pass 10 | 11 | @abstractmethod 12 | def get_streamfunction_at(self, points: np.ndarray): 13 | pass 14 | 15 | @abstractmethod 16 | def get_x_velocity_at(self, points: np.ndarray): 17 | pass 18 | 19 | @abstractmethod 20 | def get_y_velocity_at(self, points: np.ndarray): 21 | pass 22 | 23 | 24 | ### For all classes: 25 | # points is a Nx2 NumPy array of the points you want to evaluate something at. 26 | 27 | class Freestream(Singularity): 28 | def __init__(self, 29 | u, 30 | v, 31 | ): 32 | self.u = u 33 | self.v = v 34 | 35 | def get_potential_at(self, points: np.ndarray): 36 | return self.u * points[:, 0] + self.v * points[:, 1] 37 | 38 | def get_streamfunction_at(self, points: np.ndarray): 39 | return -self.v * points[:, 0] + self.u * points[:, 1] 40 | 41 | def get_x_velocity_at(self, points: np.ndarray): 42 | return self.u * np.ones_like(points[:, 0]) 43 | 44 | def get_y_velocity_at(self, points: np.ndarray): 45 | return self.v * np.ones_like(points[:, 0]) 46 | 47 | 48 | class Source(Singularity): 49 | def __init__(self, 50 | strength, 51 | x, # x-location 52 | y, # y-location 53 | ): 54 | self.strength = strength 55 | self.x = x 56 | self.y = y 57 | 58 | def get_potential_at(self, points: np.ndarray): 59 | return self.strength / (2 * np.pi) * np.log(np.sqrt( 60 | (points[:, 0] - self.x) ** 2 + 61 | (points[:, 1] - self.y) ** 2 62 | )) 63 | 64 | def get_streamfunction_at(self, points: np.ndarray): 65 | return self.strength / (2 * np.pi) * np.arctan2( 66 | points[:, 1] - self.y, 67 | points[:, 0] - self.x 68 | ) 69 | 70 | def get_x_velocity_at(self, points: np.ndarray): 71 | return self.strength / (2 * np.pi) * (points[:, 0] - self.x) / ( 72 | (points[:, 0] - self.x) ** 2 + 73 | (points[:, 1] - self.y) ** 2 74 | ) 75 | 76 | def get_y_velocity_at(self, points: np.ndarray): 77 | return self.strength / (2 * np.pi) * (points[:, 1] - self.y) / ( 78 | (points[:, 0] - self.x) ** 2 + 79 | (points[:, 1] - self.y) ** 2 80 | ) 81 | 82 | 83 | class Vortex(Singularity): 84 | def __init__(self, 85 | strength, 86 | x, # x-location 87 | y, # y-location 88 | ): 89 | self.strength = strength 90 | self.x = x 91 | self.y = y 92 | 93 | def get_potential_at(self, points: np.ndarray): 94 | return self.strength / (2 * np.pi) * np.arctan2( 95 | points[:, 1] - self.y, 96 | points[:, 0] - self.x 97 | ) 98 | 99 | def get_streamfunction_at(self, points: np.ndarray): 100 | return -self.strength / (2 * np.pi) * np.log(np.sqrt( 101 | (points[:, 0] - self.x) ** 2 + 102 | (points[:, 1] - self.y) ** 2 103 | )) 104 | 105 | def get_x_velocity_at(self, points: np.ndarray): 106 | return self.strength / (2 * np.pi) * -(points[:, 1] - self.y) / ( 107 | (points[:, 0] - self.x) ** 2 + 108 | (points[:, 1] - self.y) ** 2 109 | ) 110 | 111 | def get_y_velocity_at(self, points: np.ndarray): 112 | return self.strength / (2 * np.pi) * (points[:, 0] - self.x) / ( 113 | (points[:, 0] - self.x) ** 2 + 114 | (points[:, 1] - self.y) ** 2 115 | ) 116 | 117 | 118 | class Doublet(Singularity): 119 | """ 120 | Note that there are differing sign conventions for doublet strength in the literature. 121 | 122 | Consider the case of a unit-strength doublet, at the origin (0, 0), facing "to the right" (alpha = 0). 123 | 124 | We use the "MIT convention", where a positive strength induces a flow to right along the doublet axis (the "core"). So, 125 | points on the x-axis itself will have an induced velocity in the +x direction. Points far from the doublet axis (on "the donut") will in general 126 | have induced velocity in the -x direction. See a diagram of the MIT convention: 127 | > https://web.mit.edu/fluids-modules/www/potential_flows/LecturesHTML/lec1011/node24.html 128 | 129 | The alternative is the "Cambridge convention", where this is flipped: 130 | > http://www-mdp.eng.cam.ac.uk/web/library/enginfo/aerothermal_dvd_only/aero/fprops/poten/node33.html 131 | 132 | We use the MIT convention in this code implementation, but either convention is fine as long as you're consistent. 133 | """ 134 | 135 | def __init__(self, 136 | strength, 137 | x, # x-location 138 | y, # y-location 139 | alpha, # angle, in radians 140 | ): 141 | self.strength = strength 142 | self.x = x 143 | self.y = y 144 | self.alpha = alpha 145 | 146 | def get_potential_at(self, points: np.ndarray): 147 | return -self.strength / (2 * np.pi) * ( 148 | (points[:, 0] - self.x) * np.cos(self.alpha) + 149 | (points[:, 1] - self.y) * np.sin(self.alpha) 150 | ) / ( 151 | (points[:, 0] - self.x) ** 2 + 152 | (points[:, 1] - self.y) ** 2 153 | ) 154 | 155 | def get_streamfunction_at(self, points: np.ndarray): 156 | return self.strength / (2 * np.pi) * ( 157 | (points[:, 0] - self.x) * np.sin(self.alpha) + 158 | (points[:, 1] - self.y) * np.cos(self.alpha) 159 | ) / ( 160 | (points[:, 0] - self.x) ** 2 + 161 | (points[:, 1] - self.y) ** 2 162 | ) 163 | 164 | def get_x_velocity_at(self, points: np.ndarray): 165 | return -self.strength / (2 * np.pi) * ( 166 | ( 167 | (points[:, 0] - self.x) ** 2 + 168 | (points[:, 1] - self.y) ** 2 169 | ) * np.cos(self.alpha) - 170 | 2 * (points[:, 0] - self.x) * ( 171 | (points[:, 0] - self.x) * np.cos(self.alpha) + 172 | (points[:, 1] - self.y) * np.sin(self.alpha) 173 | ) 174 | ) / ( 175 | (points[:, 0] - self.x) ** 2 + 176 | (points[:, 1] - self.y) ** 2 177 | ) ** 2 178 | 179 | def get_y_velocity_at(self, points: np.ndarray): 180 | return -self.strength / (2 * np.pi) * ( 181 | ( 182 | (points[:, 0] - self.x) ** 2 + 183 | (points[:, 1] - self.y) ** 2 184 | ) * np.sin(self.alpha) - 185 | 2 * (points[:, 1] - self.y) * ( 186 | (points[:, 0] - self.x) * np.cos(self.alpha) + 187 | (points[:, 1] - self.y) * np.sin(self.alpha) 188 | ) 189 | ) / ( 190 | (points[:, 0] - self.x) ** 2 + 191 | (points[:, 1] - self.y) ** 2 192 | ) ** 2 193 | 194 | 195 | class LineSource(Singularity): 196 | def __init__(self, 197 | strength, 198 | x1, # x-location of start 199 | y1, # y-location of start 200 | x2, # x-location of end 201 | y2, # y-location of end 202 | ): 203 | self.strength = strength 204 | self.x1 = x1 205 | self.y1 = y1 206 | self.x2 = x2 207 | self.y2 = y2 208 | 209 | def get_potential_at(self, points: np.ndarray): 210 | A = np.array([ 211 | [self.x2 - self.x1, self.y2 - self.y1], 212 | [self.y1 - self.y2, self.x2 - self.x1] 213 | ]) / ((self.x2 - self.x1) ** 2 + (self.y2 - self.y1) ** 2) 214 | b = np.array([self.x1, self.y1]) 215 | 216 | points_transformed = np.transpose(A @ (points - b).T) 217 | xf = points_transformed[:, 0] 218 | yf = points_transformed[:, 1] 219 | 220 | potential = self.strength / (2 * np.pi) * ( 221 | yf * (np.arctan(xf / yf) - np.arctan((xf - 1) / yf)) + 222 | xf * np.log(xf ** 2 + yf ** 2) / 2 - 223 | np.log(np.sqrt((xf - 1) ** 2 + yf ** 2)) * (xf - 1) - 1 224 | ) 225 | 226 | return potential 227 | 228 | def get_streamfunction_at(self, points: np.ndarray): 229 | A = np.array([ 230 | [self.x2 - self.x1, self.y2 - self.y1], 231 | [self.y1 - self.y2, self.x2 - self.x1] 232 | ]) / ((self.x2 - self.x1) ** 2 + (self.y2 - self.y1) ** 2) 233 | b = np.array([self.x1, self.y1]) 234 | 235 | points_transformed = np.transpose(A @ (points - b).T) 236 | xf = points_transformed[:, 0] 237 | yf = points_transformed[:, 1] 238 | 239 | s1 = -yf / 2 + xf / 2 * 1j 240 | s2 = yf / 2 + xf / 2 * 1j 241 | s3 = xf - 1 + yf * 1j 242 | 243 | streamfunction = self.strength / (2 * np.pi) * ( 244 | -np.log(s3 / np.sqrt((xf - 1) ** 2 + yf ** 2)) * 1j 245 | - np.log(xf + yf * 1j) * s1 246 | + np.log(s3) * s1 247 | + np.log(-xf + yf * 1j) * s2 248 | - np.log(1 - xf + yf * 1j) * s2 249 | ) 250 | streamfunction = np.real(streamfunction) 251 | 252 | return streamfunction 253 | 254 | def get_x_velocity_at(self, points: np.ndarray): 255 | A = np.array([ 256 | [self.x2 - self.x1, self.y2 - self.y1], 257 | [self.y1 - self.y2, self.x2 - self.x1] 258 | ]) / ((self.x2 - self.x1) ** 2 + (self.y2 - self.y1) ** 2) 259 | b = np.array([self.x1, self.y1]) 260 | 261 | points_transformed = np.transpose(A @ (points - b).T) 262 | xf = points_transformed[:, 0] 263 | yf = points_transformed[:, 1] 264 | 265 | x_vel = -self.strength / (4 * np.pi) * ( 266 | np.log(xf + yf * 1j) * 1j 267 | - np.log(xf - 1 + yf * 1j) * 1j 268 | - np.log(-xf + yf * 1j) * 1j 269 | + np.log(1 - xf + yf * 1j) * 1j 270 | ) 271 | 272 | x_vel = np.real(x_vel) 273 | 274 | scalefactor = np.sqrt((self.x2 - self.x1) ** 2 + (self.y2 - self.y1) ** 2) 275 | x_vel /= scalefactor 276 | 277 | return x_vel 278 | 279 | def get_y_velocity_at(self, points: np.ndarray): 280 | A = np.array([ 281 | [self.x2 - self.x1, self.y2 - self.y1], 282 | [self.y1 - self.y2, self.x2 - self.x1] 283 | ]) / ((self.x2 - self.x1) ** 2 + (self.y2 - self.y1) ** 2) 284 | b = np.array([self.x1, self.y1]) 285 | 286 | points_transformed = np.transpose(A @ (points - b).T) 287 | xf = points_transformed[:, 0] 288 | yf = points_transformed[:, 1] 289 | 290 | y_vel = self.strength / (4 * np.pi) * ( 291 | np.log(xf + yf * 1j) 292 | - np.log(xf - 1 + yf * 1j) 293 | + np.log(-xf + yf * 1j) 294 | - np.log(1 - xf + yf * 1j) 295 | ) 296 | 297 | y_vel = np.real(y_vel) 298 | 299 | scalefactor = np.sqrt((self.x2 - self.x1) ** 2 + (self.y2 - self.y1) ** 2) 300 | y_vel /= scalefactor 301 | 302 | return y_vel 303 | 304 | 305 | 306 | 307 | class LineVortex(Singularity): 308 | def __init__(self, 309 | strength, 310 | x1, # x-location of start 311 | y1, # y-location of start 312 | x2, # x-location of end 313 | y2, # y-location of end 314 | ): 315 | self.strength = strength 316 | self.x1 = x1 317 | self.y1 = y1 318 | self.x2 = x2 319 | self.y2 = y2 320 | 321 | def get_potential_at(self, points: np.ndarray): 322 | A = np.array([ 323 | [self.x2 - self.x1, self.y2 - self.y1], 324 | [self.y1 - self.y2, self.x2 - self.x1] 325 | ]) / ((self.x2 - self.x1) ** 2 + (self.y2 - self.y1) ** 2) 326 | b = np.array([self.x1, self.y1]) 327 | 328 | points_transformed = np.transpose(A @ (points - b).T) 329 | xf = points_transformed[:, 0] 330 | yf = points_transformed[:, 1] 331 | 332 | s1 = -yf / 2 + xf / 2 * 1j 333 | s2 = yf / 2 + xf / 2 * 1j 334 | s3 = xf - 1 + yf * 1j 335 | 336 | potential = self.strength / (2 * np.pi) * ( 337 | -np.log(s3 / np.sqrt((xf - 1) ** 2 + yf ** 2)) * 1j 338 | - np.log(xf + yf * 1j) * s1 339 | + np.log(s3) * s1 340 | + np.log(-xf + yf * 1j) * s2 341 | - np.log(1 - xf + yf * 1j) * s2 342 | ) 343 | potential = np.real(potential) 344 | 345 | return potential 346 | 347 | def get_streamfunction_at(self, points: np.ndarray): 348 | A = np.array([ 349 | [self.x2 - self.x1, self.y2 - self.y1], 350 | [self.y1 - self.y2, self.x2 - self.x1] 351 | ]) / ((self.x2 - self.x1) ** 2 + (self.y2 - self.y1) ** 2) 352 | b = np.array([self.x1, self.y1]) 353 | 354 | points_transformed = np.transpose(A @ (points - b).T) 355 | xf = points_transformed[:, 0] 356 | yf = points_transformed[:, 1] 357 | 358 | streamfunction = -self.strength / (2 * np.pi) * ( 359 | yf * (np.arctan(xf / yf) - np.arctan((xf - 1) / yf)) + 360 | xf * np.log(xf ** 2 + yf ** 2) / 2 - 361 | np.log(np.sqrt((xf - 1) ** 2 + yf ** 2)) * (xf - 1) - 1 362 | ) 363 | 364 | return streamfunction 365 | 366 | def get_x_velocity_at(self, points: np.ndarray): 367 | A = np.array([ 368 | [self.x2 - self.x1, self.y2 - self.y1], 369 | [self.y1 - self.y2, self.x2 - self.x1] 370 | ]) / ((self.x2 - self.x1) ** 2 + (self.y2 - self.y1) ** 2) 371 | b = np.array([self.x1, self.y1]) 372 | 373 | points_transformed = np.transpose(A @ (points - b).T) 374 | xf = points_transformed[:, 0] 375 | yf = points_transformed[:, 1] 376 | 377 | x_vel = self.strength / (4 * np.pi) * ( 378 | np.log(xf ** 2 - 2 * xf + yf ** 2 + 1) 379 | - np.log(xf ** 2 + yf ** 2) 380 | ) 381 | scalefactor = np.sqrt((self.x2 - self.x1) ** 2 + (self.y2 - self.y1) ** 2) 382 | x_vel /= scalefactor 383 | 384 | return x_vel 385 | 386 | def get_y_velocity_at(self, points: np.ndarray): 387 | A = np.array([ 388 | [self.x2 - self.x1, self.y2 - self.y1], 389 | [self.y1 - self.y2, self.x2 - self.x1] 390 | ]) / ((self.x2 - self.x1) ** 2 + (self.y2 - self.y1) ** 2) 391 | b = np.array([self.x1, self.y1]) 392 | 393 | points_transformed = np.transpose(A @ (points - b).T) 394 | xf = points_transformed[:, 0] 395 | yf = points_transformed[:, 1] 396 | 397 | y_vel = -self.strength / (2 * np.pi) * ( 398 | np.arctan(xf / yf) 399 | - np.arctan((xf - 1) / yf) 400 | ) 401 | scalefactor = np.sqrt((self.x2 - self.x1) ** 2 + (self.y2 - self.y1) ** 2) 402 | y_vel /= scalefactor 403 | 404 | return y_vel -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aerosandbox~=3.2.6 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """A setuptools based setup module. 2 | 3 | See: 4 | https://packaging.python.org/guides/distributing-packages-using-setuptools/ 5 | https://github.com/pypa/sampleproject 6 | """ 7 | 8 | # Always prefer setuptools over distutils 9 | from setuptools import setup, find_packages 10 | from os import path 11 | 12 | here = path.abspath(path.dirname(__file__)) 13 | 14 | # Get the long description from the README file 15 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 16 | long_description = f.read() 17 | 18 | # Arguments marked as "Required" below must be included for upload to PyPI. 19 | # Fields marked as "Optional" may be commented out. 20 | 21 | setup( 22 | # This is the name of your project. The first time you publish this 23 | # package, this name will be registered for you. It will determine how 24 | # users can install this project, e.g.: 25 | # 26 | # $ pip install sampleproject 27 | # 28 | # And where it will live on PyPI: https://pypi.org/project/sampleproject/ 29 | # 30 | # There are some restrictions on what makes a valid project name 31 | # specification here: 32 | # https://packaging.python.org/specifications/core-metadata/#name 33 | name='PotentialFlowVisualizer', # Required 34 | 35 | # Versions should comply with PEP 440: 36 | # https://www.python.org/dev/peps/pep-0440/ 37 | # 38 | # For a discussion on single-sourcing the version across setup.py and the 39 | # project code, see 40 | # https://packaging.python.org/en/latest/single_source_version.html 41 | version="0.2.1", 42 | 43 | # This is a one-line description or tagline of what your project does. This 44 | # corresponds to the "Summary" metadata field: 45 | # https://packaging.python.org/specifications/core-metadata/#summary 46 | description='A fun, lightweight tool to visualize potential flows quickly!', 47 | # Optional 48 | 49 | # This is an optional longer description of your project that represents 50 | # the body of text which users will see when they visit PyPI. 51 | # 52 | # Often, this is the same as your README, so you can just read it in from 53 | # that file directly (as we have already done above) 54 | # 55 | # This field corresponds to the "Description" metadata field: 56 | # https://packaging.python.org/specifications/core-metadata/#description-optional 57 | long_description=long_description, # Optional 58 | 59 | # Denotes that our long_description is in Markdown; valid values are 60 | # text/plain, text/x-rst, and text/markdown 61 | # 62 | # Optional if long_description is written in reStructuredText (rst) but 63 | # required for plain-text or Markdown; if unspecified, "applications should 64 | # attempt to render [the long_description] as text/x-rst; charset=UTF-8 and 65 | # fall back to text/plain if it is not valid rst" (see link below) 66 | # 67 | # This field corresponds to the "Description-Content-Type" metadata field: 68 | # https://packaging.python.org/specifications/core-metadata/#description-content-type-optional 69 | long_description_content_type='text/markdown', # Optional (see note above) 70 | 71 | # This should be a valid link to your project's main homepage. 72 | # 73 | # This field corresponds to the "Home-Page" metadata field: 74 | # https://packaging.python.org/specifications/core-metadata/#home-page-optional 75 | url='https://peterdsharpe.github.io/PotentialFlowVisualizer/', # Optional 76 | 77 | # This should be your name or the name of the organization which owns the 78 | # project. 79 | author='Peter Sharpe', # Optional 80 | 81 | # This should be a valid email address corresponding to the author listed 82 | # above. 83 | author_email='peterdsharpe@gmail.com', # Optional 84 | 85 | # Classifiers help users find your project by categorizing it. 86 | # 87 | # For a list of valid classifiers, see https://pypi.org/classifiers/ 88 | classifiers=[ # Optional 89 | # How mature is this project? Common values are 90 | # 3 - Alpha 91 | # 4 - Beta 92 | # 5 - Production/Stable 93 | 'Development Status :: 4 - Beta', 94 | 95 | # Indicate who your project is intended for 96 | 'Intended Audience :: Science/Research', 97 | 'Topic :: Scientific/Engineering :: Physics', 98 | 99 | # Pick your license as you wish 100 | 'License :: OSI Approved :: MIT License', 101 | 102 | # Specify the Python versions you support here. In particular, ensure 103 | # that you indicate whether you support Python 2, Python 3 or both. 104 | # These classifiers are *not* checked by 'pip install'. See instead 105 | # 'python_requires' below. 106 | 'Programming Language :: Python :: 3.7', 107 | ], 108 | 109 | # This field adds keywords for your project which will appear on the 110 | # project page. What does your project relate to? 111 | # 112 | # Note that this is a string of words separated by whitespace, not a list. 113 | keywords='potential flow aerodynamics hydrodynamics air fluid dynamics mechanics', # Optional 114 | 115 | # You can just specify package directories manually here if your project is 116 | # simple. Or you can use find_packages(). 117 | # 118 | # Alternatively, if you just want to distribute a single Python file, use 119 | # the `py_modules` argument instead as follows, which will expect a file 120 | # called `my_module.py` to exist: 121 | # 122 | # py_modules=["my_module"], 123 | # 124 | packages=find_packages(exclude=['media', 'examples', 'studies']), # Required 125 | 126 | # Specify which Python versions you support. In contrast to the 127 | # 'Programming Language' classifiers above, 'pip install' will check this 128 | # and refuse to install the project if the version does not match. If you 129 | # do not support Python 2, you can simplify this to '>=3.5' or similar, see 130 | # https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires 131 | python_requires='>=3, <4', 132 | 133 | # This field lists other packages that your project depends on to run. 134 | # Any package you put here will be installed by pip when your project is 135 | # installed, so they must be valid existing projects. 136 | # 137 | # For an analysis of "install_requires" vs pip's requirements files see: 138 | # https://packaging.python.org/en/latest/requirements.html 139 | install_requires=['aerosandbox > 3.1.16'], 140 | # Optional 141 | 142 | # List additional groups of dependencies here (e.g. development 143 | # dependencies). Users will be able to install these using the "extras" 144 | # syntax, for example: 145 | # 146 | # $ pip install sampleproject[dev] 147 | # 148 | # Similar to `install_requires` above, these must be valid existing 149 | # projects. 150 | extras_require={ # Optional 151 | # 'xfoil': ['xfoil'], # Change the key name to something more descriptive 152 | # w.r.t. to functionality. 153 | # 'test': ['coverage'], 154 | }, 155 | 156 | # If there are data files included in your packages that need to be 157 | # installed, specify them here. 158 | # 159 | # If using Python 2.6 or earlier, then these have to be included in 160 | # MANIFEST.in as well. 161 | # package_data={ # Optional 162 | # 'sample': ['package_data.dat'], 163 | # }, 164 | 165 | # Although 'package_data' is the preferred approach, in some case you may 166 | # need to place data files outside of your packages. See: 167 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files 168 | # 169 | # In this case, 'data_file' will be installed into '/my_data' 170 | # data_files=[('my_data', ['data/data_file'])], # Optional 171 | 172 | # To provide executable scripts, use entry points in preference to the 173 | # "scripts" keyword. Entry points provide cross-platform support and allow 174 | # `pip` to create the appropriate form of executable for the target 175 | # platform. 176 | # 177 | # For example, the following would provide a command called `sample` which 178 | # executes the function `main` from this package when invoked: 179 | # entry_points={ # Optional 180 | # 'console_scripts': [ 181 | # 'sample=sample:main', 182 | # ], 183 | # }, 184 | 185 | # List additional URLs that are relevant to your project as a dict. 186 | # 187 | # This field corresponds to the "Project-URL" metadata fields: 188 | # https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use 189 | # 190 | # Examples listed include a pattern for specifying where the package tracks 191 | # issues, where the source is hosted, where to say thanks to the package 192 | # maintainers, and where to support the project financially. The key is 193 | # what's used to render the link text on PyPI. 194 | project_urls={ # Optional 195 | 'Bug Reports': 'https://github.com/peterdsharpe/PotentialFlowVisualizer/issues', 196 | 'Source' : 'https://github.com/peterdsharpe/PotentialFlowVisualizer', 197 | }, 198 | ) 199 | --------------------------------------------------------------------------------