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,506 @@
1
+ #! /usr/bin/env python
2
+ from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
3
+
4
+ import numpy as np
5
+ from matplotlib import colors
6
+
7
+ from photosurfactant.fourier import convolution_coeff, fourier_series_coeff
8
+ from photosurfactant.intensity_functions import * # noqa: F401, F403
9
+ from photosurfactant.intensity_functions import mollifier
10
+ from photosurfactant.parameters import Parameters, PlottingParameters
11
+ from photosurfactant.semi_analytic.first_order import FirstOrder, Variables
12
+ from photosurfactant.semi_analytic.leading_order import LeadingOrder
13
+ from photosurfactant.utils.arg_parser import (
14
+ first_order_parser,
15
+ leading_order_parser,
16
+ parameter_parser,
17
+ plot_parser,
18
+ )
19
+ from photosurfactant.utils.func_parser import parse_func
20
+
21
+
22
+ class Figures:
23
+ """Figures for the first order problem."""
24
+
25
+ def __init__(self, first: FirstOrder, plot_params: PlottingParameters):
26
+ """Initialize the figures class."""
27
+ self.first = first
28
+ self.plot_params = plot_params
29
+
30
+ self.params = first.params
31
+ self.leading = first.leading
32
+
33
+ self.plt = plot_params.plt
34
+
35
+ self._initialize()
36
+
37
+ def _initialize(self):
38
+ self.xx = np.linspace(
39
+ -self.first.params.L, self.first.params.L, self.plot_params.grid_size
40
+ )
41
+ self.yy = np.linspace(0, 1, self.plot_params.grid_size)
42
+
43
+ self.ggamma_tr = self.first.Gamma_tr(self.xx)
44
+ self.ggamma_ci = self.first.Gamma_ci(self.xx)
45
+ self.ttension = (
46
+ -self.params.Ma
47
+ * (self.ggamma_tr + self.ggamma_ci)
48
+ / (1 - self.leading.Gamma_tr - self.leading.Gamma_ci)
49
+ )
50
+ self.JJ_tr = self.first.J_tr(self.xx)
51
+ self.JJ_ci = self.first.J_ci(self.xx)
52
+ self.SS = self.first.S(self.xx)
53
+ self.ff = self.first.f(self.xx)
54
+
55
+ self.psii = np.array([self.first.psi(self.xx, y) for y in self.yy])
56
+ self.ppressure = np.array([self.first.p(self.xx, y) for y in self.yy])
57
+ self.uu = np.array([self.first.u(self.xx, y) for y in self.yy])
58
+ self.vv = np.array([self.first.w(self.xx, y) for y in self.yy])
59
+ self.cc_tr = np.array([self.first.c_tr(self.xx, y) for y in self.yy])
60
+ self.cc_ci = np.array([self.first.c_ci(self.xx, y) for y in self.yy])
61
+
62
+ self.label = self.plot_params.label
63
+ self.format = self.plot_params.format
64
+
65
+ def export_data(self, path: str):
66
+ """Export data to a .csv file."""
67
+ np.savetxt(path + "xx.csv", self.xx, delimiter=",")
68
+ np.savetxt(path + "yy.csv", self.yy, delimiter=",")
69
+ np.savetxt(path + "ggamma_tr.csv", self.ggamma_tr, delimiter=",")
70
+ np.savetxt(path + "ggamma_ci.csv", self.ggamma_ci, delimiter=",")
71
+ np.savetxt(path + "ttension.csv", self.ttension, delimiter=",")
72
+ np.savetxt(path + "JJ_tr.csv", self.JJ_tr, delimiter=",")
73
+ np.savetxt(path + "JJ_ci.csv", self.JJ_ci, delimiter=",")
74
+ np.savetxt(path + "SS.csv", self.SS, delimiter=",")
75
+ np.savetxt(path + "ff.csv", self.ff, delimiter=",")
76
+ np.savetxt(path + "psii.csv", self.psii, delimiter=",")
77
+ np.savetxt(path + "ppressure.csv", self.ppressure, delimiter=",")
78
+ np.savetxt(path + "uu.csv", self.uu, delimiter=",")
79
+ np.savetxt(path + "vv.csv", self.vv, delimiter=",")
80
+ np.savetxt(path + "cc_tr.csv", self.cc_tr, delimiter=",")
81
+ np.savetxt(path + "cc_ci.csv", self.cc_ci, delimiter=",")
82
+
83
+ def plot_interfacial_velocity(self):
84
+ """Plot the interfacial velocity."""
85
+ self.plt.figure(figsize=(6, 5))
86
+ self.plt.plot(self.xx, self.uu[-1, :], "k-")
87
+ self.plt.xlabel(r"$x$")
88
+ self.plt.ylabel(r"Interfacial Velocity ($u_1$)")
89
+ self.plt.grid()
90
+ self.plt.ticklabel_format(style="sci", axis="y", scilimits=(0, 0))
91
+ self.plt.tight_layout()
92
+
93
+ if self.plot_params.save:
94
+ self.plt.savefig(
95
+ self.plot_params.path
96
+ + f"interfacial_velocity{self.label}.{self.format}",
97
+ bbox_inches="tight",
98
+ )
99
+ else:
100
+ self.plt.show()
101
+
102
+ def plot_streamplot(self):
103
+ """Plot the streamlines and velocity field."""
104
+ self.plt.figure(figsize=(12, 4))
105
+ self.plt.streamplot(self.xx, self.yy, self.uu, self.vv, color="black")
106
+ self.plt.imshow(
107
+ np.sqrt(self.uu**2 + self.vv**2),
108
+ extent=[-self.params.L, self.params.L, 0, 1],
109
+ origin="lower",
110
+ aspect="auto",
111
+ cmap="Reds",
112
+ )
113
+ cbar = self.plt.colorbar(label=r"$\lVert \mathbf{u}_1 \rVert$")
114
+ cbar.formatter.set_powerlimits((0, 0))
115
+ cbar.formatter.set_useMathText(True)
116
+ self.plt.xlabel(r"$x$")
117
+ self.plt.ylabel(r"$z$")
118
+ self.plt.tight_layout()
119
+
120
+ if self.plot_params.save:
121
+ self.plt.savefig(
122
+ self.plot_params.path + f"streamplot{self.label}.{self.format}",
123
+ bbox_inches="tight",
124
+ )
125
+ else:
126
+ self.plt.show()
127
+
128
+ def plot_streamlines(self):
129
+ """Plot the streamlines and velocity field."""
130
+ self.plt.figure(figsize=(12, 4))
131
+ self.plt.contour(self.xx, self.yy, self.psii, colors="black")
132
+ self.plt.imshow(
133
+ np.sqrt(self.uu**2 + self.vv**2),
134
+ extent=[-self.params.L, self.params.L, 0, 1],
135
+ origin="lower",
136
+ aspect="auto",
137
+ cmap="Reds",
138
+ )
139
+ cbar = self.plt.colorbar(label=r"$\lVert \mathbf{u}_1 \rVert$")
140
+ cbar.formatter.set_powerlimits((0, 0))
141
+ cbar.formatter.set_useMathText(True)
142
+ self.plt.xlabel(r"$x$")
143
+ self.plt.ylabel(r"$z$")
144
+ self.plt.tight_layout()
145
+
146
+ if self.plot_params.save:
147
+ self.plt.savefig(
148
+ self.plot_params.path + f"streamlines{self.label}.{self.format}",
149
+ bbox_inches="tight",
150
+ )
151
+ else:
152
+ self.plt.show()
153
+
154
+ def plot_concentration_crop(self, lims):
155
+ """Plot the concentration field."""
156
+ _, axs = self.plt.subplots(1, 3, figsize=(12, 4), constrained_layout=True)
157
+ conc_fields = [self.cc_tr, self.cc_ci, self.cc_tr + self.cc_ci]
158
+ ax_labels = [
159
+ r"$c_{\mathrm{tr}, 1}$",
160
+ r"$c_{\mathrm{ci}, 1}$",
161
+ r"$c_{\mathrm{tot}, 1}$",
162
+ ]
163
+
164
+ conc_lim = np.max(np.abs(conc_fields))
165
+ if self.plot_params.norm_scale == "linear":
166
+ norm = colors.CenteredNorm(vcenter=0.0, halfrange=conc_lim)
167
+ elif self.plot_params.norm_scale == "log":
168
+ norm = colors.SymLogNorm(
169
+ linthresh=1e-2 * conc_lim,
170
+ vmin=-conc_lim,
171
+ vmax=conc_lim,
172
+ )
173
+ else:
174
+ raise ValueError("Invalid norm scale.")
175
+
176
+ images = []
177
+ for ax, field, label in zip(axs.flat, conc_fields, ax_labels):
178
+ images.append(
179
+ ax.imshow(
180
+ field,
181
+ extent=[-self.params.L, self.params.L, 0, 1],
182
+ origin="lower",
183
+ aspect="auto",
184
+ cmap="coolwarm",
185
+ norm=norm,
186
+ )
187
+ )
188
+
189
+ ax.set_xlabel(r"$x$")
190
+ ax.set_ylabel(r"$z$")
191
+ ax.set_title(label)
192
+ ax.set_xlim(lims)
193
+
194
+ # Hide duplicate labels
195
+ for ax in axs.flat:
196
+ ax.label_outer()
197
+
198
+ cbar = self.plt.colorbar(
199
+ images[0], label="Concentration", ax=axs.ravel().tolist()
200
+ )
201
+
202
+ if self.plot_params.norm_scale == "linear":
203
+ cbar.formatter.set_powerlimits((0, 0))
204
+ cbar.formatter.set_useMathText(True)
205
+
206
+ if self.plot_params.save:
207
+ self.plt.savefig(
208
+ self.plot_params.path + f"concentration{self.label}.{self.format}",
209
+ bbox_inches="tight",
210
+ )
211
+ else:
212
+ self.plt.show()
213
+
214
+ def _plot_field(self, field, label, prefix=""):
215
+ """Plot the field of a given scalar."""
216
+ if self.plot_params.norm_scale == "linear":
217
+ norm = colors.CenteredNorm()
218
+ elif self.plot_params.norm_scale == "log":
219
+ conc_lim = np.max(np.abs(field))
220
+ norm = colors.SymLogNorm(
221
+ linthresh=1e-2 * conc_lim,
222
+ vmin=-conc_lim,
223
+ vmax=conc_lim,
224
+ )
225
+ else:
226
+ raise ValueError("Invalid norm scale.")
227
+
228
+ self.plt.figure(figsize=(12, 4))
229
+ self.plt.imshow(
230
+ field,
231
+ extent=[-self.params.L, self.params.L, 0, 1],
232
+ origin="lower",
233
+ aspect="auto",
234
+ cmap="coolwarm",
235
+ norm=norm,
236
+ )
237
+
238
+ cbar = self.plt.colorbar(label=label)
239
+
240
+ if self.plot_params.norm_scale == "linear":
241
+ cbar.formatter.set_powerlimits((0, 0))
242
+ cbar.formatter.set_useMathText(True)
243
+
244
+ self.plt.xlabel(r"$x$")
245
+ self.plt.ylabel(r"$z$")
246
+ self.plt.tight_layout()
247
+
248
+ if self.plot_params.save:
249
+ self.plt.savefig(
250
+ self.plot_params.path + prefix + f"{self.label}.{self.format}",
251
+ bbox_inches="tight",
252
+ )
253
+ else:
254
+ self.plt.show()
255
+
256
+ def plot_pressure(self):
257
+ """Plot the pressure field."""
258
+ self._plot_field(self.ppressure, r"$p_1$", "pressure")
259
+
260
+ def plot_concentration_tr(self):
261
+ """Plot the concentration field of the trans surfactant."""
262
+ self._plot_field(self.cc_tr, r"$c_{\mathrm{tr}, 1}$", "concentration_tr")
263
+
264
+ def plot_concentration_ci(self):
265
+ """Plot the concentration field of the cis surfactant."""
266
+ self._plot_field(self.cc_ci, r"$c_{\mathrm{ci}, 1}$", "concentration_ci")
267
+
268
+ def plot_concentration_tot(self):
269
+ """Plot the total surfactant concentration field."""
270
+ self._plot_field(
271
+ self.cc_tr + self.cc_ci,
272
+ r"$c_{\mathrm{tot}, 1}$",
273
+ "concentration_tot",
274
+ )
275
+
276
+ def plot_surface_excess(self):
277
+ """Plot the surface excess concentrations."""
278
+ self.plt.figure(figsize=(6, 5))
279
+
280
+ self.plt.plot(
281
+ self.xx,
282
+ self.ggamma_tr + self.ggamma_ci,
283
+ "k-",
284
+ label=r"$\Gamma_{\mathrm{tr}, 1} + \Gamma_{\mathrm{ci}, 1}$",
285
+ )
286
+ self.plt.plot(
287
+ self.xx, self.ggamma_tr, "r--", label=r"$\Gamma_{\mathrm{tr}, 1}$"
288
+ )
289
+ self.plt.plot(
290
+ self.xx, self.ggamma_ci, "b-.", label=r"$\Gamma_{\mathrm{ci}, 1}$"
291
+ )
292
+
293
+ self.plt.xlabel(r"$x$")
294
+ self.plt.ylabel("Surface Excess")
295
+ self.plt.legend(loc="lower right")
296
+ self.plt.grid()
297
+ self.plt.ticklabel_format(style="sci", axis="y", scilimits=(0, 0))
298
+ self.plt.tight_layout()
299
+
300
+ if self.plot_params.save:
301
+ self.plt.savefig(
302
+ self.plot_params.path + f"surface_excess{self.label}.{self.format}",
303
+ bbox_inches="tight",
304
+ )
305
+ else:
306
+ self.plt.show()
307
+
308
+ def plot_intensity(self):
309
+ """Plot the light intensity."""
310
+ self.plt.figure(figsize=(6, 5))
311
+
312
+ self.plt.plot(self.xx, self.ff, "k-", label="Approx")
313
+
314
+ self.plt.xlabel(r"$x$")
315
+ self.plt.ylabel(r"$f_1$")
316
+ self.plt.ticklabel_format(style="sci", axis="y", scilimits=(0, 0))
317
+ self.plt.tight_layout()
318
+
319
+ if self.plot_params.save:
320
+ self.plt.savefig(
321
+ self.plot_params.path + f"intensity{self.label}.{self.format}",
322
+ bbox_inches="tight",
323
+ )
324
+ else:
325
+ self.plt.show()
326
+
327
+ def plot_intensity_slip_tension(self):
328
+ """Plot the light intensity, interfacial slip velocity, and surface tension."""
329
+ _, ax1 = self.plt.subplots(figsize=(10, 4.5))
330
+
331
+ slip_plt = ax1.plot(self.xx, self.uu[-1, :], "k-", label=r"$u_1$")
332
+ ax1.set_xlabel(r"$x$")
333
+ ax1.set_ylabel(r"$u_1$")
334
+ ax1.ticklabel_format(style="sci", axis="y", scilimits=(0, 0))
335
+
336
+ ax2 = ax1.twinx()
337
+
338
+ tension_plt = ax2.plot(self.xx, self.ttension, "k--", label=r"$\gamma_1$")
339
+ ax2.set_ylabel(r"$\gamma_1$")
340
+ ax2.ticklabel_format(style="sci", axis="y", scilimits=(0, 0))
341
+
342
+ # Align and stretch axes (must occur before intensity plot)
343
+ offset, scale = 2, 0.5 # Relative location/scale of intensity
344
+ _, limit = ax1.get_ylim()
345
+ ax1.set_ylim(-limit, offset * limit)
346
+ _, limit = ax2.get_ylim()
347
+ ax2.set_ylim(-limit, offset * limit)
348
+
349
+ # Plot intensity on first axes scaled by slip velocity
350
+ max_u = np.max(self.uu[-1, :])
351
+ base, max_height = offset * max_u, scale * max_u
352
+ arrow_xx, arrow_ff = self.xx[::20], self.ff[::20]
353
+ ax1.quiver(
354
+ arrow_xx,
355
+ [base + 0.5 * max_height], # base of arrow
356
+ np.zeros_like(arrow_ff),
357
+ -(0.75 + arrow_ff), # arrow length (scaled by max_height)
358
+ scale=1 / max_height,
359
+ width=0.005,
360
+ scale_units="y",
361
+ color="b",
362
+ )
363
+
364
+ # Fix for having a single legend over multiple axes
365
+ plots = slip_plt + tension_plt
366
+ labels = [plot.get_label() for plot in plots]
367
+ ax1.legend(plots, labels, loc="lower right")
368
+
369
+ if self.plot_params.save:
370
+ self.plt.savefig(
371
+ self.plot_params.path
372
+ + f"intensity_slip_tension{self.label}.{self.format}",
373
+ bbox_inches="tight",
374
+ )
375
+ else:
376
+ self.plt.show()
377
+
378
+ def plot_interface(self):
379
+ """Plot the surface shape."""
380
+ self.plt.figure(figsize=(6, 5))
381
+
382
+ self.plt.plot(self.xx, self.SS, "k-", label="Approx")
383
+
384
+ self.plt.xlabel(r"$x$")
385
+ self.plt.ylabel(r"$S_1$")
386
+ self.plt.ticklabel_format(style="sci", axis="y", scilimits=(0, 0))
387
+ self.plt.tight_layout()
388
+
389
+ if self.plot_params.save:
390
+ self.plt.savefig(
391
+ self.plot_params.path + f"interface{self.label}.{self.format}",
392
+ bbox_inches="tight",
393
+ )
394
+ else:
395
+ self.plt.show()
396
+
397
+ def plot_surface_tension(self):
398
+ """Plot the surface tension."""
399
+ self.plt.figure(figsize=(6, 5))
400
+ self.plt.plot(self.xx, self.ttension, "k-")
401
+ self.plt.xlabel(r"$x$")
402
+ self.plt.ylabel(r"Surface Tension ($\gamma_1$)")
403
+ self.plt.grid()
404
+ self.plt.ticklabel_format(style="sci", axis="y", scilimits=(0, 0))
405
+ self.plt.tight_layout()
406
+
407
+ if self.plot_params.save:
408
+ self.plt.savefig(
409
+ self.plot_params.path + f"tension{self.label}.{self.format}",
410
+ bbox_inches="tight",
411
+ )
412
+ else:
413
+ self.plt.show()
414
+
415
+ def plot_fluxes(self):
416
+ """Plot the fluxes."""
417
+ self.plt.figure(figsize=(6, 5))
418
+ self.plt.plot(
419
+ self.xx,
420
+ self.JJ_tr + self.JJ_ci,
421
+ "k-",
422
+ label=r"$J_{\mathrm{tr}, 1} + J_{\mathrm{ci}, 1}$",
423
+ )
424
+ self.plt.plot(self.xx, self.JJ_tr, "r--", label=r"$J_{\mathrm{tr}, 1}$")
425
+ self.plt.plot(self.xx, self.JJ_ci, "b-.", label=r"$J_{\mathrm{ci}, 1}$")
426
+ self.plt.xlabel(r"$x$")
427
+ self.plt.ylabel("Kinetic Flux")
428
+ self.plt.legend(loc="lower right")
429
+ self.plt.grid()
430
+ self.plt.ticklabel_format(style="sci", axis="y", scilimits=(0, 0))
431
+ self.plt.tight_layout()
432
+
433
+ if self.plot_params.save:
434
+ self.plt.savefig(
435
+ self.plot_params.path + f"flux{self.label}.{self.format}",
436
+ bbox_inches="tight",
437
+ )
438
+ else:
439
+ self.plt.show()
440
+
441
+
442
+ def plot_first_order(): # noqa: D103
443
+ parser = ArgumentParser(
444
+ description="Plot the first order surfactant concentrations.",
445
+ formatter_class=ArgumentDefaultsHelpFormatter,
446
+ )
447
+ parameter_parser(parser)
448
+ plot_parser(parser)
449
+ leading_order_parser(parser)
450
+ first_order_parser(parser)
451
+ args = parser.parse_args()
452
+
453
+ params = Parameters.from_dict(vars(args))
454
+ plot_params = PlottingParameters.from_dict(vars(args))
455
+
456
+ root_index = args.root_index
457
+ func = parse_func(args.func)
458
+ problem = args.problem
459
+
460
+ # Calculate Fourier series coefficients
461
+ if args.mollify:
462
+ wavenumbers, func_coeffs = convolution_coeff(
463
+ lambda x: func(x, params),
464
+ mollifier(delta=args.delta), # noqa: F405
465
+ params.L,
466
+ plot_params.wave_count,
467
+ )
468
+ else:
469
+ wavenumbers, func_coeffs = fourier_series_coeff(
470
+ lambda x: func(x, params), params.L, plot_params.wave_count
471
+ )
472
+
473
+ # Solve leading order problem
474
+ leading = LeadingOrder(params, root_index)
475
+
476
+ # Solve first order problem
477
+ if problem == "forward":
478
+ constraint = lambda n: ( # noqa: E731
479
+ Variables.f,
480
+ func_coeffs[n],
481
+ )
482
+ elif problem == "inverse":
483
+ constraint = lambda n: ( # noqa: E731
484
+ (Variables.f, 0.0) if n == 0 else (Variables.S, func_coeffs[n])
485
+ )
486
+
487
+ first = FirstOrder(wavenumbers, params, leading)
488
+ first.solve(constraint)
489
+
490
+ # Plot figures
491
+ figures = Figures(first, plot_params)
492
+
493
+ figures.plot_streamplot()
494
+ figures.plot_streamlines()
495
+ figures.plot_pressure()
496
+ figures.plot_concentration_crop(lims=[-3.0, 3.0])
497
+ figures.plot_concentration_tr()
498
+ figures.plot_concentration_ci()
499
+ figures.plot_concentration_tot()
500
+ figures.plot_surface_excess()
501
+ figures.plot_surface_tension()
502
+ figures.plot_fluxes()
503
+ figures.plot_interfacial_velocity()
504
+ figures.plot_intensity()
505
+ figures.plot_intensity_slip_tension()
506
+ figures.plot_interface()