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.
- photosurfactant/__init__.py +1 -0
- photosurfactant/fourier.py +47 -0
- photosurfactant/intensity_functions.py +32 -0
- photosurfactant/parameters.py +219 -0
- photosurfactant/py.typed +0 -0
- photosurfactant/scripts/__init__.py +0 -0
- photosurfactant/scripts/plot_all.sh +19 -0
- photosurfactant/scripts/plot_error.py +75 -0
- photosurfactant/scripts/plot_first_order.py +506 -0
- photosurfactant/scripts/plot_leading_order.py +288 -0
- photosurfactant/scripts/plot_spectrum.py +146 -0
- photosurfactant/scripts/plot_sweep.py +234 -0
- photosurfactant/semi_analytic/__init__.py +3 -0
- photosurfactant/semi_analytic/first_order.py +540 -0
- photosurfactant/semi_analytic/leading_order.py +237 -0
- photosurfactant/semi_analytic/limits.py +240 -0
- photosurfactant/semi_analytic/utils.py +43 -0
- photosurfactant/utils/__init__.py +0 -0
- photosurfactant/utils/arg_parser.py +162 -0
- photosurfactant/utils/func_parser.py +10 -0
- photosurfactant-1.0.0.dist-info/METADATA +25 -0
- photosurfactant-1.0.0.dist-info/RECORD +24 -0
- photosurfactant-1.0.0.dist-info/WHEEL +4 -0
- photosurfactant-1.0.0.dist-info/entry_points.txt +6 -0
|
@@ -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
|
photosurfactant/py.typed
ADDED
|
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()
|