├── pysces ├── tests │ ├── __init__.py │ ├── test_panel.py │ ├── test_flat_plate.py │ ├── test_timestepper.py │ ├── test_body.py │ ├── test_motion.py │ └── test_vortex.py ├── force.py ├── __init__.py ├── vortex.py ├── motion.py ├── timestepper.py ├── panel.py └── body.py ├── setup.cfg ├── .gitignore ├── runtests.py ├── doc-requirements.txt ├── .coveragerc ├── setup.py ├── scripts ├── plot_induced_vel.py ├── simple_panel.py ├── shed.py ├── single_vortex.py ├── plot_points.py ├── fixed_foil.py ├── flapping_foil.py ├── vortex_pair.py ├── thin_airfoil.py ├── animate_foil.py └── thin_airfoil_unsteady.py ├── benchmarks ├── bench_vortices.py ├── bench_fixed.py └── bench_flapping.py ├── doc ├── pysces.rst ├── index.rst ├── Makefile └── conf.py ├── .travis.yml └── README.rst /pysces/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /pysces/force.py: -------------------------------------------------------------------------------- 1 | __all__ = ['compute_forces'] 2 | 3 | def compute_forces(body, wake): 4 | return 0, 0 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | htmlcov 2 | .coverage 3 | *.pyc 4 | doc/_build 5 | doc/generated 6 | *.egg-info 7 | *.prof 8 | *.swp 9 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import unittest 4 | tests = unittest.defaultTestLoader.discover('pysces') 5 | runner = unittest.runner.TextTestRunner() 6 | runner.run(tests) 7 | -------------------------------------------------------------------------------- /doc-requirements.txt: -------------------------------------------------------------------------------- 1 | numpydoc==0.4 2 | mock==1.0.1 3 | # install sphinx from git to get patched version 4 | # (fixes bug in autosummary where tables do not display) 5 | -e git://github.com/sphinx-doc/sphinx.git@stable#egg=Sphinx-origin_stable 6 | -------------------------------------------------------------------------------- /pysces/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The :mod:`pysces` package implements a boundary element method for inviscid 3 | fluid flows. 4 | """ 5 | 6 | from .body import * 7 | from .panel import * 8 | from .force import * 9 | from .timestepper import * 10 | from .vortex import * 11 | 12 | __version__ = "0.1" 13 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = pysces 3 | omit = pysces/tests/* 4 | 5 | [report] 6 | exclude_lines = 7 | # Don't complain if tests don't hit defensive assertion code: 8 | raise AssertionError 9 | raise NotImplementedError 10 | 11 | # Don't complain if non-runnable code isn't run: 12 | if 0: 13 | if __name__ == .__main__.: 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='pysces', 5 | version='0.1', 6 | description='Boundary element solver', 7 | url='https://github.com/cwrowley/pysces', 8 | author='Clancy Rowley', 9 | author_email='cwrowley@princeton.edu', 10 | license='BSD', 11 | packages=['pysces'], 12 | install_requires=['numpy'], 13 | tests_require=['nose'], 14 | test_suite='nose.collector', 15 | ) 16 | -------------------------------------------------------------------------------- /scripts/plot_induced_vel.py: -------------------------------------------------------------------------------- 1 | from pysces import * 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | 5 | panels = Vortices() 6 | panels.core_radius = 0.01 7 | x = np.linspace(0,0.2,200) 8 | y = np.zeros_like(x) 9 | q = np.array([x,y]).T 10 | xvort = np.array([0,0]) 11 | vel = panels.induced_velocity_single(q, xvort, 1) 12 | plt.plot(x,vel[:,0], '-x', label="u") 13 | plt.plot(x,vel[:,1], '-+', label="v") 14 | plt.legend() 15 | plt.show() 16 | -------------------------------------------------------------------------------- /benchmarks/bench_vortices.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from pysces import Vortices 3 | from timeit import default_timer as timer 4 | 5 | n = 8192 6 | Vortices.core_radius = 0.01 7 | 8 | pos = np.array(np.random.rand(2*n), dtype=np.float32).reshape((n,2)) 9 | gamma = np.array(np.random.rand(n), dtype=np.float32) 10 | print("Computing induced velocity for %d vortices" % n) 11 | 12 | start = timer() 13 | vort = Vortices(pos, gamma) 14 | vel = vort.induced_velocity() 15 | elapsed = timer() - start 16 | 17 | print("Time: %f sec" % elapsed) 18 | -------------------------------------------------------------------------------- /scripts/simple_panel.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from pysces import * 3 | 4 | # x = [1,0] 5 | # y = [0,0] 6 | # x = [0,0,0] 7 | # y = [0,1,2] 8 | x = [2,1,0] 9 | y = [0,0,0] 10 | points = np.array([x, y]).T 11 | body = Body(points) 12 | body_panels = BoundVortices(body) 13 | body_panels.update_strengths() 14 | gam = body_panels.vortices.strengths 15 | xc = body_panels.collocation_pts 16 | Uinfty = np.array((1,0)) 17 | vel = body_panels.induced_velocity(xc) 18 | vel_dot_n = np.sum(vel * body_panels.normals, 0) 19 | print("gam:" + str(gam)) 20 | print("vel: " + str(vel)) 21 | print("vel . n:" + str(vel_dot_n)) 22 | -------------------------------------------------------------------------------- /doc/pysces.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | Function reference 3 | ==================== 4 | 5 | .. automodule:: pysces 6 | :no-members: 7 | :no-inherited-members: 8 | 9 | .. currentmodule:: pysces 10 | 11 | Generating bodies 12 | ================= 13 | .. autosummary:: 14 | :toctree: generated/ 15 | 16 | Body 17 | TransformedBody 18 | Pitching 19 | Heaving 20 | cylinder 21 | flat_plate 22 | joukowski_foil 23 | karman_trefftz_foil 24 | naca_airfoil 25 | van_de_vooren_foil 26 | 27 | Panel methods 28 | ============= 29 | .. autosummary:: 30 | :toctree: generated/ 31 | 32 | Vortices 33 | BoundVortices 34 | BoundSourceDoublets 35 | 36 | -------------------------------------------------------------------------------- /benchmarks/bench_fixed.py: -------------------------------------------------------------------------------- 1 | from pysces import * 2 | from timeit import default_timer as timer 3 | 4 | start = timer() 5 | airfoil = naca_airfoil("2412", 20) # NACA 2412 airfoil with 20 points per side 6 | airfoil = TransformedBody(airfoil, displacement=(-0.25, 0)) 7 | airfoil = TransformedBody(airfoil, angle=10) # rotate by 10 degrees about 1/4 chord 8 | bound = BoundVortices(airfoil) 9 | 10 | num_steps = 100 11 | Uinfty = (1,0) 12 | dt = 0.01 13 | Vortices.core_radius = dt 14 | 15 | stepper = RungeKutta2(dt, Uinfty, bound) 16 | 17 | print("Taking %d steps" % num_steps) 18 | for i in range(1,num_steps): 19 | stepper.advance() 20 | 21 | elapsed = timer() - start 22 | print("Time: %f sec" % elapsed) 23 | -------------------------------------------------------------------------------- /scripts/shed.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import numpy as np 3 | from pysces import * 4 | import matplotlib.pyplot as plt 5 | 6 | body = naca_airfoil("0012", 16) 7 | body = flat_plate(16) 8 | body = TransformedBody(body, angle=10) 9 | panels = BoundVortices(body) 10 | Uinfty = (1,0) 11 | dt = 1 12 | panels.update_strengths_unsteady(dt, Uinfty) 13 | 14 | gam = panels.vortices.strengths 15 | x_shed, gam_shed = panels.get_newly_shed() 16 | gam_sum = np.sum(gam) 17 | print("gam_shed = %f, bound circ = %f, total = %f" % (gam_shed, gam_sum, gam_shed+gam_sum)) 18 | 19 | # plot 20 | q = body.get_points() 21 | plt.plot(q[:,0], q[:,1], 'k-') 22 | plt.plot(x_shed[0], x_shed[1], 'bo') 23 | plt.grid(True) 24 | plt.axis('equal') 25 | plt.show() 26 | -------------------------------------------------------------------------------- /benchmarks/bench_flapping.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from pysces import * 3 | from timeit import default_timer as timer 4 | 5 | start = timer() 6 | airfoil = naca_airfoil("2412", 20) # NACA 2412 airfoil with 20 points per side 7 | airfoil = TransformedBody(airfoil, displacement=(-0.25, 0)) 8 | freq = 0.3 * 2 * np.pi 9 | airfoil = Pitching(airfoil, 10, freq, phase=90) 10 | airfoil = Heaving(airfoil, (0,0.2), freq, phase=0) 11 | bound = BoundVortices(airfoil) 12 | 13 | num_steps = 400 14 | Uinfty = (1,0) 15 | dt = 0.01 16 | Vortices.core_radius = dt 17 | 18 | stepper = RungeKutta2(dt, Uinfty, bound) 19 | 20 | print("Taking %d steps" % num_steps) 21 | for i in range(1,num_steps): 22 | stepper.advance() 23 | 24 | elapsed = timer() - start 25 | print("Time: %f sec" % elapsed) 26 | -------------------------------------------------------------------------------- /scripts/single_vortex.py: -------------------------------------------------------------------------------- 1 | from pysces import * 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | 5 | v1 = (0,0) 6 | v2 = (1,0) 7 | s1 = 2 * np.pi 8 | s2 = 0 9 | vort = Vortices([v1,v2],[s1,s2]) 10 | 11 | dt = 0.1 12 | Uinfty = (0,0) 13 | euler = ExplicitEuler(dt, Uinfty, wake=vort) 14 | rk2 = RungeKutta2(dt, Uinfty, wake=vort) 15 | rk4 = RungeKutta4(dt, Uinfty, wake=vort) 16 | 17 | num_steps = 200 18 | for i in range(1,num_steps): 19 | euler.advance() 20 | rk2.advance() 21 | rk4.advance() 22 | q1 = euler.wake.positions 23 | q2 = rk2.wake.positions 24 | q3 = rk4.wake.positions 25 | plt.plot(q1[:,0], q1[:,1], 'ro') 26 | plt.plot(q2[:,0], q2[:,1], 'bo') 27 | plt.plot(q3[:,0], q3[:,1], 'go') 28 | 29 | plt.axis('equal') 30 | plt.grid(True) 31 | plt.show() 32 | -------------------------------------------------------------------------------- /scripts/plot_points.py: -------------------------------------------------------------------------------- 1 | from pysces import * 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | 5 | body = naca_airfoil("2412", 12) 6 | body = TransformedBody(body, angle=10) 7 | 8 | # body = naca_airfoil("0012", 6) 9 | 10 | # body = cylinder(0.1, 13) 11 | 12 | # body = flat_plate(2) 13 | # body = TransformedBody(body, angle=10) 14 | 15 | body_panels = BoundVortices(body) 16 | 17 | vort = body_panels.vortices.positions 18 | coll = body_panels.collocation_pts 19 | norm = body_panels.normals 20 | foil = body.get_points(body_frame=True) 21 | plt.plot(foil[:,0], foil[:,1], 'k-+') 22 | plt.plot(vort[:,0], vort[:,1], 'ro', label="vortices") 23 | plt.plot(coll[:,0], coll[:,1], 'bx', label="collocation pts") 24 | plt.quiver(coll[:,0], coll[:,1], norm[:,0], norm[:,1]) 25 | plt.legend() 26 | plt.axis('equal') 27 | plt.grid(True) 28 | plt.show() 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | 8 | # install required system libraries 9 | before_install: 10 | # use miniconda to install numpy/scipy, to avoid lengthy build from source 11 | - wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh 12 | - bash miniconda.sh -b -p $HOME/miniconda 13 | - export PATH="$HOME/miniconda/bin:$PATH" 14 | - hash -r 15 | - conda config --set always_yes yes --set changeps1 no 16 | - conda update -q conda 17 | - conda create -n pyenv python=$TRAVIS_PYTHON_VERSION numpy coverage pip nose 18 | - source activate pyenv 19 | - conda info -a 20 | - pip install coveralls 21 | 22 | install: 23 | - python setup.py install 24 | 25 | script: 26 | - nosetests --with-cov --cover-package pysces 27 | 28 | after_success: 29 | - coveralls 30 | -------------------------------------------------------------------------------- /scripts/fixed_foil.py: -------------------------------------------------------------------------------- 1 | from pysces import * 2 | import matplotlib.pyplot as plt 3 | 4 | airfoil = naca_airfoil("2412", 20) # NACA 2412 airfoil with 20 points per side 5 | airfoil = TransformedBody(airfoil, displacement=(-0.25, 0)) 6 | airfoil = TransformedBody(airfoil, angle=10) # rotate by 10 degrees about 1/4 chord 7 | bound = BoundVortices(airfoil) 8 | 9 | num_steps = 100 10 | Uinfty = (1,0) 11 | dt = 0.01 12 | Vortices.core_radius = dt 13 | 14 | flow = RungeKutta2(dt, Uinfty, bound) 15 | 16 | for i in range(1,num_steps): 17 | flow.advance() 18 | 19 | vort = flow.wake.positions 20 | gam = flow.wake.strengths 21 | q = airfoil.get_points() 22 | plt.plot(q[:,0], q[:,1], 'k-') 23 | maxval = 0.01 24 | plt.scatter(vort[:,0], vort[:,1], c=gam, 25 | cmap='bwr', vmin=-maxval, vmax=maxval, edgecolor='none') 26 | plt.axis('equal') 27 | plt.grid(True) 28 | plt.show() 29 | -------------------------------------------------------------------------------- /scripts/flapping_foil.py: -------------------------------------------------------------------------------- 1 | from pysces import * 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | 5 | airfoil = naca_airfoil("0012", 50) # NACA 0012 airfoil with 20 points per side 6 | airfoil = TransformedBody(airfoil, displacement=(-0.25, 0)) 7 | freq = 0.3 * 2 * np.pi 8 | airfoil = Pitching(airfoil, 10, freq, phase=90) 9 | airfoil = Heaving(airfoil, (0,0.2), freq, phase=0) 10 | bound = BoundVortices(airfoil) 11 | 12 | num_steps = 400 13 | Uinfty = (1,0) 14 | dt = 0.01 15 | Vortices.core_radius = dt 16 | 17 | flow = ExplicitEuler(dt, Uinfty, bound) 18 | 19 | for i in range(1,num_steps): 20 | flow.advance() 21 | 22 | vort = flow.wake.positions 23 | gam = flow.wake.strengths 24 | q = airfoil.get_points() 25 | plt.plot(q[:,0], q[:,1], 'k-') 26 | maxval = dt 27 | plt.scatter(vort[:,0], vort[:,1], c=gam, 28 | cmap='bwr', vmin=-maxval, vmax=maxval, edgecolors='none') 29 | plt.axis('equal') 30 | plt.grid(True) 31 | plt.show() 32 | -------------------------------------------------------------------------------- /scripts/vortex_pair.py: -------------------------------------------------------------------------------- 1 | from pysces import * 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | 5 | v1 = (-2,0) 6 | v2 = (1,0) 7 | g1 = 2 * np.pi 8 | g2 = 4 * np.pi 9 | vort = Vortices([v1,v2],[g1,g2]) 10 | omega = 1./3 11 | 12 | dt = 0.2 13 | Uinfty = (0,0) 14 | euler = ExplicitEuler(dt, Uinfty, wake=vort) 15 | rk2 = RungeKutta2(dt, Uinfty, wake=vort) 16 | rk4 = RungeKutta4(dt, Uinfty, wake=vort) 17 | 18 | num_steps = 10 19 | t = dt * np.arange(num_steps) 20 | 21 | def exact(t): 22 | v = np.array([np.cos(omega * t), np.sin(omega * t)]) 23 | return np.array([-2 * v, v]) 24 | 25 | for i in range(1,num_steps): 26 | euler.advance() 27 | rk2.advance() 28 | rk4.advance() 29 | q1 = euler.wake.positions 30 | q2 = rk2.wake.positions 31 | q3 = rk4.wake.positions 32 | v = exact(t[i]) 33 | err = [np.sum((q - v) * (q - v)) for q in (q1, q2, q3)] 34 | print(err) 35 | plt.plot(q1[:,0], q1[:,1], 'r+') 36 | plt.plot(q2[:,0], q2[:,1], 'b+') 37 | plt.plot(q3[:,0], q3[:,1], 'g+') 38 | plt.plot(v[:,0], v[:,1], 'x') 39 | 40 | plt.axis('equal') 41 | plt.grid(True) 42 | plt.show() 43 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. pysces documentation master file, created by 2 | sphinx-quickstart on Wed Apr 29 11:19:33 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | pysces: Boundary Element Method for Python 7 | ========================================== 8 | 9 | This package implements a boundary element method for solving for the fluid flow 10 | around streamlined bodies moving in a potential flow. The goal is to provide a 11 | clean interface so that different methods may be easily swapped out, while being 12 | computationally efficient. 13 | 14 | .. rubric:: Testing and coverage 15 | 16 | Automated tests are provided, and may be run with:: 17 | 18 | $ python runtests.py 19 | 20 | You can also generate a coverage report as follows:: 21 | 22 | $ coverage run runtests.py 23 | $ coverage report -m 24 | 25 | To generate a nice html report, use:: 26 | 27 | $ coverage html 28 | 29 | and then open the files generated in the directory `htmlcov/`. 30 | 31 | Documentation 32 | ============= 33 | 34 | .. toctree:: 35 | :maxdepth: 2 36 | 37 | pysces 38 | 39 | Indices and tables 40 | ================== 41 | 42 | * :ref:`genindex` 43 | * :ref:`modindex` 44 | * :ref:`search` 45 | 46 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================================== 2 | pysces: a boundary element solver for python 3 | ============================================== 4 | 5 | .. image:: https://travis-ci.org/cwrowley/pysces.svg?branch=master 6 | :target: https://travis-ci.org/cwrowley/pysces 7 | 8 | .. image:: https://coveralls.io/repos/cwrowley/pysces/badge.svg?branch=master 9 | :target: https://coveralls.io/r/cwrowley/pysces 10 | 11 | This python package implements a boundary element method for solving for the 12 | fluid flow around streamlined bodies moving in a potential flow. 13 | 14 | Testing and coverage 15 | ==================== 16 | 17 | Automated tests are provided, and may be run with:: 18 | 19 | $ python runtests.py 20 | 21 | You can also generate a coverage report as follows:: 22 | 23 | $ coverage run runtests.py 24 | $ coverage report -m 25 | 26 | To generate a nice html report, use:: 27 | 28 | $ coverage html 29 | 30 | and then open the files generated in the directory ``htmlcov/``. 31 | 32 | Coverage may be installed by executing:: 33 | 34 | $ pip install coverage 35 | 36 | Documentation 37 | ============= 38 | 39 | The documentation is available online at http://pysces.readthedocs.org/ . 40 | Documentation is generated using `sphinx `_ and `numpydoc 41 | `_. Once these are installed, you can 42 | build the documentation as follows:: 43 | 44 | $ cd doc 45 | $ make html 46 | 47 | The generated documentation will then be in the directory ``doc/_build/html``. 48 | -------------------------------------------------------------------------------- /scripts/thin_airfoil.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import numpy as np 3 | from pysces import * 4 | import matplotlib.pyplot as plt 5 | 6 | def compute_gam(body): 7 | body = TransformedBody(body, angle=alpha_deg, displacement=(1,2)) 8 | panels = BoundVortices(body) 9 | panels.update_strengths() 10 | xvort, gam = panels.vortices.positions, panels.vortices.strengths 11 | q = body.get_points() 12 | ds = -np.linalg.norm(np.diff(q, axis=0), axis=1) 13 | dgam = gam / ds 14 | s = np.sqrt(xvort[:,0]**2 + xvort[:,1]**2) 15 | return s, dgam 16 | 17 | alpha_deg = 2 # degrees 18 | alpha = alpha_deg * np.pi / 180 19 | num_points = 32 20 | plate = flat_plate(num_points) 21 | airfoil = naca_airfoil("0001", num_points) 22 | 23 | s_plate, dgam_plate = compute_gam(plate) 24 | 25 | s_airfoil, dgam_airfoil = compute_gam(airfoil) 26 | # sum up vortices on top and bottom of airfoil 27 | half = dgam_airfoil.shape[0] // 2 28 | dgam_airfoil = dgam_airfoil[:half] + dgam_airfoil[-1:half-1:-1] 29 | s_airfoil = s_airfoil[:half] 30 | 31 | # exact distribution from thin airfoil theory: see Kuethe and Chow p123 32 | s = np.linspace(s_plate[-1],1,100) 33 | dgam_exact = 2 * alpha * np.sqrt((1-s) / s) 34 | 35 | plt.plot(s, dgam_exact, label="thin airfoil theory") 36 | plt.plot(s_plate, dgam_plate, 'x', label="computed, flat plate") 37 | plt.plot(s_airfoil, dgam_airfoil, '+', label="computed, NACA 0001") 38 | plt.xlabel('Arclength s') 39 | plt.ylabel(r'$\gamma = d\Gamma/ds$') 40 | plt.ylim([0,1]) 41 | plt.title('Comparison with thin airfoil theory, AoA = %.1f deg' % alpha_deg) 42 | plt.grid(True) 43 | plt.legend() 44 | plt.show() 45 | -------------------------------------------------------------------------------- /scripts/animate_foil.py: -------------------------------------------------------------------------------- 1 | from pysces import * 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | import matplotlib.animation as animation 5 | import sys 6 | 7 | #airfoil = naca_airfoil("0006", 20) # NACA 0012 airfoil with 20 points per side 8 | #airfoil = naca_airfoil("2214", 20) 9 | airfoil = joukowski_foil(-.1,.1,.5,100) 10 | #airfoil = van_de_vooren_foil(0.5, 0.1, 3) 11 | #airfoil = karman_trefftz_foil(-.1,.1,1,10,32) 12 | #airfoil = flat_plate(20) 13 | #airfoil = cylinder(1.0,50) 14 | 15 | #pts = airfoil.get_points() 16 | #plt.plot(pts[:,0],pts[:,1]) 17 | #plt.axis('equal') 18 | #plt.show() 19 | 20 | freq = 0.3 * 2*np.pi 21 | airfoil = Pitching(airfoil, 20, freq, phase=90) 22 | airfoil = Heaving(airfoil, (0,0.2), freq, phase=0) 23 | 24 | Uinfty = (1,0) 25 | bound = BoundVortices(airfoil, Uinfty) 26 | 27 | dt = 0.05 28 | flow = RungeKutta2(dt, Uinfty, bound) 29 | 30 | fig, ax = plt.subplots() 31 | ax.axis('equal') 32 | ax.axis([-3,5,-2,2]) 33 | #ax.axis([-4,5,-3,3]) 34 | ax.grid(True) 35 | q = airfoil.get_points() 36 | line, = ax.plot(q[:,0], q[:,1], '-k') 37 | maxval = dt 38 | pts = ax.scatter(0, 0, c=0, 39 | cmap='bwr', vmin=-maxval, vmax=maxval, edgecolors='none') 40 | 41 | def gen_points(): 42 | flow.initialize() 43 | num_steps = 200 44 | for i in range(num_steps): 45 | flow.advance() 46 | yield airfoil.get_points(), flow.wake.positions, flow.wake.strengths 47 | 48 | def redraw(data): 49 | q, xvort, gam = data 50 | line.set_data(q[:,0], q[:,1]) 51 | pts.set_offsets(np.array([xvort[:,0], xvort[:,1]]).T) 52 | pts.set_array(gam) 53 | 54 | movie = animation.FuncAnimation(fig, redraw, gen_points, interval=50) 55 | plt.show() 56 | -------------------------------------------------------------------------------- /pysces/tests/test_panel.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pysces.body import Body, naca_airfoil, flat_plate 3 | from pysces.panel import * 4 | import numpy as np 5 | 6 | class TestPanel(unittest.TestCase): 7 | def test_single_panel_aligned(self): 8 | x = [1, 0] 9 | y = [0, 0] 10 | points = np.vstack([x, y]) 11 | body = Body(points) 12 | body_panels = BoundVortices(body) 13 | body_panels.update_strengths() 14 | gam = body_panels.vortices.strengths 15 | self.assertEqual(gam, 0) 16 | 17 | def test_single_panel_normal(self): 18 | x = [0, 0] 19 | y = [0, 1] 20 | points = np.vstack([x, y]) 21 | body = Body(points) 22 | body_panels = BoundVortices(body) 23 | body_panels.update_strengths() 24 | gam = body_panels.vortices.strengths 25 | self.assertEqual(gam, -np.pi) 26 | 27 | def check_shed_vortex(self, body, wake_fac): 28 | panels = BoundVortices(body) 29 | Uinfty = (1,0) 30 | dt = 1 31 | panels.update_strengths_unsteady(dt, Uinfty, None, wake_fac=wake_fac) 32 | gam = panels.vortices.strengths 33 | x_shed, gam_shed = panels.get_newly_shed() 34 | gam_sum = np.sum(gam) 35 | self.assertAlmostEqual(gam_shed, -gam_sum) 36 | np.testing.assert_array_almost_equal(x_shed, (1 + wake_fac, 0)) 37 | 38 | def test_shed_vortex_thick(self): 39 | body = naca_airfoil("0012", 8) 40 | self.check_shed_vortex(body, 0.2) 41 | self.check_shed_vortex(body, 0.3) 42 | 43 | def test_shed_vortex_thin(self): 44 | body = flat_plate(8) 45 | self.check_shed_vortex(body, 0.2) 46 | self.check_shed_vortex(body, 0.3) 47 | 48 | def test_regularization(self): 49 | pass 50 | 51 | if __name__ == "__main__": 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /pysces/tests/test_flat_plate.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | from pysces import * 4 | 5 | class TestFlatPlate(unittest.TestCase): 6 | """Test unsteady flat plate problem for small angles of attack. 7 | 8 | The test compares with exact results from thin airfoil theory. 9 | The L2 norm of the computed solution relative to the exact solution 10 | must be below a given threshold.""" 11 | 12 | _threshold = 1.e-2 13 | 14 | def test_flat_plate(self): 15 | aoa_deg = [2,4,6] 16 | for aoa in aoa_deg: 17 | err_norm = self.flat_plate(aoa) 18 | success = True if (err_norm< self._threshold) else False 19 | self.assertTrue(success) 20 | 21 | def flat_plate(self, aoa_deg = 2.0): 22 | num_points = 20 23 | dt = .5 24 | Uinfty = (1,0) 25 | num_steps = 100 26 | 27 | aoa_rad = aoa_deg*np.pi/180 28 | body = TransformedBody(flat_plate(num_points), angle=aoa_deg) 29 | bound = BoundVortices(body) 30 | stepper = RungeKutta2(dt, Uinfty, bound) 31 | for i in range(num_steps): 32 | stepper.advance() 33 | vort = stepper.wake.positions 34 | s, dgam = self.compute_gam(body, stepper.bound.vortices) 35 | # Exact solution given on p.123 of Kuethe and Chow 36 | dgam_exact = 2 * aoa_rad * np.sqrt((1-s) / s) 37 | 38 | # Less accuracy at leading edge, so only compute along last half of 39 | # chord; since points advance from trailing edge to leading edge, we 40 | # want the first half of the array. 41 | n = len(s)//2 42 | err_mag = np.linalg.norm(dgam[0:n]-dgam_exact[0:n]) 43 | return err_mag 44 | 45 | def compute_gam(self, body, vort): 46 | q = body.get_points() 47 | gam = vort.strengths 48 | ds = -np.linalg.norm(np.diff(q, axis=0), axis=1) 49 | dgam = gam / ds 50 | xvort = vort.positions 51 | s = np.sqrt(xvort[:,0]**2 + xvort[:,1]**2) 52 | return s, dgam 53 | 54 | if __name__ == '__main__': 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /scripts/thin_airfoil_unsteady.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | import numpy as np 3 | from pysces import * 4 | import matplotlib.pyplot as plt 5 | 6 | alpha_deg = 2 # degrees 7 | alpha = alpha_deg * np.pi / 180 8 | 9 | def compute_gam(body, vort): 10 | q = body.get_points() 11 | gam = vort.strengths 12 | ds = -np.linalg.norm(np.diff(q, axis=0), axis=1) 13 | dgam = gam / ds 14 | xvort = vort.positions 15 | s = np.sqrt(xvort[:,0]**2 + xvort[:,1]**2) 16 | return s, gam, dgam 17 | 18 | num_points = 32 19 | body = TransformedBody(flat_plate(num_points), angle=alpha_deg) 20 | #body = TransformedBody(naca_airfoil('0012',num_points), angle=alpha_deg) 21 | bound = BoundVortices(body) 22 | 23 | # unsteady simulation 24 | dt = 0.5 25 | Uinfty = (1,0) 26 | num_steps = 100 27 | stepper = RungeKutta2(dt, Uinfty, bound) 28 | 29 | for i in range(num_steps): 30 | stepper.advance() 31 | 32 | print("Using %s timestepper" % stepper.__class__.__name__) 33 | print("Total circulation: %f" % stepper.wake.circulation) 34 | print("After %d steps, last shed vortex has strength: %f" % 35 | (num_steps, stepper.wake.strengths[-1])) 36 | print("Ratio of last vortex strength to circulation: %f" % 37 | (stepper.wake.strengths[-1] / stepper.wake.circulation)) 38 | s, gam, dgam = compute_gam(body, stepper.bound.vortices) 39 | 40 | pts = body.get_points() 41 | mid = pts[:-1]+0.5*np.diff(pts, axis=0) 42 | xmid = mid[:,0] 43 | rho = 1 44 | qinf = 0.5*rho*(Uinfty[0]**2+Uinfty[1]**2) 45 | # See Kuethe and Chow for the following pressure coefficient formula 46 | cp = -rho*np.linalg.norm(Uinfty)*gam/qinf 47 | print('xmid',xmid) 48 | print('cp',cp) 49 | plt.subplot(211) 50 | plt.plot(xmid,cp,'x') 51 | plt.xlabel('Distance along chord') 52 | plt.ylabel('$C_p$') 53 | plt.title('Computed coefficient of pressure, AoA = %.1f deg' % alpha_deg) 54 | 55 | # Exact solution given section 5.4 of Katz/Plotkin, 2nd Ed. 56 | s1 = np.linspace(s[-1],1,100) 57 | dgam_exact = 2 * alpha * np.linalg.norm(Uinfty) * np.sqrt((1-s1) / s1) 58 | plt.subplot(212) 59 | plt.plot(s1, dgam_exact, label="thin airfoil theory") 60 | plt.plot(s, dgam, 's', label="computed, flat plate") 61 | plt.xlabel('Arclength s') 62 | plt.ylabel(r'$\gamma = d\Gamma/ds$') 63 | plt.title('Comparison with thin airfoil theory, AoA = %.1f deg' % alpha_deg) 64 | plt.grid(True) 65 | plt.legend() 66 | plt.show() 67 | -------------------------------------------------------------------------------- /pysces/tests/test_timestepper.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | from pysces.timestepper import * 4 | from pysces.body import flat_plate 5 | from pysces.panel import BoundVortices 6 | from pysces.vortex import Vortices 7 | import numpy as np 8 | from numpy.testing import assert_array_equal, assert_array_almost_equal 9 | 10 | class TestTimestepper(unittest.TestCase): 11 | def check_timestepper(self, timestepper_cls): 12 | body = flat_plate(20) 13 | bound = BoundVortices(body) 14 | Uinfty = (1,0) 15 | dt = 0.1 16 | flow = timestepper_cls(dt, Uinfty, bound) 17 | self.assertEqual(flow.time, 0) 18 | self.assertEqual(len(flow.wake), 1) 19 | vort = flow.bound.vortices 20 | wake = flow.wake 21 | self.assertEqual(vort.circulation, -wake.circulation) 22 | flow.advance() 23 | self.assertEqual(flow.time, dt) 24 | self.assertEqual(len(wake), 2) 25 | self.assertEqual(vort.circulation, -wake.circulation) 26 | 27 | def test_euler(self): 28 | self.check_timestepper(ExplicitEuler) 29 | 30 | def test_rk2(self): 31 | self.check_timestepper(RungeKutta2) 32 | 33 | def test_rk4(self): 34 | self.check_timestepper(RungeKutta4) 35 | 36 | def check_vortex_pair(self, cls, tol): 37 | # compare with exact solution for a pair of vortices: 38 | # uniform rotation at frequency omega about center of vorticity (here 0) 39 | dt = 0.2 40 | Uinfty = (0,0) 41 | v1 = (-2,0) 42 | v2 = (1,0) 43 | g1 = 2 * np.pi 44 | g2 = 4 * np.pi 45 | vort = Vortices([v1,v2],[g1,g2]) 46 | omega = 1./3 47 | stepper = cls(dt, Uinfty, wake=vort) 48 | num_steps = 10 49 | t = dt * num_steps 50 | v = np.array([np.cos(omega * t), np.sin(omega * t)]) 51 | exact = np.array([-2 * v, v]) 52 | for i in range(num_steps): 53 | stepper.advance() 54 | q = stepper.wake.positions 55 | err = np.sum((q - exact) * (q - exact)) 56 | if sys.version_info >= (2, 7): 57 | self.assertLess(err, tol) 58 | else: 59 | # assertLess not available in Python 2.6 60 | self.assertTrue(err < tol) 61 | 62 | def test_vortex_pair_euler(self): 63 | tol = 0.07 64 | self.check_vortex_pair(ExplicitEuler, tol) 65 | 66 | def test_vortex_pair_rk2(self): 67 | tol = 1.6e-5 68 | self.check_vortex_pair(RungeKutta2, tol) 69 | 70 | def test_vortex_pair_rk4(self): 71 | tol = 6.4e-10 72 | self.check_vortex_pair(RungeKutta4, tol) 73 | 74 | if __name__ == "__main__": 75 | unittest.main() 76 | -------------------------------------------------------------------------------- /pysces/tests/test_body.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pysces.body import * 3 | import numpy as np 4 | from numpy.testing import assert_array_equal, assert_array_almost_equal 5 | 6 | class TestFactories(unittest.TestCase): 7 | def test_cylinder(self): 8 | num_points = 20 9 | body = cylinder(1, num_points) 10 | points = body.get_points() 11 | self.assertEqual(len(points), 20) 12 | 13 | def test_airfoil_invalid(self): 14 | num_points = 20 15 | self.assertRaises(ValueError, naca_airfoil, "10012", num_points) 16 | 17 | def test_airfoil_uniform(self): 18 | # just some quick tests to cover code 19 | npoints = 20 20 | body = naca_airfoil("0012", npoints, uniform=True) 21 | self.assertEqual(len(body.get_points()), 2*npoints - 1) 22 | body2 = naca_airfoil("2412", npoints, zero_thick_te=True) 23 | self.assertEqual(len(body2.get_points()), 2*npoints - 1) 24 | 25 | def test_joukowski_foil(self): 26 | npts = 32 27 | body = joukowski_foil(numpoints=npts) 28 | self.assertEqual(len(body.get_points()),npts) 29 | 30 | def test_karman_trefftz_foil(self): 31 | npts = 32 32 | body = karman_trefftz_foil(numpoints=npts) 33 | self.assertEqual(len(body.get_points()),npts) 34 | 35 | def test_van_de_vooren_foil(self): 36 | npts = 32 37 | body = van_de_vooren_foil(numpoints=npts) 38 | self.assertEqual(len(body.get_points()),npts) 39 | 40 | class TestBody(unittest.TestCase): 41 | def setUp(self): 42 | x1 = (0,0) 43 | x2 = (1,0) 44 | self.x = np.array([x1, x2]) 45 | self.body = Body(self.x) 46 | 47 | def test_body(self): 48 | assert_array_equal(self.body.get_points(), self.x) 49 | 50 | def test_translation(self): 51 | dx = np.array((0,1)) 52 | body = TransformedBody(self.body, displacement=(0,1)) 53 | assert_array_equal(body.get_points(), self.x + dx) 54 | assert_array_equal(body.get_points(body_frame=True), self.x) 55 | self.assertEqual(body.get_body(), self.body) 56 | 57 | def test_rotation(self): 58 | body = TransformedBody(self.body, angle=90) 59 | assert_array_almost_equal(body.get_points(), [[0,0],[0,-1]]) 60 | assert_array_equal(body.get_points(body_frame=True), self.x) 61 | 62 | def test_pitching(self): 63 | body = Pitching(self.body, 90, 2*np.pi, 0) 64 | body.time = 0 65 | assert_array_equal(body.get_points(), self.x) 66 | body.time = 0.25 67 | assert_array_almost_equal(body.get_points(), [[0,0],[0,-1]]) 68 | body.time = 0 69 | assert_array_equal(body.get_points(), self.x) 70 | 71 | def test_heaving(self): 72 | body = Heaving(self.body, (0,1), 2*np.pi, 0) 73 | body.time = 0 74 | assert_array_equal(body.get_points(), self.x) 75 | body.time = 0.25 76 | assert_array_equal(body.get_points(), [[0,1],[1,1]]) 77 | 78 | def test_composition(self): 79 | new_body = TransformedBody(self.body, displacement=(-1,0)) 80 | new_body = TransformedBody(new_body, angle=45) 81 | self.assertEqual(new_body.get_body(), self.body) 82 | -------------------------------------------------------------------------------- /pysces/tests/test_motion.py: -------------------------------------------------------------------------------- 1 | from pysces.motion import * 2 | import unittest 3 | import numpy as np 4 | 5 | class TestRigidMotion(unittest.TestCase): 6 | def assert_motion_almost_equal(self, g, h): 7 | np.testing.assert_almost_equal(g.theta, h.theta) 8 | np.testing.assert_array_almost_equal(g.x, h.x) 9 | np.testing.assert_almost_equal(g.thetadot, h.thetadot) 10 | np.testing.assert_array_almost_equal(g.xdot, h.xdot) 11 | 12 | def test_identity_action(self): 13 | e = RigidMotion(0, (0,0)) 14 | q = (13,42) 15 | np.testing.assert_array_equal(q, e.map_position(q)) 16 | 17 | def test_equality(self): 18 | g1 = RigidMotion(1, (2,3)) 19 | g2 = RigidMotion(1, (2,3)) 20 | self.assertEqual(g1, g2) 21 | g3 = RigidMotion(1, (2,3), thetadot=1) 22 | self.assertNotEqual(g1, g3) 23 | g4 = RigidMotion(1, (2,3), xdot=(1,0)) 24 | self.assertNotEqual(g1, g4) 25 | g5 = RigidMotion(0, (2,3)) 26 | self.assertNotEqual(g1, g5) 27 | g6 = RigidMotion(1, (2,4)) 28 | self.assertNotEqual(g1, g6) 29 | 30 | def test_identity(self): 31 | e = RigidMotion(0, (0,0)) 32 | self.assertEqual(e, RigidMotion.identity()) 33 | 34 | def test_compose(self): 35 | g1 = RigidMotion(np.pi/2, (0,0)) 36 | g2 = RigidMotion(0, (2,3)) 37 | g3 = RigidMotion(np.pi/2, (2,3)) 38 | self.assertEqual(g2.compose(g1), g3) 39 | g4 = RigidMotion(np.pi/2, (-3,2)) 40 | self.assert_motion_almost_equal(g1.compose(g2), g4) 41 | 42 | def test_inverse(self): 43 | e = RigidMotion.identity() 44 | g1 = RigidMotion(1, (0,0)) 45 | g1inv = RigidMotion(-1, (0,0)) 46 | self.assertEqual(g1.inverse(), g1inv) 47 | 48 | g2 = RigidMotion(0, (2,3)) 49 | g2inv = RigidMotion(0, (-2,-3)) 50 | self.assertEqual(g2.inverse(), g2inv) 51 | 52 | g3 = RigidMotion(1,(2,3)) 53 | g3inv = g1inv.compose(g2inv) 54 | self.assertEqual(g3.inverse(), g3inv) 55 | 56 | def test_map_position(self): 57 | rot = RigidMotion(np.pi/2, (1,2)) 58 | x = (13, 42) 59 | y = (-41, 15) 60 | np.testing.assert_array_almost_equal(rot.map_position(x), y) 61 | 62 | xarr = np.array([x,x]) 63 | yarr = np.array([y,y]) 64 | np.testing.assert_array_almost_equal(rot.map_position(xarr), yarr) 65 | 66 | def test_map_velocity_inplace(self): 67 | rot = RigidMotion(0, (0,0), 3, (0,0)) 68 | x = np.array((1, 0)) 69 | v = np.array((0, 3)) 70 | x2 = np.array((0, 1)) 71 | v2 = np.array((-3, 0)) 72 | np.testing.assert_array_almost_equal(rot.map_velocity(x), v) 73 | np.testing.assert_array_almost_equal(rot.map_velocity(2*x), 2*v) 74 | np.testing.assert_array_almost_equal(rot.map_velocity(x2), v2) 75 | np.testing.assert_array_almost_equal(rot.map_velocity(2*x2), 2*v2) 76 | 77 | vel = np.array((4,5)) 78 | rot2 = RigidMotion(0, (0,0), 3, vel) 79 | np.testing.assert_array_almost_equal(rot2.map_velocity(x), v + vel) 80 | np.testing.assert_array_almost_equal(rot2.map_velocity(2*x), 2*v + vel) 81 | np.testing.assert_array_almost_equal(rot2.map_velocity(x2), v2 + vel) 82 | np.testing.assert_array_almost_equal(rot2.map_velocity(2*x2), 2*v2 + vel) 83 | 84 | def test_map_velocity_rot(self): 85 | rot = RigidMotion(np.pi/2, (42,13), 3, (0,0)) 86 | x = np.array((1, 0)) 87 | v = np.array((-3, 0)) 88 | x2 = np.array((0, 1)) 89 | v2 = np.array((0, -3)) 90 | np.testing.assert_array_almost_equal(rot.map_velocity(x), v) 91 | np.testing.assert_array_almost_equal(rot.map_velocity(2*x), 2*v) 92 | np.testing.assert_array_almost_equal(rot.map_velocity(x2), v2) 93 | np.testing.assert_array_almost_equal(rot.map_velocity(2*x2), 2*v2) 94 | 95 | vel = np.array((4,5)) 96 | rot2 = RigidMotion(np.pi/2, (42,13), 3, vel) 97 | np.testing.assert_array_almost_equal(rot2.map_velocity(x), v + vel) 98 | np.testing.assert_array_almost_equal(rot2.map_velocity(2*x), 2*v + vel) 99 | np.testing.assert_array_almost_equal(rot2.map_velocity(x2), v2 + vel) 100 | np.testing.assert_array_almost_equal(rot2.map_velocity(2*x2), 2*v2 + vel) 101 | -------------------------------------------------------------------------------- /pysces/vortex.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | __all__ = ['Vortices'] 4 | 5 | class Vortices(object): 6 | core_radius = 1.e-3 7 | 8 | def __init__(self, positions=None, strengths=None): 9 | if positions is None: 10 | self._positions = None 11 | else: 12 | self._positions = np.array(positions, ndmin=2, dtype=np.float64) 13 | 14 | if strengths is None: 15 | self._circulation = 0 16 | if positions is None: 17 | self._strengths = None 18 | else: 19 | self._strengths = np.zeros(self._positions.shape[0], 20 | dtype=np.float64) 21 | else: 22 | self._strengths = np.array(strengths, ndmin=1, dtype=np.float64) 23 | self._circulation = np.sum(self._strengths) 24 | 25 | @property 26 | def positions(self): 27 | return self._positions 28 | 29 | @positions.setter 30 | def positions(self, value): 31 | self._positions = np.array(value, dtype=np.float64) 32 | 33 | @property 34 | def strengths(self): 35 | return self._strengths 36 | 37 | @strengths.setter 38 | def strengths(self, value): 39 | strengths = np.array(value, ndmin=1, dtype=np.float64) 40 | self._strengths = strengths 41 | self._circulation = np.sum(strengths) 42 | 43 | @property 44 | def circulation(self): 45 | return self._circulation 46 | 47 | def __len__(self): 48 | if self._positions is None: 49 | return 0 50 | return self._positions.shape[0] 51 | 52 | def __iter__(self): 53 | if self._positions is None: 54 | return iter([]) 55 | return iter(zip(self._positions, self._strengths)) 56 | 57 | def append(self, position, strength): 58 | position = np.array(position, ndmin=2) 59 | strength = np.array(strength, ndmin=1) 60 | if self._positions is None: 61 | self._positions = position 62 | self._strengths = strength 63 | self._circulation = np.sum(strength) 64 | else: 65 | self._positions = np.append(self._positions, position, axis=0) 66 | self._strengths = np.append(self._strengths, strength) 67 | self._circulation += np.sum(strength) 68 | 69 | def induced_velocity_single(self, x, xvort, gam): 70 | r"""Compute velocity induced at points x by a single vortex 71 | 72 | This method returns a vector of velocities at the points x induced 73 | by a single vortex of strength gam located at xvort. 74 | 75 | Parameters 76 | ---------- 77 | x : 2d array 78 | Locations at which to compute induced velocity. Expressed as 79 | column vectors (i.e., shape should be (n,2)) 80 | xvort : 1d array 81 | Location of vortex (shape should be (2,)) 82 | gam : float 83 | Strength of vortex 84 | 85 | Notes 86 | ----- 87 | Induced velocity is 88 | 89 | .. math:: u_\theta = \frac{\Gamma}{2 \pi r} 90 | 91 | where r is the distance between the point and the vortex. If this 92 | distance is less than :class:`core_radius` :math:`r_0`, the velocity is 93 | regularized as solid-body rotation, with 94 | 95 | .. math:: u_\theta = \frac{\Gamma r}{2\pi r_0^2} 96 | """ 97 | r = np.array(x, ndmin=2) - np.array(xvort) 98 | rsq = np.maximum(np.sum(r * r, 1), self.core_radius**2) 99 | # alternative regularization (Krasny, Eldredge) 100 | #rsq = np.sum(r * r, 1) + self.core_radius**2 101 | vel = np.transpose(np.array([-r[:,1], r[:,0]])) 102 | vel = gam / (2 * np.pi) * vel / rsq[:,np.newaxis] 103 | return np.squeeze(vel) 104 | 105 | def induced_velocity(self, x=None, motion=None): 106 | """Compute the induced velocity at the given point(s)""" 107 | if motion is None: 108 | positions = self._positions 109 | else: 110 | positions = motion.map_position(self._positions) 111 | if x is None: 112 | x = self._positions 113 | else: 114 | x = np.array(x) 115 | vel = np.zeros_like(x, dtype=np.float64) 116 | for xvort, gam in zip(positions, self._strengths): 117 | vel += self.induced_velocity_single(x, xvort, gam) 118 | return vel 119 | -------------------------------------------------------------------------------- /pysces/motion.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | __all__ = ['RigidMotion'] 4 | 5 | class RigidMotion(object): 6 | """A class representing rigid body motions, elements of TSE(2)""" 7 | 8 | def __init__(self, theta, x, thetadot=0, xdot=(0,0)): 9 | """angles are in radians""" 10 | self._theta = theta 11 | self._x = np.array(x, dtype="float64") 12 | self._thetadot = thetadot 13 | self._xdot = np.array(xdot, dtype="float64") 14 | self._update() 15 | 16 | def __repr__(self): 17 | if self._thetadot or self._xdot.any(): 18 | return ("RigidMotion(%s, (%s, %s), %s, (%s, %s))" % 19 | tuple(map(str, (self._theta, self._x[0], self._x[1], 20 | self._thetadot, self._xdot[0], self._xdot[1])))) 21 | else: 22 | return ("RigidMotion(%s, (%s, %s))" % 23 | tuple(map(str, (self._theta, self._x[0], self._x[1])))) 24 | 25 | def __str__(self): 26 | return self.__repr__() 27 | 28 | @property 29 | def theta(self): 30 | return self._theta 31 | 32 | @theta.setter 33 | def theta(self, value): 34 | self._theta = value 35 | self._update() 36 | 37 | def _update(self): 38 | c = np.cos(self._theta) 39 | s = np.sin(self._theta) 40 | self._R = np.array([[c, -s], [s, c]]) 41 | self._Rdot = np.array([[-s, -c], [c, -s]]) * self._thetadot 42 | 43 | @property 44 | def x(self): 45 | return self._x 46 | 47 | @x.setter 48 | def x(self, value): 49 | self._x = np.array(value, dtype="float64") 50 | 51 | @property 52 | def thetadot(self): 53 | return self._thetadot 54 | 55 | @property 56 | def xdot(self): 57 | return self._xdot 58 | 59 | @classmethod 60 | def identity(cls): 61 | return cls(0, (0,0)) 62 | 63 | def __eq__(self, other): 64 | return (np.array_equal(self._x, other._x) and 65 | np.array_equal(self._xdot, other._xdot) and 66 | self._theta == other._theta and 67 | self._thetadot == other._thetadot) 68 | 69 | def __ne__(self, other): 70 | return not self == other 71 | 72 | def inverse(self): 73 | """Return the inverse element""" 74 | xinv = -np.dot(self._R.T, self._x) 75 | xdotinv = -np.dot(self._Rdot.T, self._x) - np.dot(self._R.T, self._xdot) 76 | return RigidMotion(-self._theta, xinv, -self._thetadot, xdotinv) 77 | 78 | def compose(self, other): 79 | """Return the composition of self (left) with other (right) 80 | 81 | If g1 = (R1, v1) 82 | g2 = (R2, v2) 83 | where R1, R2 are rotation matrices and v1, v2 are displacement vectors, 84 | then 85 | g1 g2 = (R1 R2, R1 v2 + v1) 86 | """ 87 | if other is None: 88 | return self 89 | theta = self._theta + other._theta 90 | x = np.dot(self._R, other._x) + self._x 91 | thetadot = self._thetadot + other._thetadot 92 | xdot = (np.dot(self._Rdot, other._x) + np.dot(self._R, other._xdot) + 93 | self._xdot) 94 | return RigidMotion(theta, x, thetadot, xdot) 95 | 96 | def map_position(self, q): 97 | """Return the action of the transformation on the given vector(s) q 98 | 99 | `q` can be an array of length 2 or a 2d-array with 2 rows 100 | 101 | The group action of the element (R, x) is given by 102 | q -> R . q + x 103 | """ 104 | if self._theta: 105 | q_new = np.dot(q, np.transpose(self._R)) 106 | else: 107 | q_new = np.array(q, copy=True) 108 | if self._x.any(): 109 | q_new += self._x 110 | # if q.ndim == 1: 111 | # q_new += self._x 112 | # else: 113 | # q_new += self._x[:, np.newaxis] 114 | return q_new 115 | 116 | def map_vector(self, qdot): 117 | """Return the action of the transformation on the tangent vector(s) qdot 118 | 119 | `qdot` can be an array of length 2 or a 2d-array with 2 rows 120 | 121 | The tangent action is given by 122 | qdot -> R . qdot 123 | """ 124 | if self._theta: 125 | return np.dot(qdot, np.transpose(self._R)) 126 | else: 127 | return np.array(qdot, copy=True) 128 | 129 | def map_velocity(self, q, qdot=None): 130 | """Return the velocity of the transformed base point q, velocity qdot 131 | 132 | If transformation is (R, x), then 133 | d/dt (R, x) q = Rdot q + R qdot + xdot 134 | """ 135 | qdot_new = np.zeros_like(q, dtype="float64") 136 | if self._thetadot: 137 | qdot_new += np.dot(q, np.transpose(self._Rdot)) 138 | if self._theta and qdot is not None and qdot.any(): 139 | qdot_new += np.dot(qdot, np.transpose(self._R)) 140 | if self._xdot.any(): 141 | qdot_new += self._xdot 142 | # if q.ndim == 1: 143 | # qdot_new += self._xdot 144 | # else: 145 | # qdot_new += self._xdot[:, np.newaxis] 146 | return qdot_new 147 | -------------------------------------------------------------------------------- /pysces/tests/test_vortex.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pysces.vortex import * 3 | import numpy as np 4 | from numpy.testing import assert_array_equal, assert_array_almost_equal 5 | from pysces.motion import RigidMotion 6 | 7 | class TestVortex(unittest.TestCase): 8 | def test_init_empty(self): 9 | vort = Vortices() 10 | self.assertEqual(len(vort), 0) 11 | 12 | def check_vortices(self, vort, pos, gam=None): 13 | n = len(pos) 14 | self.assertEqual(len(vort), n) 15 | for i in range(n): 16 | assert_array_equal(vort.positions[i], pos[i]) 17 | if gam is None: 18 | self.assertEqual(vort.strengths[i], 0) 19 | else: 20 | self.assertEqual(vort.strengths[i], gam[i]) 21 | 22 | def test_init_tuple(self): 23 | v1 = (0,0) 24 | vort = Vortices(v1) 25 | self.check_vortices(vort, [v1]) 26 | 27 | def test_init_array1(self): 28 | v1 = np.array((0,0)) 29 | vort = Vortices(v1) 30 | self.check_vortices(vort, [v1]) 31 | 32 | def test_init_array2(self): 33 | v1 = (-1,0) 34 | v2 = (1,0) 35 | vort = Vortices(np.array([v1, v2])) 36 | self.check_vortices(vort, [v1, v2]) 37 | 38 | def test_init_list(self): 39 | v1 = (-1,0) 40 | v2 = (1,0) 41 | vort = Vortices([v1, v2]) 42 | self.check_vortices(vort, [v1, v2]) 43 | 44 | def test_init_strength(self): 45 | v1 = (0,0) 46 | s1 = 1 47 | vort = Vortices([v1], [s1]) 48 | self.check_vortices(vort, [v1], [s1]) 49 | 50 | def test_init_strength_list(self): 51 | v1 = (-1,0) 52 | v2 = (1,0) 53 | g1 = 2 54 | g2 = 3 55 | pos = [v1, v2] 56 | gam = [g1, g2] 57 | vort = Vortices(pos, gam) 58 | self.check_vortices(vort, pos, gam) 59 | 60 | def test_append(self): 61 | v1 = (-1,0) 62 | v2 = (1,0) 63 | g1 = 2 64 | g2 = 3 65 | vort = Vortices([v1], [g1]) 66 | self.check_vortices(vort, [v1], [g1]) 67 | vort.append(v2, g2) 68 | self.check_vortices(vort, [v1,v2], [g1,g2]) 69 | 70 | def test_append_empty(self): 71 | vort = Vortices() 72 | v = (0,0) 73 | s = 1 74 | vort.append(v, s) 75 | self.check_vortices(vort, [v], [s]) 76 | 77 | def test_iter(self): 78 | v1 = (-1,0) 79 | v2 = (1,0) 80 | pos = [v1, v2] 81 | gam = [1,2] 82 | vort = Vortices(pos, gam) 83 | for i, v in enumerate(vort): 84 | assert_array_equal(v[0], pos[i]) 85 | self.assertEqual(v[1], gam[i]) 86 | 87 | def test_iter_empty(self): 88 | for x, gam in Vortices(): 89 | pass 90 | 91 | def test_circulation(self): 92 | pos = [(0,0), (1,0)] 93 | gam = [42, -13] 94 | vort = Vortices(pos, gam) 95 | self.assertEqual(vort.circulation, 42 - 13) 96 | 97 | def test_core_radius(self): 98 | vort = Vortices() 99 | default_rad = 1.e-3 100 | self.assertEqual(vort.core_radius, default_rad) 101 | vort.core_radius = 0.5 102 | self.assertEqual(vort.core_radius, 0.5) 103 | Vortices.core_radius = 0.2 104 | vort2 = Vortices() 105 | self.assertEqual(vort2.core_radius, 0.2) 106 | 107 | def test_induced_velocity_single_tuple(self): 108 | v = (2,0) 109 | x = (3,0) 110 | gam = 2 * np.pi 111 | vel = Vortices().induced_velocity_single(x, v, gam) 112 | assert_array_equal(vel, (0,1)) 113 | 114 | def test_induced_velocity_single_array(self): 115 | v = (0,0) 116 | x = np.array([(1,0), (2,0), (3,0)]) 117 | gam = 2 * np.pi 118 | vel = Vortices().induced_velocity_single(x, v, gam) 119 | vel_expected = np.array([(0, 1), (0, 1./2), (0, 1./3)]) 120 | assert_array_equal(vel, vel_expected) 121 | 122 | def test_induced_velocity_tuple(self): 123 | v1 = (-1,0) 124 | v2 = (1,0) 125 | g1 = 2 * np.pi 126 | g2 = -2 * np.pi 127 | vort = Vortices([v1, v2], [g1, g2]) 128 | x = (0,0) 129 | vel = vort.induced_velocity(x) 130 | vel_expected = (0,2) 131 | assert_array_equal(vel, vel_expected) 132 | 133 | def test_induced_velocity_array(self): 134 | v1 = (-1,0) 135 | v2 = (1,0) 136 | g1 = 2 * np.pi 137 | g2 = -2 * np.pi 138 | vort = Vortices([v1, v2], [g1, g2]) 139 | x = np.array([(0,-1), (0,0), (0,1)]) 140 | vel = vort.induced_velocity(x) 141 | vel_expected = np.array([(0,1), (0,2), (0,1)]) 142 | assert_array_equal(vel, vel_expected) 143 | 144 | def test_induced_velocity_motion(self): 145 | v = (1,0) 146 | vort = Vortices(v, 2 * np.pi) 147 | motion = RigidMotion(np.pi/2, (0,0)) 148 | x = (0,0) 149 | vel = vort.induced_velocity(x, motion=motion) 150 | vel_expected = (1,0) 151 | assert_array_almost_equal(vel, vel_expected) 152 | 153 | def test_regularization(self): 154 | eps = 1.e-2 155 | vort = Vortices((0,0), 2 * np.pi) 156 | vort.core_radius = eps 157 | x0 = np.array((eps,0)) 158 | x = np.array([0.5 * x0, x0, 2 * x0]) 159 | vel = vort.induced_velocity(x) 160 | vel_expected = np.array([(0,0.5/eps), (0,1./eps), (0,0.5/eps)]) 161 | assert_array_equal(vel, vel_expected) 162 | -------------------------------------------------------------------------------- /pysces/timestepper.py: -------------------------------------------------------------------------------- 1 | """A module to easily set up and manage a simulation""" 2 | import numpy as np 3 | from .vortex import Vortices 4 | 5 | __all__ = ['ExplicitEuler', 'RungeKutta2', 'RungeKutta4'] 6 | 7 | class Timestepper(object): 8 | """Base class for timesteppers for unsteady boundary element simulation""" 9 | 10 | def __init__(self, dt, Uinfty=(1,0), bound=None, wake=None): 11 | """Initialize a simulation""" 12 | self._dt = dt 13 | self._Uinfty = np.array(Uinfty) 14 | self._bound = bound 15 | self._has_body = (bound is not None) 16 | self.initialize(wake) 17 | 18 | def initialize(self, wake=None): 19 | """Initialize a timestepper 20 | 21 | Solve for panel strengths so that surface boundary conditions are 22 | satisfied, and shed a particle into the wake so that overall circulation 23 | is zero. 24 | 25 | """ 26 | self._time = 0 27 | if wake is None: 28 | self._wake = Vortices() 29 | else: 30 | self._wake = Vortices(wake.positions, wake.strengths) 31 | 32 | if self._has_body: 33 | self._bound.time = 0 34 | self._bound.update_strengths_unsteady(self._dt, self._Uinfty) 35 | self._wake.append(*self._bound.get_newly_shed()) 36 | 37 | def advance(self, dt=None): 38 | """Advance the simulation for one timestep""" 39 | if not dt: 40 | dt = self._dt 41 | x = self._wake.positions 42 | self._advance(x, dt) # defer to subclass 43 | 44 | @property 45 | def time(self): 46 | """Current simulation time""" 47 | return self._time 48 | 49 | @property 50 | def bound(self): 51 | """Body panels used in the simulation""" 52 | return self._bound 53 | 54 | @property 55 | def wake(self): 56 | """Wake vortices used in the simulation""" 57 | return self._wake 58 | 59 | @property 60 | def dt(self): 61 | """Timestep for the simulation""" 62 | return self._dt 63 | 64 | def _wake_velocity(self, pos=None, dt=0): 65 | """Compute the induced velocity at each of the wake vortices 66 | 67 | This is the right-hand side for the timestepper that advances the 68 | positions of the wake vortices 69 | 70 | Parameters 71 | ---------- 72 | pos : array, optional 73 | Array (shape (n,2)) of positions of wake vortices. Default is the 74 | current positions of wake vortices. 75 | dt : float, optional 76 | Timestep between current simulation time, and time at which the 77 | velocity is to be computed (default is 0). 78 | 79 | Returns 80 | ------- 81 | vel : array, shape (n,2) 82 | Induced velocities at the specified locations of wake vortices. 83 | 84 | Notes 85 | ----- 86 | If ``pos`` is specified, the wake positions in the Timestepper object 87 | are updated, and the strengths of bound elements are updated as well to 88 | satisfy the no-flow-through boundary condition. 89 | 90 | """ 91 | wake = self._wake 92 | bound = self._bound 93 | if pos is None: 94 | pos = wake.positions 95 | shed = None 96 | else: 97 | wake.positions = pos 98 | if self._has_body: 99 | # update body position and strengths of surface elements 100 | bound.time = self._time + dt 101 | bound.update_strengths_unsteady(dt, self._Uinfty, wake) 102 | shed = Vortices(*bound.get_newly_shed()) 103 | vel = wake.induced_velocity() 104 | vel += self._Uinfty 105 | if self._has_body: 106 | vel += bound.induced_velocity(pos) 107 | if shed: 108 | vel += shed.induced_velocity(pos) 109 | return vel 110 | 111 | def _update_flow(self, wake_pos, dt): 112 | """Update the flow with new positions of wake vortices 113 | 114 | Parameters 115 | ---------- 116 | wake_pos : array 117 | The new locations of wake vortices 118 | dt : float 119 | The amount by which the time should be incremented 120 | 121 | Notes 122 | ----- 123 | The body motion is updated to the new time, the strengths of the 124 | bound elements are updated to enforce the no-flow-through boundary 125 | condition, and a newly shed vortex is added to the wake. 126 | 127 | """ 128 | self._wake.positions = wake_pos 129 | self._time += dt 130 | if self._has_body: 131 | self._bound.time = self._time 132 | self._bound.update_strengths_unsteady(dt, self._Uinfty, self._wake) 133 | self._wake.append(*self._bound.get_newly_shed()) 134 | 135 | 136 | class ExplicitEuler(Timestepper): 137 | """Timestepper using the explicit Euler method""" 138 | 139 | def _advance(self, x, dt): 140 | vel = self._wake_velocity() 141 | self._update_flow(x + vel * dt, dt) 142 | 143 | class RungeKutta2(Timestepper): 144 | """Timestepper using 2nd-order Runge Kutta""" 145 | 146 | def _advance(self, x, dt): 147 | k1 = self._wake_velocity() 148 | k2 = self._wake_velocity(x + dt/2 * k1, dt/2) 149 | self._update_flow(x + dt * k2, dt) 150 | 151 | class RungeKutta4(Timestepper): 152 | """Timestepper using 4th-order Runge Kutta""" 153 | 154 | def _advance(self, x, dt): 155 | k1 = self._wake_velocity() 156 | k2 = self._wake_velocity(x + dt/2 * k1, dt/2) 157 | k3 = self._wake_velocity(x + dt/2 * k2, dt/2) 158 | k4 = self._wake_velocity(x + dt * k3, dt) 159 | self._update_flow(x + dt/6 * (k1 + 2 * k2 + 2 * k3 + k4), dt) 160 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* generated/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pysces.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pysces.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pysces" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pysces" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /pysces/panel.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import numpy as np 4 | import sys 5 | from .vortex import Vortices 6 | 7 | __all__ = ['BoundVortices', 'BoundSourceDoublets'] 8 | 9 | class BoundVortices(object): 10 | """A class for bound vortex panels""" 11 | 12 | def __init__(self, body, Uinfty=(1,0)): 13 | self._body = body 14 | self._time = 0 15 | self._update(Uinfty) 16 | 17 | def _update(self, Uinfty=(1,0)): 18 | # Uinfty is used here solely to determine direction of panel and for 19 | # distributing bound vortices and collocation points 20 | q = self._body.get_points(body_frame=True) 21 | dq = np.diff(q, axis=0) 22 | self._numpanels = dq.shape[0] 23 | self._tangents = dq / np.linalg.norm(dq, axis=1)[:,np.newaxis] 24 | self._normals = np.transpose(np.array([dq[:,1], -dq[:,0]]) / 25 | np.linalg.norm(dq, axis=1)) 26 | 27 | q25 = q[:-1] + 0.25 * dq 28 | q75 = q[:-1] + 0.75 * dq 29 | # vortex positions at 1/4 chord of panel 30 | # collocation points at 3/4 chord of panel 31 | # Determine orientation from Uinfty 32 | Uinfty = np.array(Uinfty) 33 | xvort = q25.copy() 34 | self._xcoll = q75.copy() 35 | # reverse direction where dq is against flow direction 36 | top, = np.where(np.dot(dq, Uinfty) <= 0) 37 | xvort[top] = q75[top] 38 | self._xcoll[top] = q25[top] 39 | # find trailing edge and wake vortex direction 40 | if np.linalg.norm(q[0] - q[-1]) < 0.005: 41 | # closed body 42 | self._trailing_edge = 0.5 * (q[0] + q[-1]) 43 | wake_dir = dq[-1] - dq[0] 44 | self._wake_dir = wake_dir / np.linalg.norm(wake_dir) 45 | else: 46 | # thin airfoil 47 | self._trailing_edge = q[0] 48 | self._wake_dir = -dq[0] / np.linalg.norm(dq[0]) 49 | self._vortices = Vortices(xvort) 50 | self._influence_matrix = None 51 | 52 | def update_positions(self): 53 | # If non-rigid bodies are used, update panel positions here. 54 | # 55 | # Note that if the only motion is rigid body motion, the panel positions 56 | # do not need to be updated, since they are in body-fixed frame 57 | 58 | # need to recompute influence matrix when points change 59 | self._influence_matrix = None 60 | 61 | @property 62 | def influence_matrix(self): 63 | if self._influence_matrix is None: 64 | # time to recompute 65 | n = self._numpanels 66 | A = np.zeros((n, n)) 67 | for j, vort in enumerate(self._vortices): 68 | vel = self._vortices.induced_velocity_single(self._xcoll, 69 | vort[0], 1) 70 | A[:, j] = np.sum(vel * self._normals, 1) 71 | self._influence_matrix = A 72 | return self._influence_matrix 73 | 74 | @property 75 | def num_panels(self): 76 | return self._numpanels 77 | 78 | @property 79 | def tangents(self): 80 | return self._tangents 81 | 82 | @property 83 | def normals(self): 84 | return self._normals 85 | 86 | def update_strengths(self, Uinfty=(1,0)): 87 | """Update vortex strengths""" 88 | rhs = self.compute_rhs(Uinfty) 89 | self._vortices.strengths = np.linalg.solve(self.influence_matrix, rhs) 90 | 91 | def update_strengths_unsteady(self, dt, Uinfty=(1,0), wake=None, circ=None, 92 | wake_fac=0.25): 93 | """Update strengths for unsteady calculation 94 | 95 | Shed a new wake panel (not added into wake) 96 | 97 | Parameters 98 | ---------- 99 | dt : float 100 | Timestep 101 | Uinfty : array_like, optional 102 | Farfield fluid velocity (default (1,0)) 103 | wake : wake panel object, optional 104 | Induces velocities on the body (default None) 105 | circ : float, optional 106 | Total bound circulation, for enforcing Kelvin's circulation theorem. 107 | If None (default), obtain the total circulation from the wake, 108 | assuming overall circulation (body + wake) is zero 109 | wake_fac : float, optional 110 | New wake vortex is placed a distance wake_fac * Uinfty * dt from 111 | trailing edge (see Katz & Plotkin, p390). 112 | """ 113 | 114 | # determine new wake vortex position (in body-fixed frame) 115 | distance = wake_fac * np.sqrt(Uinfty[0]**2 + Uinfty[1]**2) * dt 116 | x_shed = self._trailing_edge + distance * self._wake_dir 117 | # compute velocity induced on collocation points by newly shed vortex 118 | # (done in the body-fixed frame) 119 | shed_vel = self._vortices.induced_velocity_single(self._xcoll, x_shed, 1) 120 | shed_normal = np.sum(shed_vel * self._normals, 1) 121 | # determine overall influence matrix, including newly shed vortex 122 | # last equation: sum of all the vortex strengths = total circulation 123 | A = np.vstack([np.hstack([self.influence_matrix, 124 | shed_normal[:,np.newaxis]]), 125 | np.ones((1, self._numpanels + 1))]) 126 | 127 | rhs0 = self.compute_rhs(Uinfty, wake) 128 | if circ is None: 129 | if wake is None: 130 | circ = 0 131 | else: 132 | circ = -wake.circulation 133 | rhs = np.hstack([rhs0, circ]) 134 | 135 | gam = np.linalg.solve(A, rhs) 136 | self._vortices.strengths = gam[:-1] 137 | self._x_shed = x_shed 138 | self._gam_shed = gam[-1] 139 | 140 | def compute_rhs(self, Uinfty=(1,0), wake=None): 141 | # get collocation points and normals 142 | # if a motion is present, use it to map the collocation points and 143 | # their normals from the body frame to the inertial frame. 144 | motion = self._body.get_motion() 145 | if motion: 146 | xcoll_inertial = motion.map_position(self._xcoll) 147 | normals_inertial = motion.map_vector(self._normals) 148 | else: 149 | xcoll_inertial = self._xcoll 150 | normals_inertial = self._normals 151 | # velocity induced by wake 152 | if wake: 153 | vel = wake.induced_velocity(xcoll_inertial) 154 | else: 155 | vel = np.zeros((self._numpanels, 2)) 156 | # assume body is not deforming: only motion is translation/rotation 157 | if motion: 158 | vel -= motion.map_velocity(self._xcoll) 159 | vel += np.array(Uinfty) 160 | # compute -v . n 161 | return -np.sum(vel * normals_inertial, 1) 162 | 163 | def get_newly_shed(self): 164 | """Return newly shed wake vortex in the inertial frame 165 | 166 | Returns 167 | ------- 168 | x_shed : 1d array, shape (2,) 169 | Location of newly shed wake vortex, in inertial frame 170 | gam_shed : float 171 | Strength of newly shed vortex 172 | """ 173 | motion = self._body.get_motion() 174 | if motion: 175 | x_shed_inertial = motion.map_position(self._x_shed) 176 | else: 177 | x_shed_inertial = np.array(self._x_shed, copy=True) 178 | return x_shed_inertial, self._gam_shed 179 | 180 | def induced_velocity(self, x): 181 | return self._vortices.induced_velocity(x, self._body.get_motion()) 182 | 183 | @property 184 | def time(self): 185 | return self._time 186 | 187 | @time.setter 188 | def time(self, value): 189 | self._time = value 190 | self._body.time = value 191 | 192 | @property 193 | def vortices(self): 194 | return self._vortices 195 | 196 | @property 197 | def collocation_pts(self): 198 | return self._xcoll 199 | 200 | @property 201 | def normals(self): 202 | # return self._normals[0,:], self._normals[1,:] 203 | return self._normals 204 | 205 | 206 | class BoundSourceDoublets(object): 207 | def __init__(self, body): 208 | self._body = body 209 | self.panels = body.get_points() 210 | 211 | def update_positions(self): 212 | self.panels = self._body.get_points() 213 | 214 | def update_strengths(self, wake, Uinfty, dt): 215 | # compute influence coefficients and RHS and solve for strengths 216 | pass 217 | 218 | def get_wake_panel(self): 219 | return None 220 | -------------------------------------------------------------------------------- /pysces/body.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from .motion import RigidMotion 3 | 4 | __all__ = ['Body', 'TransformedBody', 'Pitching', 'Heaving', 5 | 'cylinder', 'flat_plate', 'naca_airfoil', 'joukowski_foil', 6 | 'van_de_vooren_foil', 'karman_trefftz_foil'] 7 | 8 | class Body(object): 9 | """Base class for representing bodies 10 | """ 11 | def __init__(self, points): 12 | """Create a body with nodes at the given points 13 | 14 | Parameters 15 | ---------- 16 | points : 2d array, shape (n,2) 17 | Array of points defining the boundary of the body 18 | For a closed body, the boundary curve should be positively oriented 19 | (counter-clockwise around outside of body), starting from trailing 20 | edge 21 | """ 22 | self._time = 0 23 | self._points = np.array(points, dtype="float64") 24 | 25 | @property 26 | def time(self): 27 | """The time used to specify the body's motion""" 28 | return self._time 29 | 30 | @time.setter 31 | def time(self, value): 32 | self._time = value 33 | 34 | def get_points(self, body_frame=False): 35 | return self._points 36 | 37 | def get_body(self): 38 | """Return the Body object in the body-fixed frame""" 39 | return self 40 | 41 | def get_motion(self): 42 | """Return the transformation from the body-fixed to inertial frame""" 43 | return None 44 | 45 | def cylinder(radius, num_points): 46 | """Return a circular Body with the given radius and number of points""" 47 | th = np.linspace(0, 2 * np.pi, num_points) 48 | points = radius * np.array([np.cos(th), np.sin(th)]).T 49 | return Body(points) 50 | 51 | def flat_plate(num_points): 52 | """Return a flat plate Body with the given number of points. 53 | 54 | In body coordinates the plate runs from (0,0) to (1,0).""" 55 | x = np.linspace(1, 0, num_points) # from 1 to 0 so trailing edge at index 0 56 | y = np.zeros_like(x) 57 | return Body(np.array([x, y]).T) 58 | 59 | def joukowski_foil(xcenter=-.1, ycenter=.1, a=1, numpoints=32): 60 | """Return a Joukowski foil Body. 61 | 62 | The foil has its trailing edge at (2a,0). The foil has a total of 63 | numpoints along the boundary. Refer to chapter 4 of [1]_ for details. 64 | 65 | Parameters 66 | ---------- 67 | xcenter, ycenter : float 68 | (xcenter,ycenter) is the center of the Joukowski preimage circle. 69 | xcenter should be negative and small; its magnitude determines 70 | the bluffness of the foil. ycenter should be small; it 71 | determines the magnitude of the camber (positive gives upward 72 | camber, and negative gives downward camber). 73 | 74 | a : float 75 | radius of the Joukowski preimage circle 76 | 77 | numpoints : int 78 | number of points along the boundary 79 | 80 | References 81 | ---------- 82 | .. [1] Acheson, D. J., "Elementary Fluid Dynamics", Oxford, 1990. 83 | """ 84 | 85 | t = np.linspace(0,2*np.pi,numpoints) 86 | r = np.sqrt((a-xcenter)**2+ycenter**2) 87 | chi = xcenter + r*np.cos(t) 88 | eta = ycenter + r*np.sin(t) 89 | mag2 = chi*chi + eta*eta 90 | x = chi*(1+a**2/mag2) 91 | y = eta*(1-a**2/mag2) 92 | return Body(np.array([x,y]).T) 93 | 94 | def karman_trefftz_foil(xcenter=-.1, ycenter=0, a=.1, angle_deg=10, numpoints=32): 95 | """Return a Karman-Trefftz foil Body. 96 | 97 | The Karman-Trefftz foil is a modified version of the Joukowski 98 | foil but with a nonzero interior angle --- rather than a cusp --- at the 99 | trailing edge. Refer to [1]_ for details. 100 | 101 | Parameters 102 | ---------- 103 | xcenter, ycenter, a : float. 104 | The same as in joukowski_foil(). 105 | 106 | angle_deg : float 107 | The interior angle, in degrees, at the trailing edge. 108 | 109 | numpoints : int 110 | Number of points along the boundary 111 | 112 | See Also 113 | -------- 114 | joukowski_foil() 115 | 116 | References 117 | ---------- 118 | .. [1] https://en.wikipedia.org/wiki/Joukowsky_transform 119 | """ 120 | 121 | angle_rad = angle_deg*np.pi/180 122 | n = 2-angle_rad/np.pi 123 | t = np.linspace(0,2*np.pi,numpoints) 124 | ctr = xcenter + 1j*ycenter 125 | r = np.linalg.norm(ctr-a) 126 | zeta = ctr+r*np.exp(1j*t) 127 | mag2 = np.linalg.norm(zeta) 128 | z = n*((1+1/zeta)**n+(1-1/zeta)**n)/((1+1/zeta)**n-(1-1/zeta)**n) 129 | x = [w.real for w in z] 130 | y = [w.imag for w in z] 131 | return Body(np.array([x,y]).T) 132 | 133 | def van_de_vooren_foil(semichord=1.0, thickness=0.15, angle_deg=5, 134 | numpoints=32): 135 | """Return a van de Vooren foil Body. 136 | 137 | Refer to section 6.6 of [1]_ 138 | 139 | Parameters 140 | ---------- 141 | semichord : float 142 | half the chord c, so c=2*semichord 143 | 144 | thickness : float 145 | vertical thickness as a fraction (0 < thickness < 1) of the semichord 146 | 147 | angle_deg : float 148 | interior angle, in degrees, at the trailing edge 149 | 150 | numpoints : int 151 | number of points along the boundary 152 | 153 | References 154 | ---------- 155 | .. [1] Katz, Joseph and Plotkin, Allen, "Low-Speed Aerodynamics", 2nd Ed., 156 | Cambridge University Press, 2001. 157 | """ 158 | 159 | k = 2-(angle_deg*np.pi/180) 160 | a = 2*semichord*((1+thickness)**(k-1))*2**(-k) 161 | t = np.linspace(0,2*np.pi,numpoints) 162 | num = (a*(np.cos(t)-1)+1j*a*np.sin(t))**k 163 | den = (a*(np.cos(t)-thickness)+1j*a*np.sin(t))**(k-1) 164 | z = (num/den)+semichord 165 | x = [w.real for w in z] 166 | y = [w.imag for w in z] 167 | return Body(np.array([x,y]).T) 168 | 169 | def naca_airfoil(code, num_points, zero_thick_te=False, uniform=False): 170 | """Return a NACA 4-digit series airfoil""" 171 | # extract parameters from 4-digit code 172 | code_str = "%04d" % int(code) 173 | if len(code_str) != 4: 174 | raise ValueError("NACA designation is more than 4 digits") 175 | max_camber = 0.01 * int(code_str[0]) 176 | p = 0.1 * int(code_str[1]) # location of max camber 177 | thickness = 0.01 * int(code_str[2:]) 178 | if uniform: 179 | x = np.linspace(0, 1, num_points) 180 | else: 181 | # closer spacing near leading edge 182 | theta = np.linspace(0, 0.5 * np.pi, num_points) 183 | x = 1 - np.cos(theta) 184 | 185 | # thickness 186 | coefs = [-0.1015, 0.2843, -0.3516, -0.1260, 0, 0.2969] 187 | if zero_thick_te: 188 | coefs[0] = -0.1036 189 | y_thick = 5 * thickness * (np.polyval(coefs[:5], x) + 190 | coefs[5] * np.sqrt(x)) 191 | 192 | # camber 193 | front = np.where(x <= p) 194 | back = np.where(x > p) 195 | y_camber = np.zeros_like(x) 196 | if p: 197 | y_camber[front] = max_camber * x[front] / p**2 * (2 * p - x[front]) 198 | y_camber[back] = max_camber * ((1. - x[back])/(1. - p)**2 * 199 | (1 + x[back] - 2 * p)) 200 | x = np.hstack([x[-1:0:-1], x]) 201 | y = np.hstack([y_camber[-1:0:-1] + y_thick[-1:0:-1], 202 | y_camber - y_thick]) 203 | return Body(np.array([x, y]).T) 204 | 205 | 206 | class TransformedBody(object): 207 | """Base class for rigid (Euclidean) transformations of existing bodies 208 | """ 209 | def __init__(self, body, angle=0, displacement=(0,0)): 210 | """angles are clockwise, in degrees""" 211 | self._parent = body 212 | self._body = body.get_body() 213 | self._motion = RigidMotion(-angle * np.pi / 180, displacement) 214 | 215 | def get_body(self): 216 | return self._body 217 | 218 | def get_motion(self): 219 | self._update() 220 | return self._motion.compose(self._parent.get_motion()) 221 | 222 | def set_motion(self, value): 223 | self._motion = value 224 | 225 | @property 226 | def time(self): 227 | return self._body.time 228 | 229 | @time.setter 230 | def time(self, value): 231 | self._body.time = value 232 | 233 | def _update(self): 234 | # update body motion: subclasses override this 235 | pass 236 | 237 | def get_points(self, body_frame=False): 238 | q = self._body.get_points() 239 | if body_frame: 240 | return q 241 | return self.get_motion().map_position(q) 242 | 243 | 244 | class Pitching(TransformedBody): 245 | """Sinusoidal pitching for an existing body 246 | """ 247 | def __init__(self, body, amplitude, frequency, phase=0.): 248 | """amplitude and phase given in degrees""" 249 | super(Pitching, self).__init__(body) 250 | self._amplitude = amplitude * np.pi / 180 251 | self._frequency = frequency 252 | self._phase = phase * np.pi / 180 253 | 254 | def _update(self): 255 | theta = self._frequency * self.time + self._phase 256 | alpha = self._amplitude * np.sin(theta) 257 | alphadot = self._amplitude * self._frequency * np.cos(theta) 258 | self.set_motion(RigidMotion(-alpha, (0,0), -alphadot, (0,0))) 259 | 260 | 261 | class Heaving(TransformedBody): 262 | """Sinusoidal heaving for an existing body 263 | """ 264 | def __init__(self, body, displacement, frequency, phase=0.): 265 | super(Heaving, self).__init__(body) 266 | self._displacement = np.array(displacement, dtype="float64") 267 | self._frequency = frequency 268 | self._phase = phase * np.pi / 180 269 | 270 | def _update(self): 271 | theta = self._frequency * self.time + self._phase 272 | x = self._displacement * np.sin(theta) 273 | xdot = self._displacement * self._frequency * np.cos(theta) 274 | self.set_motion(RigidMotion(0, x, 0, xdot)) 275 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pysces documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Apr 29 11:19:33 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import re 18 | import shutil 19 | 20 | import mock 21 | 22 | MOCK_MODULES = ['numpy', 'numpy.linalg'] 23 | for mod_name in MOCK_MODULES: 24 | sys.modules[mod_name] = mock.Mock() 25 | 26 | # clean out generated files if running on readthedocs 27 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 28 | if on_rtd: 29 | print("Removing generated files") 30 | if os.path.isdir("generated"): 31 | shutil.rmtree("generated") 32 | else: 33 | import sphinx_rtd_theme 34 | html_theme = "sphinx_rtd_theme" 35 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 36 | 37 | # If extensions (or modules to document with autodoc) are in another directory, 38 | # add these directories to sys.path here. If the directory is relative to the 39 | # documentation root, use os.path.abspath to make it absolute, like shown here. 40 | sys.path.insert(0, os.path.abspath('..')) 41 | 42 | # -- General configuration ------------------------------------------------ 43 | 44 | # If your documentation needs a minimal Sphinx version, state it here. 45 | #needs_sphinx = '1.0' 46 | 47 | # Add any Sphinx extension module names here, as strings. They can be 48 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 49 | # ones. 50 | extensions = [ 51 | 'sphinx.ext.autodoc', 52 | 'sphinx.ext.autosummary', 53 | 'sphinx.ext.intersphinx', 54 | 'sphinx.ext.pngmath', 55 | 'sphinx.ext.todo', 56 | 'numpydoc', 57 | ] 58 | 59 | # disable autosummary code from numpy 60 | # (otherwise, will generate many errors, because autosummary runs twice) 61 | numpydoc_show_class_members = False 62 | 63 | autosummary_generate = True 64 | 65 | autodoc_default_flags = ['members', 'inherited-members'] 66 | 67 | # Add any paths that contain templates here, relative to this directory. 68 | templates_path = ['_templates'] 69 | 70 | # The suffix of source filenames. 71 | source_suffix = '.rst' 72 | 73 | # The encoding of source files. 74 | #source_encoding = 'utf-8-sig' 75 | 76 | # The master toctree document. 77 | master_doc = 'index' 78 | 79 | # General information about the project. 80 | project = u'pysces' 81 | copyright = u'2015, Clancy Rowley' 82 | 83 | # The version info for the project you're documenting, acts as replacement for 84 | # |version| and |release|, also used in various other places throughout the 85 | # built documents. 86 | # 87 | # The short X.Y version. 88 | import pysces 89 | # The short X.Y version. 90 | version = re.sub(r'(\d+\.\d+)\.(.*)', r'\1', pysces.__version__) 91 | # The full version, including alpha/beta/rc tags. 92 | release = pysces.__version__ 93 | print("version %s, release %s" % (version, release)) 94 | 95 | # The language for content autogenerated by Sphinx. Refer to documentation 96 | # for a list of supported languages. 97 | #language = None 98 | 99 | # There are two options for replacing |today|: either, you set today to some 100 | # non-false value, then it is used: 101 | #today = '' 102 | # Else, today_fmt is used as the format for a strftime call. 103 | #today_fmt = '%B %d, %Y' 104 | 105 | # List of patterns, relative to source directory, that match files and 106 | # directories to ignore when looking for source files. 107 | exclude_patterns = ['_build'] 108 | 109 | # The reST default role (used for this markup: `text`) to use for all 110 | # documents. 111 | #default_role = None 112 | 113 | # If true, '()' will be appended to :func: etc. cross-reference text. 114 | #add_function_parentheses = True 115 | 116 | # If true, the current module name will be prepended to all description 117 | # unit titles (such as .. function::). 118 | #add_module_names = True 119 | 120 | # If true, sectionauthor and moduleauthor directives will be shown in the 121 | # output. They are ignored by default. 122 | #show_authors = False 123 | 124 | # The name of the Pygments (syntax highlighting) style to use. 125 | pygments_style = 'sphinx' 126 | 127 | # A list of ignored prefixes for module index sorting. 128 | #modindex_common_prefix = [] 129 | 130 | # If true, keep warnings as "system message" paragraphs in the built documents. 131 | #keep_warnings = False 132 | 133 | 134 | # -- Options for HTML output ---------------------------------------------- 135 | 136 | # The theme to use for HTML and HTML Help pages. See the documentation for 137 | # a list of builtin themes. 138 | # html_theme = 'default' 139 | 140 | # Theme options are theme-specific and customize the look and feel of a theme 141 | # further. For a list of options available for each theme, see the 142 | # documentation. 143 | #html_theme_options = {} 144 | 145 | # Add any paths that contain custom themes here, relative to this directory. 146 | #html_theme_path = [] 147 | 148 | # The name for this set of Sphinx documents. If None, it defaults to 149 | # " v documentation". 150 | #html_title = None 151 | 152 | # A shorter title for the navigation bar. Default is the same as html_title. 153 | #html_short_title = None 154 | 155 | # The name of an image file (relative to this directory) to place at the top 156 | # of the sidebar. 157 | #html_logo = None 158 | 159 | # The name of an image file (within the static path) to use as favicon of the 160 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 161 | # pixels large. 162 | #html_favicon = None 163 | 164 | # Add any paths that contain custom static files (such as style sheets) here, 165 | # relative to this directory. They are copied after the builtin static files, 166 | # so a file named "default.css" will overwrite the builtin "default.css". 167 | # html_static_path = ['_static'] 168 | 169 | # Add any extra paths that contain custom files (such as robots.txt or 170 | # .htaccess) here, relative to this directory. These files are copied 171 | # directly to the root of the documentation. 172 | #html_extra_path = [] 173 | 174 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 175 | # using the given strftime format. 176 | #html_last_updated_fmt = '%b %d, %Y' 177 | 178 | # If true, SmartyPants will be used to convert quotes and dashes to 179 | # typographically correct entities. 180 | #html_use_smartypants = True 181 | 182 | # Custom sidebar templates, maps document names to template names. 183 | #html_sidebars = {} 184 | 185 | # Additional templates that should be rendered to pages, maps page names to 186 | # template names. 187 | #html_additional_pages = {} 188 | 189 | # If false, no module index is generated. 190 | #html_domain_indices = True 191 | 192 | # If false, no index is generated. 193 | #html_use_index = True 194 | 195 | # If true, the index is split into individual pages for each letter. 196 | #html_split_index = False 197 | 198 | # If true, links to the reST sources are added to the pages. 199 | #html_show_sourcelink = True 200 | 201 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 202 | #html_show_sphinx = True 203 | 204 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 205 | #html_show_copyright = True 206 | 207 | # If true, an OpenSearch description file will be output, and all pages will 208 | # contain a tag referring to it. The value of this option must be the 209 | # base URL from which the finished HTML is served. 210 | #html_use_opensearch = '' 211 | 212 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 213 | #html_file_suffix = None 214 | 215 | # Output file base name for HTML help builder. 216 | htmlhelp_basename = 'pyscesdoc' 217 | 218 | 219 | # -- Options for LaTeX output --------------------------------------------- 220 | 221 | latex_elements = { 222 | # The paper size ('letterpaper' or 'a4paper'). 223 | #'papersize': 'letterpaper', 224 | 225 | # The font size ('10pt', '11pt' or '12pt'). 226 | #'pointsize': '10pt', 227 | 228 | # Additional stuff for the LaTeX preamble. 229 | #'preamble': '', 230 | } 231 | 232 | # Grouping the document tree into LaTeX files. List of tuples 233 | # (source start file, target name, title, 234 | # author, documentclass [howto, manual, or own class]). 235 | latex_documents = [ 236 | ('index', 'pysces.tex', u'pysces Documentation', 237 | u'Clancy Rowley', 'manual'), 238 | ] 239 | 240 | # The name of an image file (relative to this directory) to place at the top of 241 | # the title page. 242 | #latex_logo = None 243 | 244 | # For "manual" documents, if this is true, then toplevel headings are parts, 245 | # not chapters. 246 | #latex_use_parts = False 247 | 248 | # If true, show page references after internal links. 249 | #latex_show_pagerefs = False 250 | 251 | # If true, show URL addresses after external links. 252 | #latex_show_urls = False 253 | 254 | # Documents to append as an appendix to all manuals. 255 | #latex_appendices = [] 256 | 257 | # If false, no module index is generated. 258 | #latex_domain_indices = True 259 | 260 | 261 | # -- Options for manual page output --------------------------------------- 262 | 263 | # One entry per manual page. List of tuples 264 | # (source start file, name, description, authors, manual section). 265 | man_pages = [ 266 | ('index', 'pysces', u'Pysces Documentation', 267 | [u'Clancy Rowley'], 1) 268 | ] 269 | 270 | # If true, show URL addresses after external links. 271 | #man_show_urls = False 272 | 273 | 274 | # -- Options for Texinfo output ------------------------------------------- 275 | 276 | # Grouping the document tree into Texinfo files. List of tuples 277 | # (source start file, target name, title, author, 278 | # dir menu entry, description, category) 279 | texinfo_documents = [ 280 | ('index', 'pysces', u'Pysces Documentation', 281 | u'Clancy Rowley', 'pysces', 'Boundary element method for python.', 282 | 'Miscellaneous'), 283 | ] 284 | 285 | # Documents to append as an appendix to all manuals. 286 | #texinfo_appendices = [] 287 | 288 | # If false, no module index is generated. 289 | #texinfo_domain_indices = True 290 | 291 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 292 | #texinfo_show_urls = 'footnote' 293 | 294 | # If true, do not generate a @detailmenu in the "Top" node's menu. 295 | #texinfo_no_detailmenu = False 296 | 297 | # Example configuration for intersphinx: refer to the Python standard library. 298 | intersphinx_mapping = {'http://docs.python.org/': None} 299 | 300 | #If this is True, todo and todolist produce output, else they produce nothing. 301 | #The default is False. 302 | todo_include_todos = True 303 | --------------------------------------------------------------------------------