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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ """Two-dimensional dynamics of floating bodies, including the free-surface
2
+ effect of liquid cargo."""
3
+
4
+ __version__ = '0.1.0'
@@ -0,0 +1,5 @@
1
+ """Run the free-surface demonstration with python -m floating_dynamics."""
2
+
3
+ from .free_surface import main
4
+
5
+ main()
@@ -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))