floating-dynamics 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- floating_dynamics-0.1.0/LICENSE +21 -0
- floating_dynamics-0.1.0/PKG-INFO +167 -0
- floating_dynamics-0.1.0/README.md +142 -0
- floating_dynamics-0.1.0/pyproject.toml +42 -0
- floating_dynamics-0.1.0/setup.cfg +4 -0
- floating_dynamics-0.1.0/src/floating_dynamics/__init__.py +4 -0
- floating_dynamics-0.1.0/src/floating_dynamics/__main__.py +5 -0
- floating_dynamics-0.1.0/src/floating_dynamics/body.py +282 -0
- floating_dynamics-0.1.0/src/floating_dynamics/drawing.py +130 -0
- floating_dynamics-0.1.0/src/floating_dynamics/free_surface.py +97 -0
- floating_dynamics-0.1.0/src/floating_dynamics/geometry.py +119 -0
- floating_dynamics-0.1.0/src/floating_dynamics/scenario.py +170 -0
- floating_dynamics-0.1.0/src/floating_dynamics/ships.py +164 -0
- floating_dynamics-0.1.0/src/floating_dynamics/simulation.py +22 -0
- floating_dynamics-0.1.0/src/floating_dynamics.egg-info/PKG-INFO +167 -0
- floating_dynamics-0.1.0/src/floating_dynamics.egg-info/SOURCES.txt +22 -0
- floating_dynamics-0.1.0/src/floating_dynamics.egg-info/dependency_links.txt +1 -0
- floating_dynamics-0.1.0/src/floating_dynamics.egg-info/entry_points.txt +2 -0
- floating_dynamics-0.1.0/src/floating_dynamics.egg-info/requires.txt +8 -0
- floating_dynamics-0.1.0/src/floating_dynamics.egg-info/top_level.txt +1 -0
- floating_dynamics-0.1.0/tests/test_body.py +147 -0
- floating_dynamics-0.1.0/tests/test_geometry.py +101 -0
- floating_dynamics-0.1.0/tests/test_scenario.py +81 -0
- floating_dynamics-0.1.0/tests/test_ships.py +34 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Christian Hill
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: floating-dynamics
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Two-dimensional dynamics of floating bodies, including the free-surface effect of liquid cargo.
|
|
5
|
+
Author: Christian Hill
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/xnx/floating-dynamics
|
|
8
|
+
Project-URL: Issues, https://github.com/xnx/floating-dynamics/issues
|
|
9
|
+
Keywords: ship stability,free-surface effect,metacentric height,buoyancy,simulation
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Education
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: numpy
|
|
19
|
+
Requires-Dist: scipy
|
|
20
|
+
Provides-Extra: animation
|
|
21
|
+
Requires-Dist: matplotlib; extra == "animation"
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest; extra == "test"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# floating-dynamics
|
|
27
|
+
|
|
28
|
+
Two-dimensional dynamics of ships floating in the sea (cross-sections, per
|
|
29
|
+
metre of length), including the *free-surface effect* of liquid cargo.
|
|
30
|
+
|
|
31
|
+
The demonstration compares three identical coasters carrying the same mass of
|
|
32
|
+
cargo: as a solid, as a liquid free to move across the whole hold, and as a
|
|
33
|
+
liquid divided into tanks. Each is heeled slightly and released. The liquid's
|
|
34
|
+
free surface reduces the ship's metacentric height, GM, so the ship with the
|
|
35
|
+
undivided hold capsizes, while dividing the hold into n tanks reduces the
|
|
36
|
+
effect by a factor of n².
|
|
37
|
+
|
|
38
|
+
With the default cargo, 2.5 m of water (30 t per metre of the ship's
|
|
39
|
+
length), heeled by 5°:
|
|
40
|
+
|
|
41
|
+
| Cargo | GM | Roll period | Outcome |
|
|
42
|
+
|--------------------|---------|-------------|--------------------|
|
|
43
|
+
| Solid | 1.00 m | 13.2 s | rolls, and settles |
|
|
44
|
+
| Liquid | −0.50 m | – | capsizes in ~12 s |
|
|
45
|
+
| Liquid, two tanks | 0.63 m | 16.5 s | rolls, and settles |
|
|
46
|
+
|
|
47
|
+
## Trying it out
|
|
48
|
+
|
|
49
|
+
Install it with the animation extra (which adds Matplotlib), and run the
|
|
50
|
+
demonstration:
|
|
51
|
+
|
|
52
|
+
pip install 'floating-dynamics[animation]'
|
|
53
|
+
free-surface
|
|
54
|
+
|
|
55
|
+
or, with [uv](https://docs.astral.sh/uv/), without installing anything:
|
|
56
|
+
|
|
57
|
+
uvx --from 'floating-dynamics[animation]' free-surface
|
|
58
|
+
|
|
59
|
+
Options choose which ships to show, and set the initial heel, the cargo's
|
|
60
|
+
density and depth and the number of tanks, or save the animation instead of
|
|
61
|
+
showing it:
|
|
62
|
+
|
|
63
|
+
free-surface --cargo free # one ship
|
|
64
|
+
free-surface --cargo solid --cargo divided # two of them
|
|
65
|
+
free-surface --heel 10 --depth 2 --tanks 3
|
|
66
|
+
free-surface --density 13.6 # mercury
|
|
67
|
+
free-surface free-surface.mp4 # save the animation
|
|
68
|
+
free-surface --help
|
|
69
|
+
|
|
70
|
+
`python -m floating_dynamics` does the same as `free-surface`.
|
|
71
|
+
|
|
72
|
+
### Denser cargoes
|
|
73
|
+
|
|
74
|
+
`--density` sets the cargo's density in t/m³. Unless `--depth` is also
|
|
75
|
+
given, the cargo's mass stays the same (2.5 m of water would weigh 13.6 times
|
|
76
|
+
as much as mercury, and sink the ship), so its depth is reduced instead. The
|
|
77
|
+
free-surface effect depends on the width of the liquid's surface but not its
|
|
78
|
+
depth, so for the same mass of cargo it grows in proportion to the density:
|
|
79
|
+
|
|
80
|
+
| Cargo (30 t/m) | Water: GM | Outcome | Mercury: GM | Outcome |
|
|
81
|
+
|--------------------|-----------|-------------------|-------------|-------------------|
|
|
82
|
+
| Solid | 1.00 m | stable | 1.37 m | stable |
|
|
83
|
+
| Liquid | −0.50 m | capsizes in ~12 s | −19.0 m | capsizes in ~4.5 s |
|
|
84
|
+
| Liquid, two tanks | 0.63 m | stable | −3.7 m | lolls at about 24° |
|
|
85
|
+
|
|
86
|
+
The mercury in two tanks makes GM negative, but the ship does not capsize: it
|
|
87
|
+
heels until its wider waterline makes up the difference, and settles at an
|
|
88
|
+
*angle of loll*. A layer of mercury only 0.18 m deep sloshes slowly, though,
|
|
89
|
+
so these timings are less reliable than water's (see Assumptions).
|
|
90
|
+
|
|
91
|
+
## Using it from Python
|
|
92
|
+
|
|
93
|
+
`floating_dynamics.scenario` sets the parameters, runs the simulations and
|
|
94
|
+
returns the results as plain data (lists, dicts and floats), for programs
|
|
95
|
+
which draw the ships themselves, such as a web page running
|
|
96
|
+
[Pyodide](https://pyodide.org):
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from floating_dynamics.scenario import Scenario, run
|
|
100
|
+
|
|
101
|
+
scenario = Scenario.from_dict({'heel': 10, 'cargoes': ['free', 'divided'],
|
|
102
|
+
'design': {'cargo_depth': 2, 'tanks': 3}})
|
|
103
|
+
result = run(scenario)
|
|
104
|
+
for ship in result.ships:
|
|
105
|
+
print(ship.ship.title, ship.summary())
|
|
106
|
+
data = result.to_dict() # JSON-serialisable
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The main parameters, with their defaults, are:
|
|
110
|
+
|
|
111
|
+
| Parameter | Default | Meaning |
|
|
112
|
+
|------------------------|-------------------------------|-----------------------------------------------|
|
|
113
|
+
| `cargoes` | `['solid', 'free', 'divided']`| which ships to simulate |
|
|
114
|
+
| `heel` | 5 | initial heel (degrees) |
|
|
115
|
+
| `duration`, `dt` | 60, 0.1 | length of the simulation, time per frame (s) |
|
|
116
|
+
| `design.cargo_depth` | 2.5 | depth of cargo when upright (m) |
|
|
117
|
+
| `design.cargo_density` | 1.0 | density of cargo (t/m³) |
|
|
118
|
+
| `design.tanks` | 2 | number of tanks for the `'divided'` cargo |
|
|
119
|
+
|
|
120
|
+
Unlike `free-surface --density`, setting `cargo_density` here does not change
|
|
121
|
+
the depth: set `cargo_depth` too to keep the mass the same. The ship's
|
|
122
|
+
dimensions, lightship mass and hydrodynamic coefficients are also fields of
|
|
123
|
+
`CoasterDesign` (in `floating_dynamics.ships`), with comments. Unknown or
|
|
124
|
+
unphysical parameters raise `ValueError`. The format of the results is
|
|
125
|
+
described in `scenario.py`. Only NumPy and SciPy are needed for this.
|
|
126
|
+
|
|
127
|
+
## Modules
|
|
128
|
+
|
|
129
|
+
- `geometry`: polygon area, centroid, second moment, clipping and filling.
|
|
130
|
+
- `body`: `FloatingBody` (hydrostatics, equations of motion, righting lever,
|
|
131
|
+
metacentric height) and `Compartment` (a tank of liquid).
|
|
132
|
+
- `ships`: a coaster's midship section and its three ways of carrying cargo.
|
|
133
|
+
- `simulation`: integrating the motion with SciPy's `solve_ivp`.
|
|
134
|
+
- `scenario`: the parameters and results, as plain data.
|
|
135
|
+
- `drawing`: animating ships side by side, with their heel against time.
|
|
136
|
+
- `free_surface`: the command-line demonstration.
|
|
137
|
+
|
|
138
|
+
## Assumptions
|
|
139
|
+
|
|
140
|
+
- The motion is two-dimensional: a cross-section of a long ship, rolling and
|
|
141
|
+
heaving but not pitching.
|
|
142
|
+
- The liquid's surface stays level (quasi-static). This is reasonable while
|
|
143
|
+
the liquid sloshes much faster than the ship rolls; the demonstration
|
|
144
|
+
prints both periods. Shallow layers slosh slowly (the 0.18 m of mercury
|
|
145
|
+
above takes 18 s across the whole hold), and then the real liquid would lag
|
|
146
|
+
behind the roll, so the motion would develop more slowly. Whether the ship
|
|
147
|
+
is stable depends only on GM, which is unaffected.
|
|
148
|
+
- The hull is watertight up to its main deck: there is no flooding, so a
|
|
149
|
+
capsized ship floats upside down.
|
|
150
|
+
- The surrounding water is represented by added mass and linear damping.
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
git clone https://github.com/xnx/floating-dynamics
|
|
155
|
+
cd floating-dynamics
|
|
156
|
+
pip install -e '.[animation,test]'
|
|
157
|
+
pytest
|
|
158
|
+
|
|
159
|
+
The editable install (`-e`) uses the source files directly, so changes take
|
|
160
|
+
effect without reinstalling. To run the demonstration without installing at
|
|
161
|
+
all, from the project folder:
|
|
162
|
+
|
|
163
|
+
PYTHONPATH=src python -m floating_dynamics
|
|
164
|
+
|
|
165
|
+
## Licence
|
|
166
|
+
|
|
167
|
+
MIT: see `LICENSE`.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# floating-dynamics
|
|
2
|
+
|
|
3
|
+
Two-dimensional dynamics of ships floating in the sea (cross-sections, per
|
|
4
|
+
metre of length), including the *free-surface effect* of liquid cargo.
|
|
5
|
+
|
|
6
|
+
The demonstration compares three identical coasters carrying the same mass of
|
|
7
|
+
cargo: as a solid, as a liquid free to move across the whole hold, and as a
|
|
8
|
+
liquid divided into tanks. Each is heeled slightly and released. The liquid's
|
|
9
|
+
free surface reduces the ship's metacentric height, GM, so the ship with the
|
|
10
|
+
undivided hold capsizes, while dividing the hold into n tanks reduces the
|
|
11
|
+
effect by a factor of n².
|
|
12
|
+
|
|
13
|
+
With the default cargo, 2.5 m of water (30 t per metre of the ship's
|
|
14
|
+
length), heeled by 5°:
|
|
15
|
+
|
|
16
|
+
| Cargo | GM | Roll period | Outcome |
|
|
17
|
+
|--------------------|---------|-------------|--------------------|
|
|
18
|
+
| Solid | 1.00 m | 13.2 s | rolls, and settles |
|
|
19
|
+
| Liquid | −0.50 m | – | capsizes in ~12 s |
|
|
20
|
+
| Liquid, two tanks | 0.63 m | 16.5 s | rolls, and settles |
|
|
21
|
+
|
|
22
|
+
## Trying it out
|
|
23
|
+
|
|
24
|
+
Install it with the animation extra (which adds Matplotlib), and run the
|
|
25
|
+
demonstration:
|
|
26
|
+
|
|
27
|
+
pip install 'floating-dynamics[animation]'
|
|
28
|
+
free-surface
|
|
29
|
+
|
|
30
|
+
or, with [uv](https://docs.astral.sh/uv/), without installing anything:
|
|
31
|
+
|
|
32
|
+
uvx --from 'floating-dynamics[animation]' free-surface
|
|
33
|
+
|
|
34
|
+
Options choose which ships to show, and set the initial heel, the cargo's
|
|
35
|
+
density and depth and the number of tanks, or save the animation instead of
|
|
36
|
+
showing it:
|
|
37
|
+
|
|
38
|
+
free-surface --cargo free # one ship
|
|
39
|
+
free-surface --cargo solid --cargo divided # two of them
|
|
40
|
+
free-surface --heel 10 --depth 2 --tanks 3
|
|
41
|
+
free-surface --density 13.6 # mercury
|
|
42
|
+
free-surface free-surface.mp4 # save the animation
|
|
43
|
+
free-surface --help
|
|
44
|
+
|
|
45
|
+
`python -m floating_dynamics` does the same as `free-surface`.
|
|
46
|
+
|
|
47
|
+
### Denser cargoes
|
|
48
|
+
|
|
49
|
+
`--density` sets the cargo's density in t/m³. Unless `--depth` is also
|
|
50
|
+
given, the cargo's mass stays the same (2.5 m of water would weigh 13.6 times
|
|
51
|
+
as much as mercury, and sink the ship), so its depth is reduced instead. The
|
|
52
|
+
free-surface effect depends on the width of the liquid's surface but not its
|
|
53
|
+
depth, so for the same mass of cargo it grows in proportion to the density:
|
|
54
|
+
|
|
55
|
+
| Cargo (30 t/m) | Water: GM | Outcome | Mercury: GM | Outcome |
|
|
56
|
+
|--------------------|-----------|-------------------|-------------|-------------------|
|
|
57
|
+
| Solid | 1.00 m | stable | 1.37 m | stable |
|
|
58
|
+
| Liquid | −0.50 m | capsizes in ~12 s | −19.0 m | capsizes in ~4.5 s |
|
|
59
|
+
| Liquid, two tanks | 0.63 m | stable | −3.7 m | lolls at about 24° |
|
|
60
|
+
|
|
61
|
+
The mercury in two tanks makes GM negative, but the ship does not capsize: it
|
|
62
|
+
heels until its wider waterline makes up the difference, and settles at an
|
|
63
|
+
*angle of loll*. A layer of mercury only 0.18 m deep sloshes slowly, though,
|
|
64
|
+
so these timings are less reliable than water's (see Assumptions).
|
|
65
|
+
|
|
66
|
+
## Using it from Python
|
|
67
|
+
|
|
68
|
+
`floating_dynamics.scenario` sets the parameters, runs the simulations and
|
|
69
|
+
returns the results as plain data (lists, dicts and floats), for programs
|
|
70
|
+
which draw the ships themselves, such as a web page running
|
|
71
|
+
[Pyodide](https://pyodide.org):
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from floating_dynamics.scenario import Scenario, run
|
|
75
|
+
|
|
76
|
+
scenario = Scenario.from_dict({'heel': 10, 'cargoes': ['free', 'divided'],
|
|
77
|
+
'design': {'cargo_depth': 2, 'tanks': 3}})
|
|
78
|
+
result = run(scenario)
|
|
79
|
+
for ship in result.ships:
|
|
80
|
+
print(ship.ship.title, ship.summary())
|
|
81
|
+
data = result.to_dict() # JSON-serialisable
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The main parameters, with their defaults, are:
|
|
85
|
+
|
|
86
|
+
| Parameter | Default | Meaning |
|
|
87
|
+
|------------------------|-------------------------------|-----------------------------------------------|
|
|
88
|
+
| `cargoes` | `['solid', 'free', 'divided']`| which ships to simulate |
|
|
89
|
+
| `heel` | 5 | initial heel (degrees) |
|
|
90
|
+
| `duration`, `dt` | 60, 0.1 | length of the simulation, time per frame (s) |
|
|
91
|
+
| `design.cargo_depth` | 2.5 | depth of cargo when upright (m) |
|
|
92
|
+
| `design.cargo_density` | 1.0 | density of cargo (t/m³) |
|
|
93
|
+
| `design.tanks` | 2 | number of tanks for the `'divided'` cargo |
|
|
94
|
+
|
|
95
|
+
Unlike `free-surface --density`, setting `cargo_density` here does not change
|
|
96
|
+
the depth: set `cargo_depth` too to keep the mass the same. The ship's
|
|
97
|
+
dimensions, lightship mass and hydrodynamic coefficients are also fields of
|
|
98
|
+
`CoasterDesign` (in `floating_dynamics.ships`), with comments. Unknown or
|
|
99
|
+
unphysical parameters raise `ValueError`. The format of the results is
|
|
100
|
+
described in `scenario.py`. Only NumPy and SciPy are needed for this.
|
|
101
|
+
|
|
102
|
+
## Modules
|
|
103
|
+
|
|
104
|
+
- `geometry`: polygon area, centroid, second moment, clipping and filling.
|
|
105
|
+
- `body`: `FloatingBody` (hydrostatics, equations of motion, righting lever,
|
|
106
|
+
metacentric height) and `Compartment` (a tank of liquid).
|
|
107
|
+
- `ships`: a coaster's midship section and its three ways of carrying cargo.
|
|
108
|
+
- `simulation`: integrating the motion with SciPy's `solve_ivp`.
|
|
109
|
+
- `scenario`: the parameters and results, as plain data.
|
|
110
|
+
- `drawing`: animating ships side by side, with their heel against time.
|
|
111
|
+
- `free_surface`: the command-line demonstration.
|
|
112
|
+
|
|
113
|
+
## Assumptions
|
|
114
|
+
|
|
115
|
+
- The motion is two-dimensional: a cross-section of a long ship, rolling and
|
|
116
|
+
heaving but not pitching.
|
|
117
|
+
- The liquid's surface stays level (quasi-static). This is reasonable while
|
|
118
|
+
the liquid sloshes much faster than the ship rolls; the demonstration
|
|
119
|
+
prints both periods. Shallow layers slosh slowly (the 0.18 m of mercury
|
|
120
|
+
above takes 18 s across the whole hold), and then the real liquid would lag
|
|
121
|
+
behind the roll, so the motion would develop more slowly. Whether the ship
|
|
122
|
+
is stable depends only on GM, which is unaffected.
|
|
123
|
+
- The hull is watertight up to its main deck: there is no flooding, so a
|
|
124
|
+
capsized ship floats upside down.
|
|
125
|
+
- The surrounding water is represented by added mass and linear damping.
|
|
126
|
+
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
git clone https://github.com/xnx/floating-dynamics
|
|
130
|
+
cd floating-dynamics
|
|
131
|
+
pip install -e '.[animation,test]'
|
|
132
|
+
pytest
|
|
133
|
+
|
|
134
|
+
The editable install (`-e`) uses the source files directly, so changes take
|
|
135
|
+
effect without reinstalling. To run the demonstration without installing at
|
|
136
|
+
all, from the project folder:
|
|
137
|
+
|
|
138
|
+
PYTHONPATH=src python -m floating_dynamics
|
|
139
|
+
|
|
140
|
+
## Licence
|
|
141
|
+
|
|
142
|
+
MIT: see `LICENSE`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "floating-dynamics"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Two-dimensional dynamics of floating bodies, including the free-surface effect of liquid cargo."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Christian Hill" }]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
keywords = ["ship stability", "free-surface effect", "metacentric height",
|
|
11
|
+
"buoyancy", "simulation"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Education",
|
|
15
|
+
"Intended Audience :: Science/Research",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
18
|
+
]
|
|
19
|
+
# Matplotlib is needed only for the command-line animation (drawing.py and
|
|
20
|
+
# free_surface.py), not for the simulations or the scenario API.
|
|
21
|
+
dependencies = ["numpy", "scipy"]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
animation = ["matplotlib"]
|
|
25
|
+
test = ["pytest"]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
free-surface = "floating_dynamics.free_surface:main"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/xnx/floating-dynamics"
|
|
32
|
+
Issues = "https://github.com/xnx/floating-dynamics/issues"
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["setuptools>=77"]
|
|
36
|
+
build-backend = "setuptools.build_meta"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.dynamic]
|
|
39
|
+
version = { attr = "floating_dynamics.__version__" }
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""The dynamics of a two-dimensional body floating in the sea.
|
|
2
|
+
|
|
3
|
+
The model is a cross-section of a long body, so every area is really a volume
|
|
4
|
+
per unit length and every "area density" is a mass per unit length: with
|
|
5
|
+
lengths in m and densities in t.m-3, masses are in tonnes, moments of inertia
|
|
6
|
+
in t.m2 and forces in kN, per metre of the body's length. The sea surface is
|
|
7
|
+
the line y = 0, and the sea lies below it.
|
|
8
|
+
|
|
9
|
+
The body is rigid, apart from any liquid it carries in its compartments. Its
|
|
10
|
+
outline determines its buoyancy; its mass, M, centre of mass, G, and moment
|
|
11
|
+
of inertia, Iz, are given separately (for a ship, they depend on how it is
|
|
12
|
+
built and loaded), or calculated for a body of uniform density.
|
|
13
|
+
|
|
14
|
+
The body's state is the vector y = (X, Y, theta, U, V, omega):
|
|
15
|
+
(X, Y) is the position of G (excluding any liquid) in world coordinates;
|
|
16
|
+
theta is its angle of rotation, anticlockwise, from its initial shape;
|
|
17
|
+
(U, V) is the velocity of G and omega = dtheta/dt its angular velocity.
|
|
18
|
+
|
|
19
|
+
Gravity acts at G and buoyancy acts at the centre of buoyancy, B: the centroid
|
|
20
|
+
of the submerged part of the outline. The body is assumed to be watertight:
|
|
21
|
+
the whole of its outline displaces water, however deeply it is submerged.
|
|
22
|
+
|
|
23
|
+
The liquid in each compartment is treated quasi-statically: its surface is
|
|
24
|
+
always level, and its weight acts at its centroid, which moves across the
|
|
25
|
+
compartment as the body rolls (the "free-surface effect"). This is a good
|
|
26
|
+
approximation if the liquid sloshes quickly compared with the body's roll
|
|
27
|
+
(see sloshing_period). The liquid moves with the body as it heaves and, for
|
|
28
|
+
its moment of inertia, it is treated as a point mass at its centroid.
|
|
29
|
+
|
|
30
|
+
The surrounding water is modelled crudely, by:
|
|
31
|
+
added mass: the body must accelerate some water along with it, which is
|
|
32
|
+
modelled by increasing its mass for linear motion, and its moment of
|
|
33
|
+
inertia for rotation, by a fraction of themselves;
|
|
34
|
+
damping: the water resists motion (and carries energy away in waves),
|
|
35
|
+
which is modelled by linear damping of the linear and angular
|
|
36
|
+
velocities at fixed rates.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import numpy as np
|
|
41
|
+
from scipy.optimize import brentq
|
|
42
|
+
from .geometry import (area, make_ccw, centroid, second_moment, transform,
|
|
43
|
+
clip_below, fill, cross2)
|
|
44
|
+
|
|
45
|
+
# Acceleration due to gravity, m.s-2.
|
|
46
|
+
g = 9.81
|
|
47
|
+
# Density of sea water, t.m-3.
|
|
48
|
+
RHO_SEA = 1.027
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def mass_properties(poly, rho):
|
|
52
|
+
"""Return the mass, centre of mass and Iz of polygon poly of density rho.
|
|
53
|
+
|
|
54
|
+
Iz is the moment of inertia about the centre of mass.
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
xy = make_ccw(np.asarray(poly, dtype=float))
|
|
59
|
+
G = centroid(xy)
|
|
60
|
+
return rho * area(xy), G, rho * second_moment(xy, G)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def combine(*parts):
|
|
64
|
+
"""Return the mass, centre of mass and Iz of a body made of parts.
|
|
65
|
+
|
|
66
|
+
Each part is a tuple (M, G, Iz) of its mass, centre of mass and moment of
|
|
67
|
+
inertia about its centre of mass; the combined Iz is about the combined
|
|
68
|
+
centre of mass (by the parallel axis theorem).
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
M = sum(p[0] for p in parts)
|
|
73
|
+
G = sum(p[0] * np.asarray(p[1]) for p in parts) / M
|
|
74
|
+
Iz = sum(p[2] + p[0] * np.sum((np.asarray(p[1]) - G)**2) for p in parts)
|
|
75
|
+
return M, G, Iz
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Compartment:
|
|
79
|
+
"""A tank fixed in the body, holding a liquid whose surface stays level."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, tank, A_liquid, rho):
|
|
82
|
+
"""Initialize the compartment.
|
|
83
|
+
|
|
84
|
+
tank is a sequence of (x, y) vertices in the same coordinates as the
|
|
85
|
+
body's outline; it holds a liquid of area A_liquid and density rho.
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
self.tank = make_ccw(np.asarray(tank, dtype=float))
|
|
90
|
+
if not 0 <= A_liquid <= area(self.tank):
|
|
91
|
+
raise ValueError('The liquid does not fit in the tank.')
|
|
92
|
+
self.A_liquid, self.rho = A_liquid, rho
|
|
93
|
+
self.M = rho * A_liquid
|
|
94
|
+
|
|
95
|
+
def upright_liquid(self):
|
|
96
|
+
"""Return the polygon of the liquid when the body is upright."""
|
|
97
|
+
return fill(self.tank, self.A_liquid)
|
|
98
|
+
|
|
99
|
+
def sloshing_period(self):
|
|
100
|
+
"""Return the period of the liquid's slowest sloshing mode (s).
|
|
101
|
+
|
|
102
|
+
The liquid is approximated, when upright, as a rectangle as wide as
|
|
103
|
+
its free surface and of the same area, for which the angular
|
|
104
|
+
frequency of the slowest mode is sqrt((pi.g/b).tanh(pi.h/b)).
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
liquid = self.upright_liquid()
|
|
109
|
+
top = np.isclose(liquid[:, 1], liquid[:, 1].max())
|
|
110
|
+
b = np.ptp(liquid[top, 0])
|
|
111
|
+
h = self.A_liquid / b
|
|
112
|
+
return 2 * np.pi / np.sqrt(np.pi * g / b * np.tanh(np.pi * h / b))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class FloatingBody:
|
|
116
|
+
"""A body with a polygonal outline, carrying liquid in compartments."""
|
|
117
|
+
|
|
118
|
+
def __init__(self, outline, M, G, Iz, compartments=(), rho_water=RHO_SEA,
|
|
119
|
+
added_mass=0, added_inertia=0, heave_damping=0,
|
|
120
|
+
roll_damping=0):
|
|
121
|
+
"""Initialize the body.
|
|
122
|
+
|
|
123
|
+
outline is a sequence of (x, y) vertices for the body's shape. M is
|
|
124
|
+
its mass, G the position of its centre of mass (in the same
|
|
125
|
+
coordinates as outline) and Iz its moment of inertia about G, all
|
|
126
|
+
excluding the liquid in compartments, a sequence of Compartment
|
|
127
|
+
objects.
|
|
128
|
+
|
|
129
|
+
added_mass and added_inertia are the fractions by which the mass
|
|
130
|
+
(including the liquid) and Iz (excluding it) are increased by the
|
|
131
|
+
water which moves with the body. The damping rates (s-1) apply to the
|
|
132
|
+
linear and angular velocities, respectively.
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
xy = make_ccw(np.asarray(outline, dtype=float))
|
|
137
|
+
self.A = area(xy)
|
|
138
|
+
self.M, self.Iz = M, Iz
|
|
139
|
+
# The body's outline and tanks in its own frame of reference, with G
|
|
140
|
+
# at the origin.
|
|
141
|
+
self.G0 = np.asarray(G, dtype=float)
|
|
142
|
+
self.xy0 = xy - self.G0
|
|
143
|
+
self.compartments = compartments
|
|
144
|
+
self.tanks0 = [c.tank - self.G0 for c in compartments]
|
|
145
|
+
self.M_total = M + sum(c.M for c in compartments)
|
|
146
|
+
self.rho_water = rho_water
|
|
147
|
+
self.added_mass, self.added_inertia = added_mass, added_inertia
|
|
148
|
+
self.heave_damping, self.roll_damping = heave_damping, roll_damping
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def uniform(cls, poly, rho, **kwargs):
|
|
152
|
+
"""Return a solid body of uniform density rho with outline poly."""
|
|
153
|
+
return cls(poly, *mass_properties(poly, rho), **kwargs)
|
|
154
|
+
|
|
155
|
+
def to_world(self, xy, X, theta):
|
|
156
|
+
"""Return polygon xy, in the outline's coordinates, in the world.
|
|
157
|
+
|
|
158
|
+
The body's G is at X, and it is rotated by theta.
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
return transform(np.asarray(xy) - self.G0, theta, X)
|
|
163
|
+
|
|
164
|
+
def world(self, X, theta):
|
|
165
|
+
"""Return the body's outline with G at X, rotated by theta."""
|
|
166
|
+
return transform(self.xy0, theta, X)
|
|
167
|
+
|
|
168
|
+
def hydrostatics(self, X, theta):
|
|
169
|
+
"""Return the submerged polygon, its area and the centre of buoyancy.
|
|
170
|
+
|
|
171
|
+
If the body is out of the water, the centre of buoyancy is None.
|
|
172
|
+
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
submerged = clip_below(self.world(X, theta))
|
|
176
|
+
A_sub = area(submerged)
|
|
177
|
+
B = centroid(submerged) if A_sub > 0 else None
|
|
178
|
+
return submerged, A_sub, B
|
|
179
|
+
|
|
180
|
+
def liquids(self, X, theta):
|
|
181
|
+
"""Return the liquid in each compartment with G at X, rotated by theta.
|
|
182
|
+
|
|
183
|
+
The return value is a list of (polygon, mass, centroid) for each
|
|
184
|
+
compartment with any liquid in it.
|
|
185
|
+
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
out = []
|
|
189
|
+
for c, tank0 in zip(self.compartments, self.tanks0):
|
|
190
|
+
if c.M > 0:
|
|
191
|
+
liquid = fill(transform(tank0, theta, X), c.A_liquid)
|
|
192
|
+
out.append((liquid, c.M, centroid(liquid)))
|
|
193
|
+
return out
|
|
194
|
+
|
|
195
|
+
def centre_of_mass(self, X, theta):
|
|
196
|
+
"""Return the centre of mass of the body and its liquid."""
|
|
197
|
+
parts = [(self.M, X, 0)] + [(M_l, G_l, 0) for _, M_l, G_l
|
|
198
|
+
in self.liquids(X, theta)]
|
|
199
|
+
return combine(*parts)[1]
|
|
200
|
+
|
|
201
|
+
def equilibrium_height(self, theta):
|
|
202
|
+
"""Return the height of G at which the body floats at angle theta.
|
|
203
|
+
|
|
204
|
+
At this height the buoyant force balances the body's weight, but
|
|
205
|
+
there is a torque on it unless B lies directly below (or above) the
|
|
206
|
+
centre of mass of the body and its liquid.
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
A_float = self.M_total / self.rho_water
|
|
211
|
+
if A_float >= self.A:
|
|
212
|
+
raise ValueError('The body is too heavy to float.')
|
|
213
|
+
y = transform(self.xy0, theta)[:, 1]
|
|
214
|
+
def excess_area(Y):
|
|
215
|
+
return area(clip_below(self.world((0, Y), theta))) - A_float
|
|
216
|
+
return brentq(excess_area, -y.max(), -y.min())
|
|
217
|
+
|
|
218
|
+
def torque(self, X, theta):
|
|
219
|
+
"""Return the torque about G and the moment of inertia about G.
|
|
220
|
+
|
|
221
|
+
The torque is due to buoyancy and to the weight of the liquid; the
|
|
222
|
+
moment of inertia excludes the added inertia.
|
|
223
|
+
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
G = np.asarray(X)
|
|
227
|
+
_, A_sub, B = self.hydrostatics(G, theta)
|
|
228
|
+
Fb = np.array((0, self.rho_water * A_sub * g))
|
|
229
|
+
tau = cross2(B - G, Fb) if B is not None else 0
|
|
230
|
+
Iz = self.Iz
|
|
231
|
+
# The weight of each liquid acts at its centroid, G_l.
|
|
232
|
+
for _, M_l, G_l in self.liquids(G, theta):
|
|
233
|
+
r = G_l - G
|
|
234
|
+
tau += cross2(r, (0, -M_l * g))
|
|
235
|
+
Iz += M_l * r @ r
|
|
236
|
+
return tau, Iz
|
|
237
|
+
|
|
238
|
+
def righting_lever(self, theta):
|
|
239
|
+
"""Return the righting lever, GZ, of the body floating at angle theta.
|
|
240
|
+
|
|
241
|
+
GZ is the horizontal distance between the lines of action of the
|
|
242
|
+
weight and the buoyancy: it is positive if the resulting couple turns
|
|
243
|
+
the body back towards theta = 0.
|
|
244
|
+
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
X = (0, self.equilibrium_height(theta))
|
|
248
|
+
tau, _ = self.torque(X, theta)
|
|
249
|
+
return -np.sign(theta) * tau / (self.M_total * g)
|
|
250
|
+
|
|
251
|
+
def metacentric_height(self, dtheta=1e-4):
|
|
252
|
+
"""Return the metacentric height, GM, for small angles of heel.
|
|
253
|
+
|
|
254
|
+
This is the slope of the righting lever curve at theta = 0, and so
|
|
255
|
+
includes the free-surface effect of any liquid.
|
|
256
|
+
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
return self.righting_lever(dtheta) / dtheta
|
|
260
|
+
|
|
261
|
+
def roll_period(self):
|
|
262
|
+
"""Return the period of small, undamped rolls, or None if GM <= 0."""
|
|
263
|
+
GM = self.metacentric_height()
|
|
264
|
+
if GM <= 0:
|
|
265
|
+
return None
|
|
266
|
+
_, Iz = self.torque((0, self.equilibrium_height(0)), 0)
|
|
267
|
+
Iz += self.added_inertia * self.Iz
|
|
268
|
+
return 2 * np.pi * np.sqrt(Iz / (self.M_total * g * GM))
|
|
269
|
+
|
|
270
|
+
def rhs(self, t, y):
|
|
271
|
+
"""Return dy/dt for the state vector y, for scipy's solve_ivp."""
|
|
272
|
+
X, Y, theta, U, V, omega = y
|
|
273
|
+
G = np.array((X, Y))
|
|
274
|
+
_, A_sub, _ = self.hydrostatics(G, theta)
|
|
275
|
+
tau, Iz = self.torque(G, theta)
|
|
276
|
+
|
|
277
|
+
F = np.array((0, (self.rho_water * A_sub - self.M_total) * g))
|
|
278
|
+
a = F / (self.M_total * (1 + self.added_mass))
|
|
279
|
+
alpha = tau / (Iz + self.added_inertia * self.Iz)
|
|
280
|
+
a -= self.heave_damping * np.array((U, V))
|
|
281
|
+
alpha -= self.roll_damping * omega
|
|
282
|
+
return np.array((U, V, omega, a[0], a[1], alpha))
|