smoldynutils 0.1.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.
- smoldynutils-0.1.0/LICENSE.md +21 -0
- smoldynutils-0.1.0/PKG-INFO +57 -0
- smoldynutils-0.1.0/README.md +34 -0
- smoldynutils-0.1.0/pyproject.toml +73 -0
- smoldynutils-0.1.0/src/smoldynutils/__init__.py +7 -0
- smoldynutils-0.1.0/src/smoldynutils/data_objects.py +190 -0
- smoldynutils-0.1.0/src/smoldynutils/metrics.py +147 -0
- smoldynutils-0.1.0/src/smoldynutils/parsing.py +93 -0
- smoldynutils-0.1.0/src/smoldynutils/plots.py +220 -0
- smoldynutils-0.1.0/src/smoldynutils/utils.py +27 -0
- smoldynutils-0.1.0/src/smoldynutils/workflows.py +97 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rgrosseholz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smoldynutils
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small collection of utility tools to process the output of Smoldyn simulations.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE.md
|
|
7
|
+
Keywords: smoldyn,simulation,modeling
|
|
8
|
+
Author: Fabian Ormersbach
|
|
9
|
+
Author-email: fabian.ormersbach@maastrichtuniversity.nl
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Dist: matplotlib (>=3.10.8,<4.0.0)
|
|
14
|
+
Requires-Dist: numpy (>=2.4.2,<3.0.0)
|
|
15
|
+
Requires-Dist: pandas (>=3.0.0,<4.0.0)
|
|
16
|
+
Requires-Dist: scipy (>=1.17.0,<2.0.0)
|
|
17
|
+
Requires-Dist: seaborn (>=0.13.2,<0.14.0)
|
|
18
|
+
Project-URL: Homepage, https://github.com/rgrosseholz/smoldynutils
|
|
19
|
+
Project-URL: Issues, https://github.com/rgrosseholz/smoldynutils/issues
|
|
20
|
+
Project-URL: Repository, https://github.com/rgrosseholz/smoldynutils
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# smoldynutils
|
|
24
|
+
Utilities for parsing, analyzing, and visualizing Smoldyn simulation outputs.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
From GitHub with poetry:
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/rgrosseholz/smoldynutils.git
|
|
30
|
+
cd smoldynutils
|
|
31
|
+
poetry install
|
|
32
|
+
poetry shell
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or via pip:
|
|
36
|
+
```bash
|
|
37
|
+
pip install smoldynutils
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quickstart
|
|
41
|
+
```python
|
|
42
|
+
from smoldynutils.parser import SmoldynParser
|
|
43
|
+
|
|
44
|
+
parser = SmoldynParser(delimiter=",")
|
|
45
|
+
trajectories = parser.parse_fixed_grid("molpos_output.txt")
|
|
46
|
+
|
|
47
|
+
for traj in trajectories:
|
|
48
|
+
print(traj.positions)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Authors
|
|
52
|
+
Fabian Ormersbach, Maastricht Centre for Systems Biology and Bioinformatics, Maastricht University
|
|
53
|
+
Ruth Grosseholz, Maastricht Centre for Systems Biology and Bioinformatics, Maastricht University
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
This project is licensed under the MIT License. See `LICENSE.md` for details.
|
|
57
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# smoldynutils
|
|
2
|
+
Utilities for parsing, analyzing, and visualizing Smoldyn simulation outputs.
|
|
3
|
+
|
|
4
|
+
## Installation
|
|
5
|
+
From GitHub with poetry:
|
|
6
|
+
```bash
|
|
7
|
+
git clone https://github.com/rgrosseholz/smoldynutils.git
|
|
8
|
+
cd smoldynutils
|
|
9
|
+
poetry install
|
|
10
|
+
poetry shell
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or via pip:
|
|
14
|
+
```bash
|
|
15
|
+
pip install smoldynutils
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
```python
|
|
20
|
+
from smoldynutils.parser import SmoldynParser
|
|
21
|
+
|
|
22
|
+
parser = SmoldynParser(delimiter=",")
|
|
23
|
+
trajectories = parser.parse_fixed_grid("molpos_output.txt")
|
|
24
|
+
|
|
25
|
+
for traj in trajectories:
|
|
26
|
+
print(traj.positions)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Authors
|
|
30
|
+
Fabian Ormersbach, Maastricht Centre for Systems Biology and Bioinformatics, Maastricht University
|
|
31
|
+
Ruth Grosseholz, Maastricht Centre for Systems Biology and Bioinformatics, Maastricht University
|
|
32
|
+
|
|
33
|
+
## License
|
|
34
|
+
This project is licensed under the MIT License. See `LICENSE.md` for details.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "smoldynutils"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A small collection of utility tools to process the output of Smoldyn simulations."
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Fabian Ormersbach",email = "fabian.ormersbach@maastrichtuniversity.nl"},
|
|
7
|
+
{name = "Ruth Grosseholz",email = "ruth.grosseholz@maastrichtuniversity.nl"},
|
|
8
|
+
]
|
|
9
|
+
license = {text = "MIT"}
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"numpy (>=2.4.2,<3.0.0)",
|
|
14
|
+
"pandas (>=3.0.0,<4.0.0)",
|
|
15
|
+
"matplotlib (>=3.10.8,<4.0.0)",
|
|
16
|
+
"scipy (>=1.17.0,<2.0.0)",
|
|
17
|
+
"seaborn (>=0.13.2,<0.14.0)"
|
|
18
|
+
]
|
|
19
|
+
keywords = ["smoldyn", "simulation", "modeling"]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
]
|
|
24
|
+
packages = [{ include = "smoldynutils", from = "src" }]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/rgrosseholz/smoldynutils"
|
|
28
|
+
Repository = "https://github.com/rgrosseholz/smoldynutils"
|
|
29
|
+
Issues = "https://github.com/rgrosseholz/smoldynutils/issues"
|
|
30
|
+
|
|
31
|
+
[dependency-groups]
|
|
32
|
+
dev = [
|
|
33
|
+
"flake8 (>=7.3.0,<8.0.0)",
|
|
34
|
+
"pytest (>=9.0.2,<10.0.0)",
|
|
35
|
+
"pytest-cov (>=7.0.0,<8.0.0)",
|
|
36
|
+
"black (>=26.1.0,<27.0.0)",
|
|
37
|
+
"pre-commit (>=4.5.1,<5.0.0)",
|
|
38
|
+
"ruff (>=0.15.0,<0.16.0)",
|
|
39
|
+
"isort (>=7.0.0,<8.0.0)",
|
|
40
|
+
"mypy (>=1.19.1,<2.0.0)"
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.poetry]
|
|
44
|
+
packages = [{include = "smoldynutils", from = "src"}]
|
|
45
|
+
|
|
46
|
+
[tool.black]
|
|
47
|
+
line-length = 100
|
|
48
|
+
target-version = ["py312"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
line-length = 100
|
|
52
|
+
lint.select = ["E", "F", "W", "C90", "N"]
|
|
53
|
+
lint.ignore = ["E501"]
|
|
54
|
+
extend-exclude = ["tests"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint.pep8-naming]
|
|
57
|
+
ignore-names = ["D", "MSD"]
|
|
58
|
+
|
|
59
|
+
[tool.isort]
|
|
60
|
+
profile = "black"
|
|
61
|
+
src_paths = ["src", "tests"]
|
|
62
|
+
|
|
63
|
+
[tool.mypy]
|
|
64
|
+
python_version = "3.12"
|
|
65
|
+
strict = true
|
|
66
|
+
ignore_missing_imports = true
|
|
67
|
+
mypy_path = ["src"]
|
|
68
|
+
explicit_package_bases = true
|
|
69
|
+
exclude = ["^tests/"]
|
|
70
|
+
|
|
71
|
+
[build-system]
|
|
72
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
73
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Iterator, Optional, Sequence, Type, Union, overload
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class Trajectory:
|
|
10
|
+
"""Immutable container for trajectory."""
|
|
11
|
+
|
|
12
|
+
serialnumber: int
|
|
13
|
+
t: np.ndarray
|
|
14
|
+
x: np.ndarray
|
|
15
|
+
y: np.ndarray
|
|
16
|
+
species: np.ndarray
|
|
17
|
+
|
|
18
|
+
def __post_init__(self) -> None:
|
|
19
|
+
"""Performs sensibility checks.
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
ValueError: Differing lenghts of t, x, y, or species
|
|
23
|
+
ValueError: >1D for t, x, y, or species
|
|
24
|
+
TypeError: Species is not integer
|
|
25
|
+
"""
|
|
26
|
+
n = len(self.t)
|
|
27
|
+
if not (len(self.x) == len(self.y) == len(self.species) == n):
|
|
28
|
+
raise ValueError("t, x, y, and species must have the same length")
|
|
29
|
+
if self.t.ndim != 1 or self.x.ndim != 1 or self.y.ndim != 1 or self.species.ndim != 1:
|
|
30
|
+
raise ValueError("t, x, y, species must be 1D arrays")
|
|
31
|
+
if not np.issubdtype(self.species.dtype, np.integer):
|
|
32
|
+
raise TypeError("Species must be integer-coded")
|
|
33
|
+
self._check_jumps(self.x)
|
|
34
|
+
self._check_jumps(self.y)
|
|
35
|
+
|
|
36
|
+
def _check_jumps(self, positions: np.ndarray) -> None:
|
|
37
|
+
jump_sensitivity = 0.5
|
|
38
|
+
max_pos = np.max(np.abs(positions))
|
|
39
|
+
forward_diff = np.diff(positions)
|
|
40
|
+
upper_jumps = forward_diff < jump_sensitivity * max_pos * -1
|
|
41
|
+
lower_jumps = forward_diff > jump_sensitivity * max_pos
|
|
42
|
+
|
|
43
|
+
def user_format_warning(
|
|
44
|
+
message: Warning | str,
|
|
45
|
+
category: Type[Warning],
|
|
46
|
+
filename: str,
|
|
47
|
+
lineno: int,
|
|
48
|
+
line: Optional[str] = None,
|
|
49
|
+
) -> str:
|
|
50
|
+
return f"Warning: {message}\n"
|
|
51
|
+
|
|
52
|
+
if (upper_jumps + lower_jumps).sum() != 0:
|
|
53
|
+
warnings.formatwarning = user_format_warning
|
|
54
|
+
warnings.warn(f"Large jumps in trajectory {self.serialnumber} detected.", UserWarning)
|
|
55
|
+
|
|
56
|
+
def __len__(self) -> int:
|
|
57
|
+
"""Returns number of points in trajectory
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
int: Number of timepoints in trajectory
|
|
61
|
+
"""
|
|
62
|
+
return len(self.t)
|
|
63
|
+
|
|
64
|
+
def __eq__(self, other: object) -> bool:
|
|
65
|
+
"""Checks for equality.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
other (object): Can be Trajectory or dict containing data.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
bool: True if t, x, y, and species match. False otherwise. NotImplemented if other is not dict or Trajectory.
|
|
72
|
+
"""
|
|
73
|
+
if isinstance(other, Trajectory):
|
|
74
|
+
serial_bool = self.serialnumber == other.serialnumber
|
|
75
|
+
t_bool = np.allclose(self.t, other.t)
|
|
76
|
+
x_bool = np.allclose(self.x, other.x)
|
|
77
|
+
y_bool = np.allclose(self.y, other.y)
|
|
78
|
+
species_bool = np.allclose(self.species, other.species)
|
|
79
|
+
return serial_bool and t_bool and x_bool and y_bool and species_bool
|
|
80
|
+
if isinstance(other, dict):
|
|
81
|
+
if len(other["t"]) != len(self):
|
|
82
|
+
return False
|
|
83
|
+
serial_bool = self.serialnumber == other["serialnum"]
|
|
84
|
+
t_bool = np.allclose(self.t, other["t"])
|
|
85
|
+
x_bool = np.allclose(self.x, other["x"])
|
|
86
|
+
y_bool = np.allclose(self.y, other["y"])
|
|
87
|
+
species_bool = np.allclose(self.species, other["species"])
|
|
88
|
+
return serial_bool and t_bool and x_bool and y_bool and species_bool
|
|
89
|
+
|
|
90
|
+
return NotImplemented
|
|
91
|
+
|
|
92
|
+
def __getitem__(self, i: int) -> tuple[int, float, float, float, int]:
|
|
93
|
+
return (
|
|
94
|
+
self.serialnumber,
|
|
95
|
+
self.t[i],
|
|
96
|
+
self.x[i],
|
|
97
|
+
self.y[i],
|
|
98
|
+
self.species[i],
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def adjust_for_periodic_boundaries(
|
|
103
|
+
position: np.ndarray, min_pos: float, max_pos: float
|
|
104
|
+
) -> np.ndarray:
|
|
105
|
+
size = max_pos - min_pos
|
|
106
|
+
half_delta = 0.5 * (size)
|
|
107
|
+
forward_diff = np.diff(position, prepend=position[0])
|
|
108
|
+
upper_jumps = forward_diff < -1 * half_delta
|
|
109
|
+
lower_jumps = forward_diff > half_delta
|
|
110
|
+
if (upper_jumps + lower_jumps).sum() == 0:
|
|
111
|
+
return position
|
|
112
|
+
upper_jumps_cumsum = upper_jumps.cumsum() * size
|
|
113
|
+
lower_jumps_cumsum = lower_jumps.cumsum() * size * -1
|
|
114
|
+
position_mask = upper_jumps_cumsum + lower_jumps_cumsum
|
|
115
|
+
return position + position_mask
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True, slots=True)
|
|
119
|
+
class TrajectorySet:
|
|
120
|
+
"""Immutable container for set of trajectories."""
|
|
121
|
+
|
|
122
|
+
trajectories: tuple[Trajectory, ...]
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def from_list(cls, trajectories: Sequence[Trajectory]) -> "TrajectorySet":
|
|
126
|
+
"""Create TrajectorySet from sequence of trajectories
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
trajectories (Sequence[Trajectory]): Sequence of `Trajectory` objects.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
TrajectorySet: Contains provided Trajectories
|
|
133
|
+
"""
|
|
134
|
+
return cls(tuple(trajectories))
|
|
135
|
+
|
|
136
|
+
def __len__(self) -> int:
|
|
137
|
+
"""Returns the number of trajectories in the set
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
int: Number of stored trajectories
|
|
141
|
+
"""
|
|
142
|
+
return len(self.trajectories)
|
|
143
|
+
|
|
144
|
+
def __getitem__(self, key: int) -> Trajectory:
|
|
145
|
+
"""Return trajectory by index.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
key (int): Index of trajectory to retrieve
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Trajectory: Trajectory at given index
|
|
152
|
+
"""
|
|
153
|
+
return self.trajectories[key]
|
|
154
|
+
|
|
155
|
+
@overload
|
|
156
|
+
def __add__(self, other: "TrajectorySet") -> "TrajectorySet": ...
|
|
157
|
+
@overload
|
|
158
|
+
def __add__(self, other: Trajectory) -> "TrajectorySet": ...
|
|
159
|
+
|
|
160
|
+
def __add__(self, other: Union["TrajectorySet", Trajectory]) -> "TrajectorySet":
|
|
161
|
+
"""Combines given trajectories
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
other (Union[TrajectorySet, Trajectory]): TrajectorySet or Trajectory to combine with current
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
TrajectorySet: New TrajectorySet containing the combined trajectories.
|
|
168
|
+
"""
|
|
169
|
+
if isinstance(other, TrajectorySet):
|
|
170
|
+
return TrajectorySet(self.trajectories + other.trajectories)
|
|
171
|
+
if isinstance(other, Trajectory):
|
|
172
|
+
return TrajectorySet(self.trajectories + (other,))
|
|
173
|
+
return NotImplemented
|
|
174
|
+
|
|
175
|
+
def __iter__(self) -> Iterator[Trajectory]:
|
|
176
|
+
"""Iterate over trajectories
|
|
177
|
+
|
|
178
|
+
Yields:
|
|
179
|
+
Trajectory: Trajectorie object
|
|
180
|
+
"""
|
|
181
|
+
return iter(self.trajectories)
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def serialnums(self) -> np.ndarray:
|
|
185
|
+
serialnums = np.zeros(len(self))
|
|
186
|
+
for index, traj in enumerate(self):
|
|
187
|
+
serialnums[index] = traj.serialnumber
|
|
188
|
+
return serialnums
|
|
189
|
+
|
|
190
|
+
# TODO: Methods .t, .x, ... that return array of values of all trajectories
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from scipy.optimize import curve_fit
|
|
6
|
+
|
|
7
|
+
from smoldynutils.data_objects import Trajectory
|
|
8
|
+
from smoldynutils.utils import theoretical_msd, theoretical_msd_residue
|
|
9
|
+
|
|
10
|
+
FloatArray = np.typing.NDArray[np.floating]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def calc_displacements(traj_values: FloatArray, lag: int = 1) -> FloatArray:
|
|
14
|
+
"""Calculates the displacement depending on time lag.
|
|
15
|
+
|
|
16
|
+
Eq: x(t+lag) - x(t)
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
traj_values (np.ndarray): x or y values
|
|
20
|
+
lag (int, optional): Controls the shift of the window. Defaults to 1.
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
ValueError: Chosen timelag is bigger than the length of x/y
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
np.ndarray: Timelag displacement values
|
|
27
|
+
"""
|
|
28
|
+
if lag > len(traj_values) - 1:
|
|
29
|
+
raise ValueError("Timelag is bigger than length of trajectory.")
|
|
30
|
+
displacement = traj_values[lag:] - traj_values[:-lag]
|
|
31
|
+
return displacement
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def calc_xy_displacement(traj: Trajectory, lag: int = 1) -> tuple[np.ndarray, np.ndarray]:
|
|
35
|
+
"""Feeds x and y of Trajectory into calc_displacements
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
traj (Trajectory): Trajectory object for which x and y displacements should be calculated.
|
|
39
|
+
lag (int, optional): Controls the shift of the window. Defaults to 1.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: Chosen Timelag is bigger than the trajectory is long.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
tuple[np.ndarray, np.ndarray]: Timelag displacements in x and y direction
|
|
46
|
+
"""
|
|
47
|
+
if lag > len(traj.x) - 1 or lag > len(traj.y) - 1:
|
|
48
|
+
raise ValueError("Timelag is bigger than number of datapoints in x or y")
|
|
49
|
+
x_displacement = calc_displacements(traj.x, lag)
|
|
50
|
+
y_displacement = calc_displacements(traj.y, lag)
|
|
51
|
+
|
|
52
|
+
return (x_displacement, y_displacement)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def calc_msd(displacment: np.ndarray) -> np.ndarray:
|
|
56
|
+
"""Calculates mean squeared displacement.
|
|
57
|
+
|
|
58
|
+
Equation: mean(dx**2)
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
displacment (np.ndarray): Displacement values.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
np.ndarray: Mean squared displacement values.
|
|
65
|
+
"""
|
|
66
|
+
squared_displacement = displacment**2
|
|
67
|
+
mean_squared_displacement = np.array(np.mean(squared_displacement))
|
|
68
|
+
return mean_squared_displacement
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def calc_xy_msd(displacements: tuple[np.ndarray, np.ndarray]) -> tuple[np.ndarray, np.ndarray]:
|
|
72
|
+
"""Feeds x and y into calc_msd
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
displacements (tuple[np.ndarray, np.ndarray]): x displacement followed by y displacement
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
tuple[np.ndarray, np.ndarray]: MSD of x and y
|
|
79
|
+
"""
|
|
80
|
+
x_msd = calc_msd(displacements[0])
|
|
81
|
+
y_msd = calc_msd(displacements[1])
|
|
82
|
+
return (x_msd, y_msd)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def calc_sq_displacement_from_zero(traj_values: FloatArray) -> FloatArray:
|
|
86
|
+
"""Calculates displacement relative to start position.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
traj_values (np.ndarray): Position value of Trajectory
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
np.ndarray: Displacement from start position.
|
|
93
|
+
"""
|
|
94
|
+
x0 = float(traj_values[0])
|
|
95
|
+
return (traj_values - x0) ** 2
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def calc_combined_msd(msds: tuple[np.ndarray, np.ndarray]) -> np.ndarray:
|
|
99
|
+
return np.array(msds[0] + msds[1])
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def estimate_diffcoff_fullinfo(
|
|
103
|
+
msds: np.ndarray, timepoints: np.ndarray, add_epsilon: bool = False
|
|
104
|
+
) -> tuple[FloatArray, FloatArray]:
|
|
105
|
+
"""Estimates diffusion coefficient from MSD.
|
|
106
|
+
|
|
107
|
+
Fitted equation is MSD = 4*D*t
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
msds (np.ndarray): Array of MSD values
|
|
111
|
+
timepoints (np.ndarray): Array of timelag or time values
|
|
112
|
+
add_epsilon (bool, optional): Use equation MSD = 4*D*t + epsilon for fitting. Defaults to False.
|
|
113
|
+
return_full (bool, optional): Return full information about curve fitting. Defaults to False.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
np.ndarray: _description_
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
if len(timepoints) < 2 and add_epsilon is True:
|
|
120
|
+
warnings.warn(
|
|
121
|
+
"Cannot fit with epsilon if only one timelag given. Setting add_epsilon to False.",
|
|
122
|
+
UserWarning,
|
|
123
|
+
)
|
|
124
|
+
add_epsilon = False
|
|
125
|
+
if add_epsilon is True:
|
|
126
|
+
line_fit = curve_fit(theoretical_msd_residue, timepoints, msds)
|
|
127
|
+
else:
|
|
128
|
+
line_fit = curve_fit(theoretical_msd, timepoints, msds)
|
|
129
|
+
return cast(tuple[FloatArray, FloatArray], line_fit)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def estimate_diffcoff(msds: np.ndarray, timepoints: np.ndarray, add_epsilon: bool = False) -> float:
|
|
133
|
+
"""Estimates diffusion coefficient from MSD.
|
|
134
|
+
|
|
135
|
+
Fitted equation is MSD = 4*D*t
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
msds (np.ndarray): Array of MSD values
|
|
139
|
+
timepoints (np.ndarray): Array of timelag or time values
|
|
140
|
+
add_epsilon (bool, optional): Use equation MSD = 4*D*t + epsilon for fitting. Defaults to False.
|
|
141
|
+
return_full (bool, optional): Return full information about curve fitting. Defaults to False.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
np.ndarray: _description_
|
|
145
|
+
"""
|
|
146
|
+
popt, _ = estimate_diffcoff_fullinfo(msds, timepoints, add_epsilon)
|
|
147
|
+
return float(popt[0])
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import numpy.typing as npt
|
|
8
|
+
|
|
9
|
+
from smoldynutils.data_objects import Trajectory, TrajectorySet
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SmoldynParser:
|
|
14
|
+
path: str
|
|
15
|
+
delimiter: str = ","
|
|
16
|
+
dt: float = 0.5
|
|
17
|
+
min_val: Optional[float] = None
|
|
18
|
+
max_val: Optional[float] = None
|
|
19
|
+
|
|
20
|
+
def parse_fixed_grid(
|
|
21
|
+
self,
|
|
22
|
+
dtype_xy: npt.DTypeLike = np.float64,
|
|
23
|
+
dtype_t: npt.DTypeLike = np.float32,
|
|
24
|
+
dtype_species: npt.DTypeLike = np.uint16,
|
|
25
|
+
dtype_serialnum: npt.DTypeLike = np.uint32,
|
|
26
|
+
) -> TrajectorySet:
|
|
27
|
+
"""Parser based on numpy loadtxt assuming equal size of all trajectories.
|
|
28
|
+
|
|
29
|
+
Sorts based on time and serialnumber. Then generates Trajectories based on expected size.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
path (str): Path to smoldyn data (assuming listmols2 command)
|
|
33
|
+
delimiter (str, optional): Column delimiter. Defaults to ",".
|
|
34
|
+
dtype_xy (np.float32, optional): xy data type. Defaults to np.float32.
|
|
35
|
+
dtype_t (np.float32, optional): t data type. Defaults to np.float32.
|
|
36
|
+
dtype_species (np.uint16, optional): Species data type. Defaults to np.uint16.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
TrajectorySet: Set of read trajectories.
|
|
40
|
+
"""
|
|
41
|
+
file_content = np.loadtxt(self.path, delimiter=self.delimiter, dtype=np.float32)
|
|
42
|
+
if file_content.size == 0:
|
|
43
|
+
raise ValueError("Data file appears to be empty.")
|
|
44
|
+
t = file_content[:, 0].astype(dtype_t, copy=False)
|
|
45
|
+
serial_number = file_content[:, 5].astype(dtype_serialnum, copy=False)
|
|
46
|
+
order = np.lexsort((t, serial_number))
|
|
47
|
+
|
|
48
|
+
t = t[order]
|
|
49
|
+
serial_number = serial_number[order]
|
|
50
|
+
species = file_content[:, 1].astype(dtype_species, copy=False)[order]
|
|
51
|
+
x = file_content[:, 3].astype(dtype_xy, copy=False)[order]
|
|
52
|
+
y = file_content[:, 4].astype(dtype_xy, copy=False)[order]
|
|
53
|
+
serial_number = file_content[:, 5].astype(dtype_serialnum, copy=False)[order]
|
|
54
|
+
|
|
55
|
+
serial_ids, serial_start, serial_counts = np.unique(
|
|
56
|
+
serial_number, return_index=True, return_counts=True
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
expected = int(serial_counts[0])
|
|
60
|
+
if not np.all(serial_counts == expected):
|
|
61
|
+
raise NotImplementedError(
|
|
62
|
+
"Not a fixed grid. Serials have different number of timepoints."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
trajs: list[Trajectory] = []
|
|
66
|
+
for sid, start in zip(serial_ids, serial_start):
|
|
67
|
+
end = start + expected
|
|
68
|
+
if self.min_val is not None and self.max_val is not None:
|
|
69
|
+
trajs.append(
|
|
70
|
+
Trajectory(
|
|
71
|
+
int(sid),
|
|
72
|
+
t=t[start:end],
|
|
73
|
+
x=Trajectory.adjust_for_periodic_boundaries(
|
|
74
|
+
x[start:end], self.min_val, self.max_val
|
|
75
|
+
),
|
|
76
|
+
y=Trajectory.adjust_for_periodic_boundaries(
|
|
77
|
+
y[start:end], self.min_val, self.max_val
|
|
78
|
+
),
|
|
79
|
+
species=species[start:end],
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
trajs.append(
|
|
84
|
+
Trajectory(
|
|
85
|
+
int(sid),
|
|
86
|
+
t=t[start:end],
|
|
87
|
+
x=x[start:end],
|
|
88
|
+
y=y[start:end],
|
|
89
|
+
species=species[start:end],
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return TrajectorySet(tuple(trajs))
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from typing import Optional, Sequence, Union, Dict
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from matplotlib.axes import Axes
|
|
5
|
+
|
|
6
|
+
from smoldynutils.data_objects import Trajectory, TrajectorySet
|
|
7
|
+
|
|
8
|
+
import seaborn as sns
|
|
9
|
+
|
|
10
|
+
FloatArray = np.typing.NDArray[np.floating]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def plot_gauss_comparison(
|
|
14
|
+
displacement: np.ndarray,
|
|
15
|
+
gauss_vals: np.ndarray,
|
|
16
|
+
ax: Axes,
|
|
17
|
+
bins: Union[str, Sequence[float]] = "fd",
|
|
18
|
+
title: str = "Title",
|
|
19
|
+
) -> Axes:
|
|
20
|
+
"""Plots histogram of measured displacement and theoretical displacement.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
displacement (np.ndarray): Measured displacement
|
|
24
|
+
gauss_vals (np.ndarray): Theoretical expectation
|
|
25
|
+
ax (Axes, optional): Axis to plot onto. Defaults to None.
|
|
26
|
+
bins (str, optional): Algorithm to determine bins or bins. Defaults to "fd".
|
|
27
|
+
title (str, optional): Title for the plot. Defaults to "Title".
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ValueError: No axis to plot onto provided.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Axes: Axis that contains the histogram.
|
|
34
|
+
"""
|
|
35
|
+
ax.hist(displacement, bins=bins, density=True)
|
|
36
|
+
ax.hist(gauss_vals, bins=bins, density=True)
|
|
37
|
+
ax.set_xlabel("Δx")
|
|
38
|
+
ax.set_ylabel("density")
|
|
39
|
+
ax.set_title(title)
|
|
40
|
+
return ax
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def plot_trajectorie(traj: Trajectory, ax: Axes, title: str = "Title") -> Axes:
|
|
44
|
+
"""Simple xy plot of a single trajectory.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
traj (Trajectory): Trajectory to plot
|
|
48
|
+
ax (Axes): Axis onto which the trajectory will be plotted
|
|
49
|
+
title (str, optional): Figure title. Defaults to "Title".
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
ValueError: No axis to plot onto provided
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Axes: Axis that contains the xy plot
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
ax.plot(traj.x, traj.y, color="black")
|
|
59
|
+
ax.scatter(traj.x, traj.y, c=traj.t)
|
|
60
|
+
ax.set_xlabel("x")
|
|
61
|
+
ax.set_ylabel("y")
|
|
62
|
+
ax.set_title(title)
|
|
63
|
+
return ax
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def plot_trajectories(trajs: TrajectorySet, ax: Axes, title: str = "Title") -> Axes:
|
|
67
|
+
"""Creates xy plot for multiple trajectories.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
trajs (TrajectorySet): Set of trajectories
|
|
71
|
+
ax (Axes): Axis onto which the trajectory will be plotted
|
|
72
|
+
title (str, optional): Figure title. Defaults to "Title".
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
ValueError: No axis to plot onto provided
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Axes: Axis that contains the xy plot
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
for traj in trajs:
|
|
82
|
+
ax = plot_trajectorie(traj, ax, title)
|
|
83
|
+
return ax
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def plot_msd(
|
|
87
|
+
msd: np.ndarray,
|
|
88
|
+
ax: Axes,
|
|
89
|
+
time: Optional[np.ndarray] = None,
|
|
90
|
+
title: str = "Title",
|
|
91
|
+
color: Optional[str] = None,
|
|
92
|
+
) -> Axes:
|
|
93
|
+
if time is None:
|
|
94
|
+
time = np.arange(len(msd))
|
|
95
|
+
if color is None:
|
|
96
|
+
color = "blue"
|
|
97
|
+
if len(msd.shape) > 2:
|
|
98
|
+
raise ValueError("Input MSD array is > 2D")
|
|
99
|
+
if not len(msd) == len(time):
|
|
100
|
+
msd = msd.T
|
|
101
|
+
if not len(msd) == len(time):
|
|
102
|
+
raise ValueError("Input MSD array and time array have no shape in common.")
|
|
103
|
+
ax.plot(time, msd, color=color)
|
|
104
|
+
ax.set_xlabel("time")
|
|
105
|
+
ax.set_ylabel("msd")
|
|
106
|
+
ax.set_title(title)
|
|
107
|
+
return ax
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def plot_msd_comparison(
|
|
111
|
+
msd: np.ndarray,
|
|
112
|
+
theoretical_msd: np.ndarray,
|
|
113
|
+
ax: Axes,
|
|
114
|
+
time: Optional[np.ndarray] = None,
|
|
115
|
+
title: str = "Title",
|
|
116
|
+
) -> Axes:
|
|
117
|
+
"""Lineplot showing the calculated MSD values vs the theoretical expectation.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
msd (np.ndarray): Calculated MSD values
|
|
121
|
+
theoretical_msd (np.ndarray): Theoretically expected MSD values
|
|
122
|
+
ax (Axes): Axis onto which will be plotted
|
|
123
|
+
title (str, optional): Figure title. Defaults to "Title".
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Axes: Axis that contains the msd comparison.
|
|
127
|
+
"""
|
|
128
|
+
if len(msd) != len(theoretical_msd):
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"Mismatch in MSD arrays: input array length={len(msd)}, theoretical array length={len(theoretical_msd)}"
|
|
131
|
+
)
|
|
132
|
+
if time is None:
|
|
133
|
+
time = np.arange(len(msd))
|
|
134
|
+
ax = plot_msd(msd, ax, time=time)
|
|
135
|
+
ax = plot_msd(theoretical_msd, ax, time=time, color="red")
|
|
136
|
+
ax.set_xlabel("time")
|
|
137
|
+
ax.set_ylabel("msd")
|
|
138
|
+
ax.set_title(title)
|
|
139
|
+
return ax
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def plot_diffconst_hist(
|
|
143
|
+
diffcoffs: np.ndarray, reference_diffcoff: float, ax: Axes, title: str = "Title"
|
|
144
|
+
) -> Axes:
|
|
145
|
+
"""Plots histogram of diffusion coefficients
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
diffcoffs (np.ndarray): Array of diffusion coefficients
|
|
149
|
+
reference_diffcoff (float): Expected diffusion coefficient
|
|
150
|
+
ax (Axes): Axes onto which will be plotted
|
|
151
|
+
title (str, optional): Plot title. Defaults to "Title".
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Axes: Axes with histogram
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
lower_bound = min(diffcoffs)
|
|
158
|
+
upper_bound = max(diffcoffs)
|
|
159
|
+
bins = list(np.linspace(lower_bound, upper_bound, 20))
|
|
160
|
+
ax.hist(diffcoffs, bins=bins)
|
|
161
|
+
ax.set_xscale("log")
|
|
162
|
+
|
|
163
|
+
ax.axvline(float(np.mean(diffcoffs)))
|
|
164
|
+
|
|
165
|
+
ax.axvline(reference_diffcoff)
|
|
166
|
+
|
|
167
|
+
ax.set_xlabel("Diffusion coefficient")
|
|
168
|
+
ax.set_ylabel("Count")
|
|
169
|
+
ax.set_title(title)
|
|
170
|
+
return ax
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def plot_violin_with_mean(
|
|
174
|
+
diffcoff: Dict[float, FloatArray],
|
|
175
|
+
reference_diffcoffs: Sequence[float],
|
|
176
|
+
permeability: Sequence[float],
|
|
177
|
+
ax: Axes,
|
|
178
|
+
title: str = "Title",
|
|
179
|
+
) -> Axes:
|
|
180
|
+
"""Generates a violinplot of diffcoff vs permeability.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
diffcoff (Dict[float, FloatArray]): Permeability vs diffusion coefficients
|
|
184
|
+
reference_diffcoffs (Sequence[float]): Expected diffusion coefficients
|
|
185
|
+
permeability (Sequence[float]): Permeabilities for x axis
|
|
186
|
+
ax (Axes): Axes onto which will be plotted
|
|
187
|
+
title (str, optional): Title of plot. Defaults to "Title".
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
ValueError: Number of entries in diffcoff does not match number of permeabilities
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Axes: Axis that contains violin plots
|
|
194
|
+
"""
|
|
195
|
+
if not len(diffcoff.keys()) == len(permeability):
|
|
196
|
+
raise ValueError(
|
|
197
|
+
"Number of entries in diffcoff dict does not match number of permeabilites."
|
|
198
|
+
)
|
|
199
|
+
sns.violinplot(diffcoff, ax=ax, order=list(diffcoff.keys()), color="skyblue", inner=None)
|
|
200
|
+
mean_ds = [np.mean(vals) for vals in diffcoff.values()]
|
|
201
|
+
indices = np.arange(0, len(diffcoff.keys()))
|
|
202
|
+
ax.scatter(indices, mean_ds, color="black", marker="_", zorder=10, alpha=1, s=100)
|
|
203
|
+
ax.axhline(
|
|
204
|
+
reference_diffcoffs[0],
|
|
205
|
+
color="red",
|
|
206
|
+
linestyle="--",
|
|
207
|
+
linewidth=1,
|
|
208
|
+
label=f"WT D={reference_diffcoffs[0]}",
|
|
209
|
+
)
|
|
210
|
+
ax.axhline(
|
|
211
|
+
reference_diffcoffs[1],
|
|
212
|
+
color="blue",
|
|
213
|
+
linestyle=":",
|
|
214
|
+
linewidth=1,
|
|
215
|
+
label=f"PHSD D={reference_diffcoffs[1]}",
|
|
216
|
+
)
|
|
217
|
+
ax.set_xlabel("Permeability")
|
|
218
|
+
ax.set_ylabel("Diffusion coefficient")
|
|
219
|
+
ax.set_title(title)
|
|
220
|
+
return ax
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def gauss_probability_density(x: float, mu: float, sigma: float) -> float:
|
|
5
|
+
if sigma <= 0:
|
|
6
|
+
raise ValueError("sigma must be > 0")
|
|
7
|
+
|
|
8
|
+
value = (1 / (np.sqrt(2 * np.pi) * sigma)) * np.exp(-np.square(x - mu) / (2 * sigma**2))
|
|
9
|
+
return float(value)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def theoretical_brownian_motion_pdf(x: float, D: float, t: float) -> float:
|
|
13
|
+
if D <= 0:
|
|
14
|
+
raise ValueError("D must be > 0")
|
|
15
|
+
if t < 0:
|
|
16
|
+
raise ValueError("t must be >= 0")
|
|
17
|
+
sigma = np.sqrt(2 * D * t)
|
|
18
|
+
mu = 0
|
|
19
|
+
return gauss_probability_density(x, mu, sigma)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def theoretical_msd(t: float, D: float) -> float:
|
|
23
|
+
return 4 * D * t
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def theoretical_msd_residue(t: float, D: float, epsilon: float) -> float:
|
|
27
|
+
return 4 * D * t + epsilon
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from typing import Dict, Sequence
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from smoldynutils.data_objects import Trajectory, TrajectorySet
|
|
6
|
+
from smoldynutils.metrics import (
|
|
7
|
+
calc_combined_msd,
|
|
8
|
+
calc_sq_displacement_from_zero,
|
|
9
|
+
calc_xy_displacement,
|
|
10
|
+
calc_xy_msd,
|
|
11
|
+
estimate_diffcoff,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def estimate_timelag_msd_from_traj(traj: Trajectory, timelags: Sequence[int]) -> Dict[int, float]:
|
|
16
|
+
"""Calculates MSD(timelag) for trajectory.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
traj (Trajectory): Trajectory for which MSD will be calculated
|
|
20
|
+
timelags (Sequence[int]): Sequence of timelags that will be used
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Dict[int, float]: Keys are timelags, values the corresponding MSD
|
|
24
|
+
"""
|
|
25
|
+
msd_dict = {}
|
|
26
|
+
for timelag in timelags:
|
|
27
|
+
xy_displacement = calc_xy_displacement(traj, timelag)
|
|
28
|
+
xy_msd = calc_xy_msd(xy_displacement)
|
|
29
|
+
msd = calc_combined_msd(xy_msd)
|
|
30
|
+
msd_dict[timelag] = float(msd)
|
|
31
|
+
return msd_dict
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def estimate_timelag_diffcoff_from_trajset(
|
|
35
|
+
trajs: TrajectorySet, timelags: Sequence[int] = (1, 2, 3, 4), add_epsilon: bool = False
|
|
36
|
+
) -> Dict[int, float]:
|
|
37
|
+
"""Calculates observed diffusion coefficient based on MSD(timelag) for set of trajectories
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
trajs (TrajectorySet): Set of trajectories for which diff coff will be calculated
|
|
41
|
+
timelags (Sequence[int], optional): Sequence of timelags for MSD calculation. Defaults to (1, 2, 3, 4).
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dict[int, float]: Keys are trajectory serialnums or index, values the corresponding diff coff.
|
|
45
|
+
"""
|
|
46
|
+
diffcoffs = {}
|
|
47
|
+
use_index_for_dict = False
|
|
48
|
+
timelag_array = np.array(timelags)
|
|
49
|
+
if len(np.unique(trajs.serialnums)) < len(trajs):
|
|
50
|
+
use_index_for_dict = True
|
|
51
|
+
for index, traj in enumerate(trajs):
|
|
52
|
+
msd_dict = estimate_timelag_msd_from_traj(traj, timelags)
|
|
53
|
+
msds = np.array(list(msd_dict.values()))
|
|
54
|
+
if use_index_for_dict is True:
|
|
55
|
+
diffcoffs[index] = estimate_diffcoff(msds, timelag_array, add_epsilon=add_epsilon)
|
|
56
|
+
else:
|
|
57
|
+
diffcoffs[traj.serialnumber] = estimate_diffcoff(
|
|
58
|
+
msds, timelag_array, add_epsilon=add_epsilon
|
|
59
|
+
)
|
|
60
|
+
return diffcoffs
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def estimate_time_msd_from_traj(traj: Trajectory) -> np.ndarray:
|
|
64
|
+
"""Calculates MSD(time) for trajectory.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
traj (Trajectory): Trajectory for which MSD will be calculated
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
np.ndarray: Calculated MSDs.
|
|
71
|
+
"""
|
|
72
|
+
x_sqdisplacement = calc_sq_displacement_from_zero(traj.x)
|
|
73
|
+
y_sqdisplacement = calc_sq_displacement_from_zero(traj.y)
|
|
74
|
+
msd = calc_combined_msd((x_sqdisplacement, y_sqdisplacement))
|
|
75
|
+
return msd
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def estimate_time_diffcoff_from_trajset(trajs: TrajectorySet) -> Dict[int, float]:
|
|
79
|
+
"""Estimates diffusion coefficient of set of Trajectories based on MSD(time)
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
trajs (TrajectorySet): Set of trajectories for which diffusion coefficient will be estimated.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Dict[int, float]: Keys are serialnums or index, values are diffcoffs
|
|
86
|
+
"""
|
|
87
|
+
diffcoffs = {}
|
|
88
|
+
use_index_for_dict = False
|
|
89
|
+
if len(np.unique(trajs.serialnums)) < len(trajs):
|
|
90
|
+
use_index_for_dict = True
|
|
91
|
+
for index, traj in enumerate(trajs):
|
|
92
|
+
msd = estimate_time_msd_from_traj(traj)
|
|
93
|
+
if use_index_for_dict is True:
|
|
94
|
+
diffcoffs[index] = estimate_diffcoff(msd, traj.t)
|
|
95
|
+
else:
|
|
96
|
+
diffcoffs[traj.serialnumber] = estimate_diffcoff(msd, traj.t)
|
|
97
|
+
return diffcoffs
|