photosurfactant 1.0.0__tar.gz
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-1.0.0/.github/workflows/publish.yml +31 -0
- photosurfactant-1.0.0/.github/workflows/pytest.yml +69 -0
- photosurfactant-1.0.0/.gitignore +175 -0
- photosurfactant-1.0.0/PKG-INFO +25 -0
- photosurfactant-1.0.0/README.md +13 -0
- photosurfactant-1.0.0/examples/slip-velocity.py +36 -0
- photosurfactant-1.0.0/pyproject.toml +36 -0
- photosurfactant-1.0.0/src/photosurfactant/__init__.py +1 -0
- photosurfactant-1.0.0/src/photosurfactant/fourier.py +47 -0
- photosurfactant-1.0.0/src/photosurfactant/intensity_functions.py +32 -0
- photosurfactant-1.0.0/src/photosurfactant/parameters.py +219 -0
- photosurfactant-1.0.0/src/photosurfactant/py.typed +0 -0
- photosurfactant-1.0.0/src/photosurfactant/scripts/__init__.py +0 -0
- photosurfactant-1.0.0/src/photosurfactant/scripts/plot_all.sh +19 -0
- photosurfactant-1.0.0/src/photosurfactant/scripts/plot_error.py +75 -0
- photosurfactant-1.0.0/src/photosurfactant/scripts/plot_first_order.py +506 -0
- photosurfactant-1.0.0/src/photosurfactant/scripts/plot_leading_order.py +288 -0
- photosurfactant-1.0.0/src/photosurfactant/scripts/plot_spectrum.py +146 -0
- photosurfactant-1.0.0/src/photosurfactant/scripts/plot_sweep.py +234 -0
- photosurfactant-1.0.0/src/photosurfactant/semi_analytic/__init__.py +3 -0
- photosurfactant-1.0.0/src/photosurfactant/semi_analytic/first_order.py +540 -0
- photosurfactant-1.0.0/src/photosurfactant/semi_analytic/leading_order.py +237 -0
- photosurfactant-1.0.0/src/photosurfactant/semi_analytic/limits.py +240 -0
- photosurfactant-1.0.0/src/photosurfactant/semi_analytic/utils.py +43 -0
- photosurfactant-1.0.0/src/photosurfactant/utils/__init__.py +0 -0
- photosurfactant-1.0.0/src/photosurfactant/utils/arg_parser.py +162 -0
- photosurfactant-1.0.0/src/photosurfactant/utils/func_parser.py +10 -0
- photosurfactant-1.0.0/tests/__init__.py +0 -0
- photosurfactant-1.0.0/tests/test_semi_analytic/__init__.py +0 -0
- photosurfactant-1.0.0/tests/test_semi_analytic/test_first_fourier.py +226 -0
- photosurfactant-1.0.0/tests/test_semi_analytic/test_first_order.py +254 -0
- photosurfactant-1.0.0/tests/test_semi_analytic/test_leading_order.py +87 -0
- photosurfactant-1.0.0/uv.lock +680 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: "Publish"
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
# Publish on any tag starting with a `v`, e.g., v0.1.0
|
|
7
|
+
- v*
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
run:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment:
|
|
13
|
+
name: pypi
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write
|
|
16
|
+
contents: read
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout
|
|
19
|
+
uses: actions/checkout@v6
|
|
20
|
+
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v7
|
|
23
|
+
|
|
24
|
+
- name: Install Python 3.13
|
|
25
|
+
run: uv python install 3.13
|
|
26
|
+
|
|
27
|
+
- name: Build
|
|
28
|
+
run: uv build
|
|
29
|
+
|
|
30
|
+
- name: Publish
|
|
31
|
+
run: uv publish
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
# Trigger the workflow on push to main or dev, except tag creation
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- 'main'
|
|
8
|
+
- 'dev'
|
|
9
|
+
# Trigger the workflow on pull request
|
|
10
|
+
pull_request: ~
|
|
11
|
+
# Trigger the workflow manually
|
|
12
|
+
workflow_dispatch: ~
|
|
13
|
+
# Trigger after public PR approved for CI
|
|
14
|
+
pull_request_target:
|
|
15
|
+
types: [labeled]
|
|
16
|
+
release:
|
|
17
|
+
types: [created]
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
qa:
|
|
21
|
+
name: qa
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- name: Checkout Repository
|
|
26
|
+
uses: actions/checkout@v4
|
|
27
|
+
|
|
28
|
+
- name: Install uv
|
|
29
|
+
uses: astral-sh/setup-uv@v5
|
|
30
|
+
with:
|
|
31
|
+
version: "0.7.3"
|
|
32
|
+
|
|
33
|
+
- name: Install Python
|
|
34
|
+
run: uv python install
|
|
35
|
+
|
|
36
|
+
- name: Install the project
|
|
37
|
+
run: uv sync --locked --all-extras --dev
|
|
38
|
+
|
|
39
|
+
- name: Check isort
|
|
40
|
+
run: uvx ruff check --select I .
|
|
41
|
+
|
|
42
|
+
- name: Check formatting
|
|
43
|
+
run: uvx ruff format --diff .
|
|
44
|
+
|
|
45
|
+
- name: Check linting
|
|
46
|
+
run: uvx ruff check .
|
|
47
|
+
|
|
48
|
+
pytest:
|
|
49
|
+
name: pytest
|
|
50
|
+
needs: qa
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
|
|
53
|
+
steps:
|
|
54
|
+
- name: Checkout Repository
|
|
55
|
+
uses: actions/checkout@v4
|
|
56
|
+
|
|
57
|
+
- name: Install uv
|
|
58
|
+
uses: astral-sh/setup-uv@v5
|
|
59
|
+
with:
|
|
60
|
+
version: "0.7.3"
|
|
61
|
+
|
|
62
|
+
- name: Install Python
|
|
63
|
+
run: uv python install
|
|
64
|
+
|
|
65
|
+
- name: Install the project
|
|
66
|
+
run: uv sync --dev
|
|
67
|
+
|
|
68
|
+
- name: Run tests
|
|
69
|
+
run: uv run pytest tests
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
.python-version
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py,cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
cover/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
.pybuilder/
|
|
77
|
+
target/
|
|
78
|
+
|
|
79
|
+
# Jupyter Notebook
|
|
80
|
+
.ipynb_checkpoints
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
+
# .python-version
|
|
90
|
+
m4r_venv/
|
|
91
|
+
|
|
92
|
+
# pipenv
|
|
93
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
94
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
95
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
96
|
+
# install all needed dependencies.
|
|
97
|
+
#Pipfile.lock
|
|
98
|
+
|
|
99
|
+
# poetry
|
|
100
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
101
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
102
|
+
# commonly ignored for libraries.
|
|
103
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
104
|
+
#poetry.lock
|
|
105
|
+
|
|
106
|
+
# pdm
|
|
107
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
108
|
+
#pdm.lock
|
|
109
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
110
|
+
# in version control.
|
|
111
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
112
|
+
.pdm.toml
|
|
113
|
+
|
|
114
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
115
|
+
__pypackages__/
|
|
116
|
+
|
|
117
|
+
# Celery stuff
|
|
118
|
+
celerybeat-schedule
|
|
119
|
+
celerybeat.pid
|
|
120
|
+
|
|
121
|
+
# SageMath parsed files
|
|
122
|
+
*.sage.py
|
|
123
|
+
|
|
124
|
+
# Environments
|
|
125
|
+
.env
|
|
126
|
+
.venv
|
|
127
|
+
env/
|
|
128
|
+
venv/
|
|
129
|
+
ENV/
|
|
130
|
+
env.bak/
|
|
131
|
+
venv.bak/
|
|
132
|
+
|
|
133
|
+
# Spyder project settings
|
|
134
|
+
.spyderproject
|
|
135
|
+
.spyproject
|
|
136
|
+
|
|
137
|
+
# Rope project settings
|
|
138
|
+
.ropeproject
|
|
139
|
+
|
|
140
|
+
# mkdocs documentation
|
|
141
|
+
/site
|
|
142
|
+
|
|
143
|
+
# mypy
|
|
144
|
+
.mypy_cache/
|
|
145
|
+
.dmypy.json
|
|
146
|
+
dmypy.json
|
|
147
|
+
|
|
148
|
+
# Pyre type checker
|
|
149
|
+
.pyre/
|
|
150
|
+
|
|
151
|
+
# pytype static type analyzer
|
|
152
|
+
.pytype/
|
|
153
|
+
|
|
154
|
+
# Cython debug symbols
|
|
155
|
+
cython_debug/
|
|
156
|
+
|
|
157
|
+
# PyCharm
|
|
158
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
159
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
160
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
161
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
162
|
+
#.idea/
|
|
163
|
+
|
|
164
|
+
# Figures and data
|
|
165
|
+
figures/
|
|
166
|
+
data/
|
|
167
|
+
|
|
168
|
+
# Mac stuff
|
|
169
|
+
.DS_Store
|
|
170
|
+
|
|
171
|
+
# IDEA
|
|
172
|
+
.idea/
|
|
173
|
+
|
|
174
|
+
# Lock files
|
|
175
|
+
# uv.lock
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: photosurfactant
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Models of chromocapillary flows for photo-actuated liquid mixing and sculpting.
|
|
5
|
+
Author-email: Niall Oswald <niall.oswald20@imperial.ac.uk>
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: alive-progress>=3.3.0
|
|
8
|
+
Requires-Dist: matplotlib>=3.10.7
|
|
9
|
+
Requires-Dist: numpy>=2.3.4
|
|
10
|
+
Requires-Dist: scipy>=1.16.2
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Photosurfactant
|
|
14
|
+
|
|
15
|
+
[](https://zenodo.org/badge/latestdoi/734330906)
|
|
16
|
+

|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
Numerical solvers for liquid mixing and sculpting using light-actuated photosurfactants.
|
|
20
|
+
|
|
21
|
+
# Publications
|
|
22
|
+
|
|
23
|
+
This package has been used in the following publications:
|
|
24
|
+
|
|
25
|
+
- (Submitted) M. D. Mayer, N. J. Oswald, D. T. Papageorgiou, 2026. Liquid mixing and sculpting using light-actuated photosurfactants.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Photosurfactant
|
|
2
|
+
|
|
3
|
+
[](https://zenodo.org/badge/latestdoi/734330906)
|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
Numerical solvers for liquid mixing and sculpting using light-actuated photosurfactants.
|
|
8
|
+
|
|
9
|
+
# Publications
|
|
10
|
+
|
|
11
|
+
This package has been used in the following publications:
|
|
12
|
+
|
|
13
|
+
- (Submitted) M. D. Mayer, N. J. Oswald, D. T. Papageorgiou, 2026. Liquid mixing and sculpting using light-actuated photosurfactants.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from photosurfactant import Parameters
|
|
5
|
+
from photosurfactant.fourier import fourier_series_coeff
|
|
6
|
+
from photosurfactant.semi_analytic import (
|
|
7
|
+
FirstOrder,
|
|
8
|
+
LeadingOrder,
|
|
9
|
+
Variables,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
params = Parameters()
|
|
13
|
+
leading = LeadingOrder(params)
|
|
14
|
+
|
|
15
|
+
# Find the Fourier series of the slip velocity
|
|
16
|
+
wavenumbers, func_coeffs = fourier_series_coeff(
|
|
17
|
+
lambda x: 1e-3 * np.sin(2 * np.pi * x / params.L), params.L, 10
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Solve the first-order problem by fixing u(x, 1) = f(x)
|
|
21
|
+
first = FirstOrder(wavenumbers, params, leading)
|
|
22
|
+
first.solve(
|
|
23
|
+
lambda n: (
|
|
24
|
+
(first._psi(wavenumbers[n], 1, z_order=1), func_coeffs[n])
|
|
25
|
+
if n > 0
|
|
26
|
+
else (Variables.f, 0)
|
|
27
|
+
) # There is no flow at n = 0, so we fix the light intensity instead
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Evaluate and plot the light intensity profile
|
|
31
|
+
xx = np.linspace(-params.L, params.L, 100)
|
|
32
|
+
|
|
33
|
+
plt.plot(xx, first.f(xx))
|
|
34
|
+
plt.xlabel("$x$")
|
|
35
|
+
plt.ylabel("$f(x)$")
|
|
36
|
+
plt.show()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "photosurfactant"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Models of chromocapillary flows for photo-actuated liquid mixing and sculpting."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Niall Oswald", email = "niall.oswald20@imperial.ac.uk" }
|
|
12
|
+
]
|
|
13
|
+
requires-python = ">=3.12"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"alive-progress>=3.3.0",
|
|
16
|
+
"matplotlib>=3.10.7",
|
|
17
|
+
"numpy>=2.3.4",
|
|
18
|
+
"scipy>=1.16.2",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
plot_error = "photosurfactant.scripts.plot_error:plot_error"
|
|
23
|
+
plot_first_order = "photosurfactant.scripts.plot_first_order:plot_first_order"
|
|
24
|
+
plot_leading_order = "photosurfactant.scripts.plot_leading_order:plot_leading_order"
|
|
25
|
+
plot_spectrum = "photosurfactant.scripts.plot_spectrum:plot_spectrum"
|
|
26
|
+
plot_sweep = "photosurfactant.scripts.plot_sweep:main"
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=8.4.2",
|
|
31
|
+
"pytest-cases>=3.9.1",
|
|
32
|
+
"scipy-stubs>=1.16.3.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.ruff.lint.isort]
|
|
36
|
+
known-first-party = ["photosurfactant"]
|
|
@@ -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
|