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 @@
1
+ from .parameters import Parameters as Parameters
@@ -0,0 +1,47 @@
1
+ """Module for Fourier series calculations."""
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+
6
+
7
+ def fourier_series_coeff(func, L: float, N: int) -> npt.NDArray[np.complex128]:
8
+ """Calculate the first N+1 Fourier series coeff. of a periodic function.
9
+
10
+ Given a periodic function f(x) with period 2L, this function returns the
11
+ coefficients {c0,c1,c2,...} such that:
12
+
13
+ f(x) ~= sum_{k=-N}^{N} c_k * exp(i*2*pi*k*x/L)
14
+
15
+ where we define c_{-n} = complex_conjugate(c_{n}).
16
+
17
+ :param func: The periodic function, a callable like f(x).
18
+ :param L: Half the period of the function f, so that f(-L)==f(L).
19
+ :param N: The function will return the first N + 1 Fourier coeff.
20
+ """
21
+ xx = np.linspace(-L, L, 2 * N, endpoint=False)
22
+ f_coeffs = np.fft.rfft(np.array([func(x) for x in xx])) / len(xx)
23
+ f_coeffs *= np.exp(1.0j * np.pi * np.arange(N + 1)) # Shift frequency domain
24
+
25
+ return np.arange(0, N + 1) * np.pi / L, f_coeffs
26
+
27
+
28
+ def convolution_coeff(f, g, L: float, N: int):
29
+ """Calculate the first N+1 Fourier series coeff. of a convolution.
30
+
31
+ Given periodic functions f(x), g(x) with period 2L, this function returns the
32
+ coefficients {c0,c1,c2,...} such that:
33
+
34
+ f(x) * g(x) ~= sum_{k=-N}^{N} c_k * exp(i*2*pi*k*x/L)
35
+
36
+ where we define c_{-n} = complex_conjugate(c_{n}).
37
+
38
+ :param f: A periodic function, a callable like f(x).
39
+ :param g: A periodic function, a callable like f(x).
40
+ :param L: Half the period of the function f, so that f(-L)==f(L).
41
+ :param N: The function will return the first 2N + 1 Fourier coeff.
42
+ """
43
+ omega, f_full = fourier_series_coeff(f, L, N)
44
+ _, g_full = fourier_series_coeff(g, L, N)
45
+ f_conv_full = 2 * L * f_full * g_full
46
+
47
+ return omega, f_conv_full
@@ -0,0 +1,32 @@
1
+ """Example light intensity and surface perturbations."""
2
+
3
+ from typing import Callable
4
+
5
+ import numpy as np
6
+ import scipy as sp
7
+
8
+
9
+ def gaussian(x: float, d=1.0) -> float:
10
+ return super_gaussian(x, 2.0, d)
11
+
12
+
13
+ def super_gaussian(x: float, k: float, d=1.0) -> float:
14
+ return np.exp(-(abs(x / d) ** k))
15
+
16
+
17
+ def square_wave(x: float) -> float:
18
+ return float(abs(x) < 1)
19
+
20
+
21
+ def smoothed_square(x: float, delta: float) -> float:
22
+ return 0.5 * (np.tanh((x + 1) / delta) - np.tanh((x - 1) / delta))
23
+
24
+
25
+ def mollifier(delta: float) -> Callable[[float], float]:
26
+ def _(x):
27
+ if abs(x) < delta:
28
+ return np.exp(-(delta**2) / (delta**2 - x**2))
29
+ else:
30
+ return 0.0
31
+
32
+ return lambda x: _(x) / sp.integrate.quad(_, -1.0, 1.0)[0]
@@ -0,0 +1,219 @@
1
+ """A module for the Parameters class."""
2
+
3
+ import inspect
4
+ from copy import copy
5
+ from dataclasses import asdict, dataclass
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Parameters:
13
+ """The Parameters for the model.
14
+
15
+ :param L: The aspect ratio of the domain.
16
+ :param Da_tr: The Damkohler number for the trans surfactant.
17
+ :param Da_ci: The Damkohler number for the cis surfactant.
18
+ :param Pe_tr: The Peclet number for the trans surfactant.
19
+ :param Pe_ci: The Peclet number for the cis surfactant.
20
+ :param Pe_tr_s: The Peclet number for the trans surfactant on the
21
+ interface.
22
+ :param Pe_ci_s: The Peclet number for the cis surfactant on the interface.
23
+ :param Bi_tr: The Biot number for the trans surfactant.
24
+ :param Bi_ci: The Biot number for the cis surfactant.
25
+ :param Ma: The Marangoni number.
26
+ :param k_tr: The adsorption rate for the trans surfactant.
27
+ :param k_ci: The adsorption rate for the cis surfactant.
28
+ :param chi_tr: The desorption rate for the trans surfactant.
29
+ :param chi_ci: The desorption rate for the cis surfactant.
30
+ """
31
+
32
+ # Aspect ratio
33
+ L: float = 10.0
34
+
35
+ # Reynolds numbers
36
+ Re: float = 0.0 # TODO: Deprecate this parameter
37
+
38
+ # Damkohler numbers
39
+ Da_tr: float = 1.0
40
+ Da_ci: float = 2.0
41
+
42
+ # Peclet numbers
43
+ Pe_tr: float = 10.0
44
+ Pe_ci: float = 10.0
45
+
46
+ # Interfacial Peclet numbers
47
+ Pe_tr_s: float = 10.0
48
+ Pe_ci_s: float = 10.0
49
+
50
+ # Biot numbers
51
+ Bi_tr: float = 1 / 300
52
+ Bi_ci: float = 1.0
53
+
54
+ # Marangoni number
55
+ Ma: float = 2.0
56
+
57
+ # Adsorption and desorption rates
58
+ k_tr: float = 1.0
59
+ k_ci: float = 1 / 30
60
+
61
+ chi_tr: float = 100 / 30
62
+ chi_ci: float = 100.0
63
+
64
+ def __post_init__(self): # noqa: D105
65
+ if not np.isclose(self.k_tr * self.chi_tr, self.k_ci * self.chi_ci):
66
+ raise ValueError(
67
+ "Adsorption rates do not satisfy the condition k * chi = const."
68
+ )
69
+
70
+ def from_dict(kwargs: dict[str, float]) -> "Parameters":
71
+ """Load parameters from a dictionary."""
72
+ return Parameters(
73
+ **{
74
+ k: v
75
+ for k, v in kwargs.items()
76
+ if k in inspect.signature(Parameters).parameters
77
+ }
78
+ )
79
+
80
+ def copy(self) -> "Parameters":
81
+ """Return a copy of the `Parameters` object."""
82
+ return copy(self)
83
+
84
+ def update(self, **new_kwargs) -> "Parameters":
85
+ """Return a new `Parameter` object with missing values derived."""
86
+ derived_kwargs = asdict(self)
87
+ # new_kwargs overwrite derived_kwargs
88
+ return Parameters(**(derived_kwargs | new_kwargs))
89
+
90
+ @property
91
+ def alpha(self) -> float:
92
+ return self.Da_ci / self.Da_tr
93
+
94
+ @property
95
+ def eta(self) -> float:
96
+ return self.Pe_tr / self.Pe_ci
97
+
98
+ @property
99
+ def zeta(self) -> float:
100
+ return self.Pe_tr * self.Da_tr + self.Pe_ci * self.Da_ci
101
+
102
+ @property
103
+ def P(self):
104
+ return np.array([[self.Pe_tr, 0.0], [0.0, self.Pe_ci]])
105
+
106
+ @property
107
+ def P_s(self):
108
+ return np.array([[self.Pe_tr_s, 0.0], [0.0, self.Pe_ci_s]])
109
+
110
+ @property
111
+ def B(self):
112
+ return np.array([[self.Bi_tr, 0.0], [0.0, self.Bi_ci]])
113
+
114
+ @property
115
+ def K(self):
116
+ return np.array([[self.k_tr, 0.0], [0.0, self.k_ci]])
117
+
118
+ @property
119
+ def A(self):
120
+ return self.P @ self._D
121
+
122
+ @property
123
+ def A_s(self):
124
+ return self.P_s @ self._D
125
+
126
+ @property
127
+ def V(self):
128
+ return np.array([[self.alpha, self.eta], [1.0, -1.0]])
129
+
130
+ @property
131
+ def Lambda(self):
132
+ return np.array([[0.0, 0.0], [0.0, self.zeta]])
133
+
134
+ @property
135
+ def _D(self):
136
+ return np.array([[self.Da_tr, -self.Da_ci], [-self.Da_tr, self.Da_ci]])
137
+
138
+
139
+ @dataclass
140
+ class PlottingParameters:
141
+ """Additional parameters for plotting.
142
+
143
+ :param wave_count: The number of wave numbers to use.
144
+ :param grid_size: The number of grid points to evaluate the solution on.
145
+ :param mollify: A flag to mollify the input function.
146
+ :param delta: The mollification parameter.
147
+ :param norm_scale: Normalization type. Either "linear" or "log".
148
+ :param save: A flag to save the figures to disk.
149
+ :param path: The path to save the figures to.
150
+ :param label: A label to append to the figure filenames.
151
+ :param format: The format to save the figures in.
152
+ """
153
+
154
+ wave_count: int = 100
155
+ grid_size: int = 1000
156
+ mollify: bool = False
157
+ delta: float = 0.5
158
+ norm_scale: str = "linear"
159
+ save: bool = False
160
+ path: str = "./"
161
+ label: str = ""
162
+ usetex: bool = False
163
+ format: str = "png"
164
+
165
+ def __post_init__(self): # noqa: D105
166
+ self.label = "_" + self.label if self.label else ""
167
+ self.plot_setup()
168
+
169
+ def from_dict(kwargs: dict[str, Any]) -> "PlottingParameters":
170
+ """Load parameters from a dictionary."""
171
+ return PlottingParameters(
172
+ **{
173
+ k: v
174
+ for k, v in kwargs.items()
175
+ if k in inspect.signature(PlottingParameters).parameters
176
+ }
177
+ )
178
+
179
+ def copy(self):
180
+ """Return a copy of the class."""
181
+ raise NotImplementedError
182
+ # TODO: Deprecate this method
183
+ return copy(self)
184
+
185
+ def plot_setup(self):
186
+ """Set up the matplotlib rcParams."""
187
+ import matplotlib.pyplot as plt
188
+
189
+ rcparams = {
190
+ "font.size": 18,
191
+ "axes.labelsize": 18,
192
+ "axes.titlesize": 18,
193
+ "axes.formatter.useoffset": True,
194
+ "xtick.labelsize": 16,
195
+ "ytick.labelsize": 16,
196
+ "legend.fontsize": 16,
197
+ "figure.figsize": [7, 6],
198
+ "figure.dpi": 100,
199
+ "figure.autolayout": True,
200
+ "savefig.dpi": 300,
201
+ }
202
+
203
+ if self.usetex:
204
+ plt.rcParams.update(
205
+ {
206
+ "text.usetex": True,
207
+ "font.family": "serif",
208
+ "font.serif": ["Computer Modern Roman"],
209
+ "axes.formatter.use_mathtext": True,
210
+ "text.latex.preamble": r"\usepackage{amsmath}",
211
+ }
212
+ | rcparams
213
+ )
214
+
215
+ else:
216
+ plt.rcParams.update(rcparams)
217
+
218
+ plt.close("all")
219
+ self.plt = plt
File without changes
File without changes
@@ -0,0 +1,19 @@
1
+ #! /bin/bash
2
+ echo "Plotting leading order..."
3
+ plot_leading_order --intensities -6.0 2.0 --log --count 13 -s --path "./figures/leading/" --usetex --format "eps"
4
+ plot_leading_order --interface --limits --intensities -6.0 2.0 --log --count 1000 -s --path "./figures/leading/" --usetex --format "eps"
5
+
6
+ echo "Plotting first order: laser pointer..."
7
+ plot_first_order -s --path "./figures/laser-point/" --usetex --format "eps"
8
+ echo "Plotting first order: mixing..."
9
+ plot_first_order --func "np.cos(2 * np.pi * x / params.L)" -s --path "./figures/mixing/" --usetex --format "eps"
10
+ echo "Plotting first order: inverse problem..."
11
+ plot_first_order --func "np.cos(np.pi * x / params.L) + np.cos(2 * np.pi * x / params.L) / 2 - np.cos(3 * np.pi * x / params.L) / 3" --problem inverse --wave_count 4 -s --path "./figures/inverse/" --usetex --format "eps"
12
+ echo "Plotting first order: inverse problem..."
13
+ plot_first_order --func "(np.sinh(1) / (np.cosh(1) - np.cos(np.pi * x / params.L)) - 1) / 2" --problem inverse --wave_count 30 -s --path "./figures/inverse-adv/" --usetex --format "eps"
14
+
15
+ echo "Plotting first order: sweeping parameters..."
16
+ plot_sweep -s --path "./figures/sweep/" --usetex --format "eps"
17
+
18
+ echo "Plotting convergence...:"
19
+ plot_error -s --path "" --usetex --format "eps"
@@ -0,0 +1,75 @@
1
+ #! /usr/bin/env python
2
+ from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
3
+
4
+ import matplotlib.pyplot as plt
5
+ import numpy as np
6
+
7
+ from photosurfactant.fourier import fourier_series_coeff
8
+ from photosurfactant.parameters import Parameters
9
+ from photosurfactant.semi_analytic.first_order import FirstOrder, Variables
10
+ from photosurfactant.semi_analytic.leading_order import LeadingOrder
11
+ from photosurfactant.utils.arg_parser import parameter_parser
12
+
13
+ WAVE_N = 5
14
+ GRID_N = 100
15
+
16
+
17
+ def plot_error(): # noqa: D103
18
+ parser = ArgumentParser(
19
+ description="Plot the first order surfactant concentrations.",
20
+ formatter_class=ArgumentDefaultsHelpFormatter,
21
+ )
22
+ parameter_parser(parser)
23
+ args = parser.parse_args()
24
+
25
+ # Extract parameters
26
+ kwargs = vars(args)
27
+ Da_tr = kwargs["Da_tr"]
28
+ Da_ci = kwargs["Da_ci"]
29
+
30
+ params = Parameters.from_dict(vars(args))
31
+
32
+ wavenumbers, func_coeffs = fourier_series_coeff(lambda x: 1.0, params.L, WAVE_N)
33
+
34
+ # Solve first order problem
35
+ leading = LeadingOrder(params)
36
+ first = FirstOrder(wavenumbers, params, leading)
37
+ first.solve(lambda n: (Variables.f, func_coeffs[n]))
38
+
39
+ # Solve the linearised system
40
+ yy = np.linspace(0, 1, GRID_N)
41
+ cc_tr_1 = np.array([first.c_tr(np.array([0]), y) for y in yy])[:, 0]
42
+ cc_tr_0 = leading.c_tr(yy)
43
+
44
+ # Set delta values
45
+ delta_values = 2.0 ** np.arange(0, -20, -1)
46
+ error = np.zeros((len(delta_values), 2))
47
+
48
+ for i, delta in enumerate(delta_values):
49
+ # Update parameters
50
+ kwargs["Da_tr"] = (1 + delta) * Da_tr
51
+ kwargs["Da_ci"] = (1 + delta) * Da_ci
52
+
53
+ params_delta = Parameters.from_dict(kwargs)
54
+ leading_delta = LeadingOrder(params_delta)
55
+ cc_tr_delta = leading_delta.c_tr(yy)
56
+
57
+ # Compute error
58
+ error[i, 0] = np.linalg.norm(cc_tr_delta - cc_tr_0)
59
+ error[i, 1] = np.linalg.norm(cc_tr_delta - (cc_tr_0 + delta * cc_tr_1))
60
+ print(
61
+ f"Error at delta = {delta:.2e}. Leading: {error[i, 0]:.2e}, "
62
+ f"first: {error[i, 1]:.2e}"
63
+ )
64
+
65
+ plt.loglog(delta_values, error[:, 0], "o-", label="Leading order")
66
+ plt.loglog(delta_values, error[:, 1], "o-", label="First order")
67
+ plt.loglog(delta_values, error[0, 0] * delta_values, "k:", label=r"O($\delta$)")
68
+ plt.loglog(
69
+ delta_values, error[0, 1] * delta_values**2, "k--", label=r"O($\delta^2$)"
70
+ )
71
+ plt.xlabel(r"$\delta$")
72
+ plt.ylabel("Error")
73
+
74
+ plt.legend()
75
+ plt.show()