Creating a Python Package uwa#
Here we create a Python package from scratch for underwater acoustic wave calculations.
We will implement:
✅ A class AcousticWave
✅ Functions for wavelength, wave number, and near-field distance inside the class and a *deadzone distance calculator outside of the class
✅ A proper package structure
Typically a Python package contains at least an initialisation file (init.py), and a number of modules (containing classes, functions and variables) stored in *.py files, a setup.py file and a readme file.

Step 1: Project Structure#
Create a directory and organize files as follows, all modules to be included in the oackage should be organised in a subfolder. The name of the subfolder defines how the package will be imported (here we call the folder uwa and will import the package using import uwa. This folder has to include an initialisation file __init__.py and at least one module, here wave.py. In the main package folder we have a toml file, contains installation and metadata information (in older versions this was a setup.py file), a README.md which conains a project description, LICENSE which is the license text and if needed, a requirements.txt file:
uwa-underwater_acoustics/
│── uwa/ # Main package directory
│ │── init.py # Makes this a package
│ │── wave.py # Contains AcousticWave class & functions
|
│── pyproject.toml # Package metadata & installation
|
│── README.md # Project description
|
|── LICENSE
|
│── requirements.txt # Dependencies (if needed)
|```
Step 2: Implement the Package#
1) Create an initialisation file __init__.py**#
The __init__.py file basically tells our Python package to look for bits and pieces in the folder where the __init__.py file is located (prior to Python 3.3 it was impossible to import funcitons from folders without an __init__.py file).
Here we import the class AcousticWaveand the functions wavelength, wave_number and nearfield_distance from the the module acoustic (contained in the acoustic.py file in the same directory):
Content of __init__.py
# uwa/__init__.py
from .wave import AcousticWave, deadzone
A major benefit of initialisation files is that we can control which modules or sub-packages are available when the package is imported . Through importing functions or classes in the init file, we can use them without the need to import them explicitly.
As a consequence of this init file, at a later stage, when we ar eusing our package we can use the class AcousticWave as follows:
from uwa import AcousticWave
instead of having to import it from the wave module within the uwa package:
from uwa.wave import AcousticWave
Besides loading modules or subpackages, the init file can also be used to run some code, when the package is imported (logging or setup codes would be an example).
2) Create the wave module wave.py#
import dependencies: Our package depends on the
numpyandmathpackages for some basic operations.class definition: We start by defining a
class AcousticWaveclass docstring: The first part is the docstring, containing information on what the purpose of this class is, and the a description of the input variables or class attributes.
class initialisation: the class contains a self reference instance
selfand the needed attributesfrequency,speedandbwwhich we assign to self during the initialisation, such that we can use it within the methods described in the classdefine methods: we defined 3 methods within the
AcousticWaveclass.wave_numberandwave_lengthonly depend on the attributes of the class, whilenearfield_distanceneeds an additional input value.define function
deadzoneoutside of the class definition
Note
self
In Python, self refers to the instance of a class and is used to access the instance’s attributes and methods. It allows each object created from a class to maintain its own state and interact with its own data. When you define a method in a class, self is the first parameter, and it is automatically passed when you call the method on an object. This allows you to work with the specific instance of the class.
In our example self allows our functions or methods to access the frequency and speed attributes from the AcousticWave class.
Note
Function or method?
Most likely the nomenclature is not used consistently within this tutorial…Technically a functioncan be defined in any part of a code and form standalone blocks of code. methods are tied to an object or class and can’t operate outside of this. functionsare defined locally or globally while methodsare defined within classes and called on class instances.
# uwa/wave.py
from numpy import sin, cos, pi
from math import radians
class AcousticWave:
"""
A class to represent an underwater acoustic wave.
Attributes:
frequency (float): Frequency of the wave in Hz.
speed (float): Speed of sound in water in m/s (default 1500 m/s).
bw (float): θ3dB, 3 dB beamwidth in ° (default 7°).
"""
def __init__(self, frequency, speed=1500, bw = 7):
#assign inputs to self
self.frequency = frequency
self.speed = speed
self.bw = bw
#initialisation calculations:
self.wl = self.wavelength()
self.k = self.wave_number()
self.ar = self.active_radius()
self.rnf = self.nearfield_distance()
def wavelength(self):
"""Calculate the wavelength λ = c / f"""
return self.speed / self.frequency
def wave_number(self):
"""Calculate the wave number k = 2π / λ"""
return 2 * pi / self.wavelength()
def active_radius(self):
"""Calculates the active radius of a round transducer in ster"""
return 3.2 / (self.wave_number() * sin(radians(self.bw /2) ))
def nearfield_distance(self):
"""Calculate the acoustic near-field distance in m"""
return pi * self.active_radius()**2 / (4 * self.wavelength())
def deadzone(d, speed,q, tau):
"""
Calculate the distance from the bottom at which there is bias
Parameters:
d (float or integer): Bottom Depth in m
speed (float or integer): Ambient sound speed in m/s
q (float or integer): slope of the seafloor in °
tau (float or integer): pulse duration in s
Returns:
Distance from the bottom where there is bias in m
"""
return (d / sin(radians(90 - q)) - d) + speed * tau / 2
We can test our functions quickly here:
aw = AcousticWave(speed=1470, frequency = 120e3, bw=7)
aw.__dict__
{'frequency': 120000.0,
'speed': 1470,
'bw': 7,
'wl': 0.01225,
'k': 512.9130863003744,
'ar': 0.102195299293607,
'rnf': 2.008800867092126}
deadzone(d=100,speed=1450,q=15, tau=0.001)
4.252618041008295
Step 3: Create a setup file pyproject.toml#
Formerly this was a setup.py but this has gradually been replaced by toml files. Just like setup.py files, toml files can be used to build wheels used to install python packages.
nameis the distribution name of your packageversionis the package versionauthorsidentifies the authorsdescriptionone snetence summary of packagereadmepath to readme filerequires-pythonprovides supported version to packageclassifiersadditional metadatalicencelicence of packagelicence-filespaths to licence filesurlslist any additional urls that should be contained within the package description
# pyproject.toml
[build-system]
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"
[project]
name = "uwa"
version = "0.1"
authors = [
{ name="Jupyter Pythonista", email="jupyter@python.net" },
]
description = "A package for underwater acoustic wave calculations"
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"numpy",
"math"
]
classifiers = [
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
]
license = "MIT AND (Apache-2.0 OR BSD-2-Clause)"
[project.urls]
Homepage = "https://github.com/xxx/underwater_acoustics"
Issues = "https://github.com/xxx/underwater_acoustics/issues"
Step 4: Create a documentation file README.md#
# uwa - Underwater Acoustics
This Python package provides calculations related to **underwater acoustic waves**, including:
- **Wavelength (λ = c / f)**
- **Wave number (k = 2π / λ)**
- **Near-field distance**
- **Acoustic Deadzone**
## Installation
You can install this package using the provided wheel
## Usage
```{code-block} python
import uwa
aw = uwa.wave.AcousticWave(38000)
```
Step 5: Create a LICENSE file#
Go to https://choosealicense.com/, pick a license and save it in the LICENSE file.
You could for example pick the MIT license:
MIT License
Copyright (c) [year] [fullname]
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
Step 6: Package up the Python code and install it#
We will create a wheel for our project. A wheel is a built package that can be installed without needing to go through the “build” process. Installing wheels is substantially faster for the end user than installing from a source distribution.
Before we can build wheels we need to make sure the build package is available. Run:
python -m pip install build
Now we are ready to build our source package and wheel. Move into the directory where all files are stored.
We create a Source distribution:
python -m build --sdist
Now we create a wheel to make sharing and isntalling the package easier:
python -m build
This will take a little while…but eventually you should see:
...
running install_scripts
creating build\bdist.win-amd64\wheel\uwa-0.1.dist-info\WHEEL
creating 'C:\Users\gastauer\Documents\Projekte\WGFAST\2025\boat\wgfast_2025\tutorials\python_package\dist\.tmp-4r6y9lfp\uwa-0.1-py3-none-any.whl' and adding 'build\bdist.win-amd64\wheel' to it
adding 'uwa/__init__.py'
adding 'uwa/wave.py'
adding 'uwa-0.1.dist-info/licenses/LICENSE'
adding 'uwa-0.1.dist-info/METADATA'
adding 'uwa-0.1.dist-info/WHEEL'
adding 'uwa-0.1.dist-info/top_level.txt'
adding 'uwa-0.1.dist-info/RECORD'
removing build\bdist.win-amd64\wheel
Successfully built uwa-0.1.tar.gz and uwa-0.1-py3-none-any.whl
Now we have created a package. You can easily install and share the source package *.tar.gzor the wheel *.whl. The package could be uploaded to pypi and be installed from anywhere through pip.
whl files can be installed by calling pip install my-package.whl.
The whl file is in the dist folder:
pip install ./dist/uwa-0.1-py3-none-any.whl
You should see something like:
Processing ...\dist\uwa-0.1-py3-none-any.whl
Requirement already satisfied: numpy in ...\wgfast25\lib\site-packages (from uwa==0.1) (1.26.4)
Installing collected packages: uwa
Successfully installed uwa-0.1
Now we can test if it all worked:
import uwa
No error message means the package is imported successfully 😀
Let’s test it:
ac = uwa.AcousticWave(speed = 1450, frequency = 38e3, bw = 7)
ac.__dict__
{'frequency': 38000.0,
'speed': 1450,
'bw': 7,
'wl': 0.038157894736842106,
'k': 164.66278736056847,
'ar': 15.916561114471552,
'rnf': 2.6556697580708692}
uwa.deadzone(d = 100, speed = 1450, q = 20, tau = 0.0004)
6.707777247591218
Can we get some help?
help(uwa.AcousticWave)
Help on class AcousticWave in module uwa.wave:
class AcousticWave(builtins.object)
| AcousticWave(frequency, speed=1500, bw=7)
|
| A class to represent an underwater acoustic wave.
|
| Attributes:
| frequency (float): Frequency of the wave in Hz.
| speed (float): Speed of sound in water in m/s (default 1500 m/s).
| bw (float): θ3dB, 3 dB beamwidth in ° (default 7°).
|
| Methods defined here:
|
| __init__(self, frequency, speed=1500, bw=7)
| Initialize self. See help(type(self)) for accurate signature.
|
| active_radius(self)
| Calculates the active radius of a round transducer in ster
|
| nearfield_distance(self)
| Calculate the acoustic near-field distance in m
|
| wave_number(self)
| Calculate the wave number k = 2π / λ
|
| wavelength(self)
| Calculate the wavelength λ = c / f
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables
|
| __weakref__
| list of weak references to the object
help(uwa.deadzone)
Help on function deadzone in module uwa.wave:
deadzone(d, speed, q, tau)
Calculate the distance from the bottom at which there is bias
Parameters:
d (float or integer): Bottom Depth in m
speed (float or integer): Ambient sound speed in m/s
q (float or integer): slope of the seafloor in °
tau (float or integer): pulse duration in s
Returns:
Distance from the bottom where there is bias in m