Coverage for src/swiift/lib/dr.py: 28%
91 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-09-11 16:23 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-09-11 16:23 +0200
1from __future__ import annotations
3import functools
4import typing
5import warnings
7import attrs
8import numpy as np
9import scipy.optimize as optimize
11if typing.TYPE_CHECKING:
12 from ..model.model import DiscreteSpectrum, FloatingIce, Ocean
15@attrs.define
16class FreeSurfaceSolver:
17 alphas: np.ndarray
18 depth: float
20 @classmethod
21 def from_ocean(cls, ocean: Ocean, spectrum: DiscreteSpectrum, gravity: float):
22 alphas = spectrum._ang_freqs_pow2 / gravity
23 return cls(alphas, ocean.depth)
25 def f(self, kk: float, alpha: float) -> float:
26 # Dispersion relation (form f(k) = 0), for a free surface,
27 # admitting one positive real root.
28 return kk * np.tanh(kk) - alpha
30 def df_dk(self, kk: float, alpha: float) -> float:
31 # Derivative of dr with respect to kk.
32 tt = np.tanh(kk)
33 return tt + kk * (1 - tt**2)
35 def find_k(self, k0, alpha):
36 res = optimize.root_scalar(self.f, args=(alpha,), fprime=self.df_dk, x0=alpha)
37 if not res.converged:
38 warnings.warn(
39 "Root finding did not converge: free surface",
40 stacklevel=2,
41 )
42 return res.root
44 def _compute_real_wavenumbers(self):
45 if np.isposinf(self.depth):
46 return self.alphas
48 coefs_d0 = self.alphas * self.depth
49 roots = np.full(len(coefs_d0), np.nan)
50 for i, _d0 in enumerate(coefs_d0):
51 if _d0 >= np.arctanh(np.nextafter(1, 0)):
52 roots[i] = _d0
53 continue
55 find_k_i = functools.partial(
56 self.find_k,
57 alpha=_d0,
58 )
59 roots[i] = find_k_i(_d0)
61 return roots / self.depth
63 def compute_wavenumbers(
64 self, real: bool = True, n: int | None = None
65 ) -> np.ndarray:
66 if real:
67 return self._compute_real_wavenumbers()
68 else:
69 raise NotImplementedError(
70 "Solving imaginary wavenumbers has not been implemented yet."
71 )
74@attrs.define
75class ElasticMassLoadingSolver:
76 alphas: np.ndarray
77 deg1: np.ndarray
78 deg0: np.ndarray
79 scaled_ratio: float
81 @classmethod
82 def from_floating(
83 cls,
84 ice: FloatingIce,
85 spectrum: DiscreteSpectrum,
86 gravity: float,
87 ):
88 alphas = spectrum._ang_freqs_pow2 / gravity
89 deg1 = 1 - alphas * ice.draft
90 deg0 = -alphas * ice.elastic_length
91 scaled_ratio = ice.dud / ice.elastic_length
92 return cls(alphas, deg1, deg0, scaled_ratio)
94 def f(self, kk: float, d0: float, d1: float, rr: float) -> float:
95 return (kk**5 + d1 * kk) * np.tanh(rr * kk) + d0
97 def df_dk(self, kk: float, d0: float, d1: float, rr: float) -> float:
98 return (5 * kk**4 + d1 + rr * d0) * np.tanh(rr * kk) + rr * (kk**5 + d1 * kk)
100 def extract_real_root(self, roots):
101 mask = (np.imag(roots) == 0) & (np.real(roots) > 0)
102 if mask.nonzero()[0].size != 1:
103 raise ValueError("An approximate initial guess could not be found")
104 return np.real(roots[mask][0])
106 def find_k(self, k0, alpha, d0, d1, rr):
107 res = optimize.root_scalar(
108 self.f,
109 args=(d0, d1, rr),
110 fprime=self.df_dk,
111 x0=k0,
112 xtol=1e-10,
113 )
114 if not res.converged:
115 warnings.warn(
116 "Root finding did not converge: ice-covered surface",
117 stacklevel=2,
118 )
119 return res.root
121 def _compute_real_wavenumbers(self):
122 roots = np.full(self.alphas.size, np.nan)
124 for i, (alpha, _d0, _d1) in enumerate(zip(self.alphas, self.deg0, self.deg1)):
125 find_k_i = functools.partial(
126 self.find_k,
127 alpha=alpha,
128 d0=_d0,
129 d1=_d1,
130 rr=self.scaled_ratio,
131 )
133 # We always expect one positive real root,
134 # and if _d1 < 0, eventually two additional negative real roots.
135 roots_dw = np.polynomial.polynomial.polyroots([_d0, _d1, 0, 0, 0, 1])
136 k0_dw = self.extract_real_root(roots_dw)
137 if np.isposinf(self.scaled_ratio):
138 roots[i] = k0_dw
139 continue
140 # Use a DW initial guess if |1-1/tanh(rr*k_DW)| < 0.15
141 # Use a SW initial guess if |1-rr*k_SW/tanh(rr*k_SW)| < 0.20
142 thrsld_dw, thrsld_sw = 1.33, 0.79
143 if self.scaled_ratio * k0_dw > thrsld_dw:
144 roots[i] = find_k_i(k0_dw)
145 else:
146 roots_sw = np.polynomial.polynomial.polyroots(
147 [_d0 / self.scaled_ratio, 0, _d1, 0, 0, 0, 1]
148 )
149 k0_sw = self.extract_real_root(roots_sw)
151 if self.scaled_ratio * k0_sw < thrsld_sw:
152 roots[i] = find_k_i(k0_sw)
153 # Use an initial guess in the middle otherwise
154 else:
155 k0_ = (k0_sw + k0_dw) / 2
156 roots[i] = find_k_i(k0_)
158 # -d_0 / alpha == L_D
159 return roots / (-self.deg0 / self.alphas)
161 def compute_wavenumbers(
162 self, real: bool = True, n: int | None = None
163 ) -> np.ndarray:
164 if real:
165 return self._compute_real_wavenumbers()
166 else:
167 raise NotImplementedError(
168 "Solving complex wavenumbers has not been implemented yet."
169 )