photosurfactant 1.0.0__py3-none-any.whl

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,540 @@
1
+ """First order solution to the photosurfactant model."""
2
+
3
+ from enum import Enum
4
+ from functools import wraps
5
+ from typing import Callable
6
+
7
+ import numpy as np
8
+
9
+ from photosurfactant.parameters import Parameters
10
+ from photosurfactant.semi_analytic.leading_order import LeadingOrder
11
+ from photosurfactant.semi_analytic.utils import Y, cosh, polyder, sinh, to_arr
12
+
13
+
14
+ class Symbols(Enum): # TODO: This is unnecessary
15
+ """Symbols used in the first order solution."""
16
+
17
+ A = "A"
18
+ B = "B"
19
+ C = "C"
20
+ D = "D"
21
+ E = "E"
22
+ F = "F"
23
+ G = "G"
24
+ H = "H"
25
+ gamma_tr = "Gamma_tr"
26
+ gamma_ci = "Gamma_ci"
27
+ J_tr = "J_tr"
28
+ J_ci = "J_ci"
29
+ S = "S"
30
+ f = "f"
31
+
32
+
33
+ class Variables(object): # TODO: Try to make this an enum
34
+ """Variables used in the first order solution."""
35
+
36
+ A = to_arr({Symbols.A: 1}, Symbols)
37
+ B = to_arr({Symbols.B: 1}, Symbols)
38
+ C = to_arr({Symbols.C: 1}, Symbols)
39
+ D = to_arr({Symbols.D: 1}, Symbols)
40
+ E = to_arr({Symbols.E: 1}, Symbols)
41
+ F = to_arr({Symbols.F: 1}, Symbols)
42
+ G = to_arr({Symbols.G: 1}, Symbols)
43
+ H = to_arr({Symbols.H: 1}, Symbols)
44
+ gamma_tr = to_arr({Symbols.gamma_tr: 1}, Symbols)
45
+ gamma_ci = to_arr({Symbols.gamma_ci: 1}, Symbols)
46
+ J_tr = to_arr({Symbols.J_tr: 1}, Symbols)
47
+ J_ci = to_arr({Symbols.J_ci: 1}, Symbols)
48
+ S = to_arr({Symbols.S: 1}, Symbols)
49
+ f = to_arr({Symbols.f: 1}, Symbols)
50
+
51
+
52
+ class FirstOrder(object):
53
+ """First order solution to the photosurfactant model."""
54
+
55
+ def __init__(
56
+ self,
57
+ wavenumbers: np.ndarray,
58
+ params: Parameters,
59
+ leading: LeadingOrder,
60
+ ):
61
+ """Initalise solution to the first order model.
62
+
63
+ :param wavenumbers: Array of wavenumbers.
64
+ :param params: :class:`~.parameters.Parameters` object containing the
65
+ model parameters.
66
+ :param leading: :class:`~.leadingrder.LeadingOrder` object containing
67
+ the leading order solution.
68
+ """
69
+ self.wavenumbers = wavenumbers
70
+ self.params = params
71
+ self.leading = leading
72
+
73
+ self.solution = np.zeros([len(self.wavenumbers), len(Symbols)], dtype=complex)
74
+
75
+ def solve(self, constraint: Callable[[int], tuple]):
76
+ """Initialize the first order solution.
77
+
78
+ :param constraint: Prescription to close the system. Should be a linear
79
+ function in the given variables.
80
+ """
81
+ zeros = np.zeros([len(Symbols) - 1], dtype=complex)
82
+
83
+ bc = BoundaryConditions(self)
84
+ for n, k in enumerate(self.wavenumbers):
85
+ # Formulate the boundary conditions
86
+ sys = bc.formulate(k)
87
+
88
+ # Apply prescrition
89
+ cond, val = constraint(n)
90
+ sys = np.vstack([sys, cond])
91
+
92
+ # Solve the system of equations
93
+ self.solution[n, :] = np.linalg.solve(sys, np.hstack([zeros, val]))
94
+
95
+ def _invert(self, func):
96
+ """Invert the function to real space."""
97
+
98
+ @wraps(func)
99
+ def wrapper(x, *args, x_order=0, **kwargs):
100
+ if x_order < 0:
101
+ raise ValueError("Cannot integrate arbitrary functions in x.")
102
+
103
+ vals = np.array([func(k, *args, **kwargs) for k in self.wavenumbers])
104
+ coeffs = np.einsum("ij,ij->i", self.solution, vals)
105
+
106
+ res = (
107
+ coeffs[0] * (x_order == 0)
108
+ + 2
109
+ * np.sum(
110
+ (1.0j * self.wavenumbers[1:, np.newaxis]) ** x_order
111
+ * coeffs[1:, np.newaxis]
112
+ * np.exp(
113
+ 1.0j
114
+ * self.wavenumbers[1:, np.newaxis]
115
+ * (x if isinstance(x, np.ndarray) else np.array([x]))[
116
+ np.newaxis, :
117
+ ]
118
+ ),
119
+ axis=0,
120
+ )
121
+ ).real
122
+
123
+ return res if isinstance(x, np.ndarray) else res[0]
124
+
125
+ return wrapper
126
+
127
+ # Real space variables
128
+ def psi(self, x, y, *, x_order=0, z_order=0):
129
+ """Stream function at first order."""
130
+ return self._invert(self._psi)(x, y, x_order=x_order, z_order=z_order)
131
+
132
+ def u(self, x, y, *, x_order=0, z_order=0):
133
+ """Horizontal velocity at first order."""
134
+ return self.psi(x, y, x_order=x_order, z_order=z_order + 1)
135
+
136
+ def w(self, x, y, *, x_order=0, z_order=0):
137
+ """Vertical velocity at first order."""
138
+ return -self.psi(x, y, x_order=x_order + 1, z_order=z_order)
139
+
140
+ def p(self, x, y, *, x_order=0, z_order=0):
141
+ """Pressure at first order."""
142
+ return self._invert(self._p)(x, y, x_order=x_order, z_order=z_order)
143
+
144
+ def c_tr(self, x, y, *, x_order=0, z_order=0):
145
+ """Concentration of trans surfactant at first order."""
146
+ return self._invert(self._c_tr)(x, y, x_order=x_order, z_order=z_order)
147
+
148
+ def c_ci(self, x, y, *, x_order=0, z_order=0):
149
+ """Concentration of cis surfactant at first order."""
150
+ return self._invert(self._c_ci)(x, y, x_order=x_order, z_order=z_order)
151
+
152
+ def i_c_tr(self, x, y, y_s=0, *, x_order=0):
153
+ """Integral of trans surfactant concentration at first order."""
154
+ return self._invert(self._c_tr_i)(x, y, y_s, x_order=x_order)
155
+
156
+ def i_c_ci(self, x, y, y_s=0, *, x_order=0):
157
+ """Integral of cis surfactant concentration at first order."""
158
+ return self._invert(self._c_ci_i)(x, y, y_s, x_order=x_order)
159
+
160
+ def Gamma_tr(self, x, *, x_order=0):
161
+ """Surface excess of trans surfactant at first order."""
162
+ return self._invert(lambda k: Variables.gamma_tr)(x, x_order=x_order)
163
+
164
+ def Gamma_ci(self, x, *, x_order=0):
165
+ """Surface excess of cis surfactant at first order."""
166
+ return self._invert(lambda k: Variables.gamma_ci)(x, x_order=x_order)
167
+
168
+ def J_tr(self, x, *, x_order=0):
169
+ """Kinetic flux of trans surfactant at first order."""
170
+ return self._invert(lambda k: Variables.J_tr)(x, x_order=x_order)
171
+
172
+ def J_ci(self, x, *, x_order=0):
173
+ """Kinetic flux of cis surfactant at first order."""
174
+ return self._invert(lambda k: Variables.J_ci)(x, x_order=x_order)
175
+
176
+ def S(self, x, *, x_order=0):
177
+ """Interface shape at first order."""
178
+ return self._invert(lambda k: Variables.S)(x, x_order=x_order)
179
+
180
+ def f(self, x, *, x_order=0):
181
+ """Light intensity at first order."""
182
+ return self._invert(lambda k: Variables.f)(x, x_order=x_order)
183
+
184
+ def gamma(self, x, *, x_order=0):
185
+ """Surface tension at first order."""
186
+ return self._invert(lambda k: self._gamma)(x, x_order=x_order)
187
+
188
+ # Fourier space variables
189
+ def _psi(self, k, z, z_order=0):
190
+ """Stream function at first order in Fourier space."""
191
+ if k == 0:
192
+ return (
193
+ Variables.A * polyder(Y**3, z_order)(z)
194
+ + Variables.B * polyder(Y**2, z_order)(z)
195
+ + Variables.C * polyder(Y, z_order)(z)
196
+ + Variables.D * polyder(Y**0, z_order)(z)
197
+ )
198
+ else:
199
+ return (
200
+ Variables.A * k ** (z_order - 1) * (z_order + k * z) * np.exp(k * z)
201
+ + Variables.B * k**z_order * np.exp(k * z)
202
+ + Variables.C
203
+ * (-k) ** (z_order - 1)
204
+ * (z_order - k * z)
205
+ * np.exp(-k * z)
206
+ + Variables.D * (-k) ** z_order * np.exp(-k * z)
207
+ )
208
+
209
+ def _p(self, k, z, z_order=0):
210
+ """Pressure at first order in Fourier space."""
211
+ if k == 0:
212
+ return 0 * Variables.f
213
+ else:
214
+ return (
215
+ self._psi(k, z, z_order=z_order + 3)
216
+ - k**2 * self._psi(k, z, z_order=z_order + 1)
217
+ ) / (1.0j * k)
218
+
219
+ def _c_tr(self, k, z, z_order=0):
220
+ """Concentration of trans surfactant at first order in Fourier space."""
221
+ return self._c(k, z, z_order)[0, ...]
222
+
223
+ def _c_ci(self, k, z, z_order=0):
224
+ """Concentration of cis surfactant at first order in Fourier space."""
225
+ return self._c(k, z, z_order)[1, ...]
226
+
227
+ def _c(self, k, z, z_order=0):
228
+ """Concentration of surfactant at first order in Fourier space."""
229
+ return self.params.V @ self._q(k, z, z_order)
230
+
231
+ def _c_tr_i(self, k, z, z_s=0):
232
+ """Integral of the trans concentration at first order in Fourier space."""
233
+ return self._c_i(k, z, z_s)[0, ...]
234
+
235
+ def _c_ci_i(self, k, z, z_s=0):
236
+ """Integral of the cis concentration at first order in Fourier space."""
237
+ return self._c_i(k, z, z_s)[1, ...]
238
+
239
+ def _c_i(self, k, z, z_s=0):
240
+ """Integral of the surfactant concentration at first order in Fourier space."""
241
+ return np.einsum("ij,j...->i...", self.params.V, self._q_i(k, z, z_s))
242
+
243
+ @property
244
+ def _gamma(self):
245
+ """Surface tension at first order in Fourier space."""
246
+ return (
247
+ -self.params.Ma
248
+ * (Variables.gamma_tr + Variables.gamma_ci)
249
+ / (1 - self.leading.Gamma_tr - self.leading.Gamma_ci)
250
+ )
251
+
252
+ # Private variables
253
+ def _q(self, k, z, z_order=0):
254
+ return (
255
+ self._q_0(k, z, z_order)
256
+ + self._q_1(k, z, z_order)
257
+ + self._q_2(k, z, z_order)
258
+ )
259
+
260
+ def _q_i(self, k, z, z_s=0):
261
+ return self._q(k, z, z_order=-1) - self._q(k, z_s, z_order=-1)
262
+
263
+ def _q_0(self, k, z, z_order=0):
264
+ zeta = self.params.zeta
265
+ if k == 0:
266
+ return np.array(
267
+ [
268
+ Variables.E * polyder(Y, z_order)(z)
269
+ + Variables.F * polyder(Y**0, z_order)(z),
270
+ np.sqrt(zeta) ** z_order
271
+ * (
272
+ Variables.G * np.exp(z * np.sqrt(zeta))
273
+ + Variables.H * (-1) ** z_order * np.exp(-z * np.sqrt(zeta))
274
+ ),
275
+ ]
276
+ )
277
+ else:
278
+ return np.array(
279
+ [
280
+ k**z_order
281
+ * (
282
+ Variables.E * sinh(k * z, z_order)
283
+ + Variables.F * cosh(k * z, z_order)
284
+ ),
285
+ np.sqrt(zeta + k**2) ** z_order
286
+ * (
287
+ Variables.G * np.exp(z * np.sqrt(zeta + k**2))
288
+ + Variables.H
289
+ * (-1) ** z_order
290
+ * np.exp(-z * np.sqrt(zeta + k**2))
291
+ ),
292
+ ]
293
+ )
294
+
295
+ def _q_1(self, k, z, z_order=0):
296
+ zeta = self.params.zeta
297
+ if k == 0:
298
+ return (
299
+ Variables.f
300
+ * self.leading.B
301
+ * np.sqrt(zeta) ** z_order
302
+ / 2
303
+ * (
304
+ z_order * cosh(z * np.sqrt(zeta), z_order)
305
+ + z * np.sqrt(zeta) * sinh(z * np.sqrt(zeta), z_order)
306
+ )
307
+ )[np.newaxis, :] * np.array([0, 1])[:, np.newaxis]
308
+ else:
309
+ return (
310
+ Variables.f
311
+ * np.sqrt(zeta) ** z_order
312
+ * -self.leading.B
313
+ * zeta
314
+ / k**2
315
+ * cosh(z * np.sqrt(zeta), z_order)
316
+ )[np.newaxis, :] * np.array([0, 1])[:, np.newaxis]
317
+
318
+ def _q_2(self, k, z, z_order=0):
319
+ alpha, eta, zeta = self.params.alpha, self.params.eta, self.params.zeta
320
+ null = to_arr(dict(), Symbols)
321
+ if k == 0:
322
+ return np.array([null, null])
323
+ else:
324
+ return (
325
+ -1.0j
326
+ * k
327
+ * self.leading.B
328
+ * np.sqrt(zeta)
329
+ * self.params.Pe_ci
330
+ / (2 * (alpha + eta))
331
+ * np.einsum(
332
+ "ij...,j->i...",
333
+ np.array(
334
+ [
335
+ [self._q_2_scalar(k, z, 0, z_order), null],
336
+ [null, self._q_2_scalar(k, z, 1, z_order)],
337
+ ]
338
+ ),
339
+ np.array(
340
+ [
341
+ eta**2 - eta,
342
+ eta**2 + alpha,
343
+ ]
344
+ ),
345
+ )
346
+ )
347
+
348
+ def _q_2_scalar(self, k, z, index, z_order=0):
349
+ return (
350
+ self._q_2_gfunc(
351
+ k, z, self._a_c(k, index), self._b_c(k, index), 1, 1, z_order
352
+ )
353
+ + self._q_2_gfunc(
354
+ k, z, self._c_c(k, index), self._d_c(k, index), 1, -1, z_order
355
+ )
356
+ + self._q_2_gfunc(
357
+ k, z, self._e_c(k, index), self._f_c(k, index), -1, -1, z_order
358
+ )
359
+ + self._q_2_gfunc(
360
+ k, z, self._g_c(k, index), self._h_c(k, index), -1, 1, z_order
361
+ )
362
+ )
363
+
364
+ def _q_2_gfunc(self, k, z, a, b, s_1, s_2, z_order=0):
365
+ return (
366
+ z_order * self._q_2_base(k, s_1, s_2) ** (z_order - 1) * a
367
+ + self._q_2_base(k, s_1, s_2) ** z_order * (a * z + b)
368
+ ) * np.exp(self._q_2_base(k, s_1, s_2) * z)
369
+
370
+ def _q_2_base(self, k, s_1, s_2):
371
+ return s_1 * (k + s_2 * np.sqrt(self.params.zeta))
372
+
373
+ def _a_c(self, k, index):
374
+ zeta = self.params.zeta
375
+ return Variables.A / (2 * k * np.sqrt(zeta) + zeta * (index == 0))
376
+
377
+ def _c_c(self, k, index):
378
+ zeta = self.params.zeta
379
+ return Variables.A / (2 * k * np.sqrt(zeta) - zeta * (index == 0))
380
+
381
+ def _e_c(self, k, index):
382
+ zeta = self.params.zeta
383
+ return -Variables.C / (2 * k * np.sqrt(zeta) - zeta * (index == 0))
384
+
385
+ def _g_c(self, k, index):
386
+ zeta = self.params.zeta
387
+ return -Variables.C / (2 * k * np.sqrt(zeta) + zeta * (index == 0))
388
+
389
+ def _b_c(self, k, index):
390
+ zeta = self.params.zeta
391
+ return (Variables.B - 2 * self._a_c(k, index) * (k + np.sqrt(zeta))) / (
392
+ 2 * k * np.sqrt(zeta) + zeta * (index == 0)
393
+ )
394
+
395
+ def _d_c(self, k, index):
396
+ zeta = self.params.zeta
397
+ return (Variables.B + 2 * self._c_c(k, index) * (k - np.sqrt(zeta))) / (
398
+ 2 * k * np.sqrt(zeta) - zeta * (index == 0)
399
+ )
400
+
401
+ def _f_c(self, k, index):
402
+ zeta = self.params.zeta
403
+ return -(Variables.D + 2 * self._e_c(k, index) * (k - np.sqrt(zeta))) / (
404
+ 2 * k * np.sqrt(zeta) - zeta * (index == 0)
405
+ )
406
+
407
+ def _h_c(self, k, index):
408
+ zeta = self.params.zeta
409
+ return -(Variables.D - 2 * self._g_c(k, index) * (k + np.sqrt(zeta))) / (
410
+ 2 * k * np.sqrt(zeta) + zeta * (index == 0)
411
+ )
412
+
413
+
414
+ class BoundaryConditions(object):
415
+ """Boundary conditions for the photosurfactant model."""
416
+
417
+ def __init__(self, first: FirstOrder):
418
+ """Initialise boundary conditions for the photosurfactant model.
419
+
420
+ :param first: :class:`~.first_order.FirstOrder` object containing the
421
+ first order solution.
422
+ """
423
+ self.params = first.params
424
+ self.leading = first.leading
425
+ self.first = first
426
+
427
+ def formulate(self, k):
428
+ """Formulate the boundary conditions."""
429
+ return np.vstack(
430
+ [
431
+ self.no_slip(k),
432
+ self.normal_stress(k),
433
+ self.tangential_stress(k),
434
+ self.kinematic(k),
435
+ self.no_flux(k),
436
+ self.kinetic_flux(k),
437
+ self.surface_excess(k),
438
+ self.mass_balance(k),
439
+ ]
440
+ )
441
+
442
+ def no_slip(self, k):
443
+ """No slip boundary condition."""
444
+ return np.array(
445
+ [
446
+ self.first._psi(k, 0),
447
+ self.first._psi(k, 0, z_order=1),
448
+ ]
449
+ )
450
+
451
+ def normal_stress(self, k):
452
+ """Normal stress boundary condition.""" # noqa: D401
453
+ if k == 0:
454
+ return self.first._psi(k, 1, z_order=3)[np.newaxis, ...]
455
+ else:
456
+ return (
457
+ self.first._psi(k, 1, z_order=3)
458
+ - 3 * k**2 * self.first._psi(k, 1, z_order=1)
459
+ - 1.0j * k**3 * self.leading.gamma * Variables.S
460
+ )[np.newaxis, ...]
461
+
462
+ def tangential_stress(self, k):
463
+ """Tangential stress boundary condition."""
464
+ if k == 0:
465
+ return self.first._psi(k, 1, z_order=2)[np.newaxis, ...]
466
+ else:
467
+ return (
468
+ self.first._psi(k, 1, z_order=2)
469
+ + k**2 * self.first._psi(k, 1)
470
+ - 1.0j * k * self.first._gamma
471
+ )[np.newaxis, ...]
472
+
473
+ def kinematic(self, k):
474
+ """Kinematic boundary condition."""
475
+ if k == 0:
476
+ # Replace with conservation of mass
477
+ return Variables.S[np.newaxis, ...]
478
+ else:
479
+ return self.first._psi(k, 1)[np.newaxis, ...]
480
+
481
+ def no_flux(self, k):
482
+ """No flux boundary condition."""
483
+ return self.first._c(k, 0, z_order=1)
484
+
485
+ def kinetic_flux(self, k):
486
+ """Kinetic flux boundary condition."""
487
+ return self.params.B @ (
488
+ self.params.K
489
+ @ (
490
+ (
491
+ self.first._c(k, 1)
492
+ + Variables.S[np.newaxis, :]
493
+ * self.leading.c(1, z_order=1)[:, np.newaxis]
494
+ )
495
+ * (1 - self.leading.Gamma_tr - self.leading.Gamma_ci)
496
+ - self.leading.c(1)[:, np.newaxis]
497
+ * (Variables.gamma_tr + Variables.gamma_ci)
498
+ )
499
+ - np.array([Variables.gamma_tr, Variables.gamma_ci])
500
+ ) - np.array([Variables.J_tr, Variables.J_ci])
501
+
502
+ def surface_excess(self, k):
503
+ """Surface excess boundary condition."""
504
+ gamma_vec = np.array([Variables.gamma_tr, Variables.gamma_ci])
505
+ J_vec = np.array([Variables.J_tr, Variables.J_ci])
506
+
507
+ d_psi_vec = self.first._psi(k, 1, z_order=1)
508
+
509
+ return (
510
+ 1.0j
511
+ * k
512
+ * (self.params.P_s @ self.leading.Gamma)[:, np.newaxis]
513
+ * d_psi_vec[np.newaxis, :] # Fix this line
514
+ + k**2 * gamma_vec
515
+ - self.params.P_s @ J_vec
516
+ + self.params.A_s
517
+ @ (
518
+ np.array([Variables.gamma_tr, Variables.gamma_ci])
519
+ + Variables.f[np.newaxis, :] * self.leading.Gamma[:, np.newaxis]
520
+ )
521
+ )
522
+
523
+ def mass_balance(self, k):
524
+ """Mass balance boundary condition."""
525
+ cond = (self.params.k_tr * self.params.chi_tr) * (
526
+ self.first._c(k, 1, z_order=1)
527
+ + Variables.S[np.newaxis, :] * self.leading.c(1, z_order=2)[:, np.newaxis]
528
+ ) + self.params.P @ np.array([Variables.J_tr, Variables.J_ci])
529
+
530
+ if k == 0:
531
+ # Replace one mass balance with conservation of surfactant
532
+ cond[0, ...] = (
533
+ 1
534
+ / (self.params.k_tr * self.params.chi_tr)
535
+ * (Variables.gamma_tr + Variables.gamma_ci)
536
+ + self.first._c_tr_i(k, 1)
537
+ + self.first._c_ci_i(k, 1)
538
+ )
539
+
540
+ return cond