|
| 1 | +import logging |
| 2 | + |
| 3 | +import matplotlib.pyplot as plt |
| 4 | +import MDAnalysis as mda |
| 5 | +import numpy as np |
| 6 | +from IPython.display import display |
| 7 | +from joblib import delayed |
| 8 | +from matplotlib.collections import LineCollection |
| 9 | + |
| 10 | +from mdadash.backend.widgets.base import WidgetBase |
| 11 | + |
| 12 | +logger = logging.getLogger(__name__) |
| 13 | + |
| 14 | + |
| 15 | +class MSDAnalysis(WidgetBase): |
| 16 | + name = "MSD Analysis" |
| 17 | + description = "Mean squared displacement analysis" |
| 18 | + |
| 19 | + _inputs = [ |
| 20 | + { |
| 21 | + "attribute": "_run_mode", |
| 22 | + "name": "Run mode", |
| 23 | + "description": "The mode in which the widget is run", |
| 24 | + "type": "select", |
| 25 | + "items": [ |
| 26 | + "serial", |
| 27 | + "parallel", |
| 28 | + ], |
| 29 | + }, |
| 30 | + { |
| 31 | + "attribute": "selection", |
| 32 | + "name": "Selection", |
| 33 | + "description": "MDAnalysis selection phrase", |
| 34 | + "type": "str", |
| 35 | + }, |
| 36 | + { |
| 37 | + "attribute": "msd_type", |
| 38 | + "name": "MSD type", |
| 39 | + "description": "Desired dimensions to be included in the MSD", |
| 40 | + "type": "select", |
| 41 | + "items": [ |
| 42 | + "xyz", |
| 43 | + "xy", |
| 44 | + "yz", |
| 45 | + "xz", |
| 46 | + "x", |
| 47 | + "y", |
| 48 | + "z", |
| 49 | + ], |
| 50 | + }, |
| 51 | + { |
| 52 | + "attribute": "show_particle_msds", |
| 53 | + "name": "Show particle MSDs", |
| 54 | + "description": "Show MSDs for individual particles of the selection", |
| 55 | + "type": "bool", |
| 56 | + }, |
| 57 | + { |
| 58 | + "attribute": "log_scale", |
| 59 | + "name": "Log scale", |
| 60 | + "description": "Use a log scale for the axes", |
| 61 | + "type": "bool", |
| 62 | + }, |
| 63 | + { |
| 64 | + "attribute": "custom_title", |
| 65 | + "name": "Custom title", |
| 66 | + "description": "Custom title for the plot", |
| 67 | + "type": "str", |
| 68 | + }, |
| 69 | + ] |
| 70 | + |
| 71 | + def __init__(self): |
| 72 | + super().__init__() |
| 73 | + self.msd = None |
| 74 | + self.selection = "all" |
| 75 | + self.msd_type = "xyz" |
| 76 | + self.log_scale = False |
| 77 | + self.show_particle_msds = False |
| 78 | + self.title = "MSD" |
| 79 | + self.custom_title = None |
| 80 | + self._setup_plot() |
| 81 | + |
| 82 | + def _setup_plot(self): |
| 83 | + """Setup matplotlib plot""" |
| 84 | + self.fig, self.ax = plt.subplots() |
| 85 | + # use non-empty values to prevent initial exception |
| 86 | + # if widget is configured to use log scale |
| 87 | + (self.plot,) = self.ax.plot([1], [1], color="red", zorder=2) |
| 88 | + self.lc = LineCollection([], colors="gray", alpha=0.2, lw=0.5, zorder=1) |
| 89 | + self.ax.add_collection(self.lc) |
| 90 | + self.ax.set_xlabel(r"Lag time $\Delta$t (ps)") |
| 91 | + self.ax.set_ylabel(r"MSD ($\AA^2$)") |
| 92 | + self.ax.grid(True, linestyle="--", alpha=0.6) |
| 93 | + self._set_title() |
| 94 | + self._set_axes_scale() |
| 95 | + |
| 96 | + def _set_title(self): |
| 97 | + """Set plot title""" |
| 98 | + self.ax.set_title(self.custom_title if self.custom_title else self.title) |
| 99 | + |
| 100 | + def _set_axes_scale(self): |
| 101 | + """Set axes scale""" |
| 102 | + self.ax.set_xscale("log" if self.log_scale else "linear") |
| 103 | + self.ax.set_yscale("log" if self.log_scale else "linear") |
| 104 | + |
| 105 | + def _create_msd(self): |
| 106 | + """Create msd instance""" |
| 107 | + self.msd = SlidingWindowMSD( |
| 108 | + self.u, |
| 109 | + select=self.selection, |
| 110 | + msd_type=self.msd_type, |
| 111 | + show_particle_msds=self.show_particle_msds, |
| 112 | + ) |
| 113 | + self.title = f"MSD of '{self.selection}'" |
| 114 | + self._set_title() |
| 115 | + |
| 116 | + def on_post_create(self): |
| 117 | + """on_post_create handler""" |
| 118 | + self._set_title() |
| 119 | + self._set_axes_scale() |
| 120 | + |
| 121 | + def on_post_connect(self): |
| 122 | + """on_post_connect handler""" |
| 123 | + self._create_msd() |
| 124 | + |
| 125 | + def on_input_change(self, attribute, _old_value, new_value): |
| 126 | + """on_input_change handler""" |
| 127 | + if attribute == "custom_title": |
| 128 | + self._set_title() |
| 129 | + elif attribute == "log_scale": |
| 130 | + self._set_axes_scale() |
| 131 | + elif attribute == "_run_mode": |
| 132 | + pass |
| 133 | + else: |
| 134 | + self._create_msd() |
| 135 | + |
| 136 | + def _compute(self, parallel: bool = False): |
| 137 | + """Run MSD for the current timesteps window""" |
| 138 | + return self.msd.run(parallel=parallel) |
| 139 | + |
| 140 | + def _update_plot(self, x, y1, y2): |
| 141 | + """Update plot with computed values""" |
| 142 | + self.plot.set_data(x, y1) |
| 143 | + self.lc.set_segments(y2 if self.show_particle_msds else []) |
| 144 | + self.ax.relim() |
| 145 | + self.ax.autoscale_view() |
| 146 | + self.fig.canvas.draw() |
| 147 | + display(self.fig) |
| 148 | + |
| 149 | + def run_every_frame(self): |
| 150 | + """every-frame run handler""" |
| 151 | + x, y1, y2, _ = self._compute() |
| 152 | + self._update_plot(x, y1, y2) |
| 153 | + |
| 154 | + def get_parallel_job(self): |
| 155 | + """get parallel job handler""" |
| 156 | + return delayed(self._compute)(parallel=True) |
| 157 | + |
| 158 | + def apply_parallel_results(self, values): |
| 159 | + """apply parallel results handler""" |
| 160 | + x, y1, y2, (v1, v2, v3, v4) = values |
| 161 | + self._update_plot(x, y1, y2) |
| 162 | + # update msd state |
| 163 | + self.msd.msd_sums = v1 |
| 164 | + self.msd.msd_counts = v2 |
| 165 | + if self.show_particle_msds: |
| 166 | + self.msd.particle_msd_sums = v3 |
| 167 | + self.msd.particle_msd_counts = v4 |
| 168 | + |
| 169 | + |
| 170 | +class SlidingWindowMSD: |
| 171 | + """Sliding Window MSD |
| 172 | +
|
| 173 | + Calculate MSD for a sliding window of frames |
| 174 | +
|
| 175 | + """ |
| 176 | + |
| 177 | + def __init__( |
| 178 | + self, |
| 179 | + u: mda.Universe, |
| 180 | + select: str = "all", |
| 181 | + msd_type: str = "xyz", |
| 182 | + show_particle_msds: bool = False, |
| 183 | + ): |
| 184 | + self.u = u |
| 185 | + self.select = select |
| 186 | + self.msd_type = msd_type |
| 187 | + self.show_particle_msds = show_particle_msds |
| 188 | + self._parse_msd_type() |
| 189 | + self.ag = u.select_atoms(self.select) |
| 190 | + self.n_atoms = self.ag.atoms.n_atoms |
| 191 | + self.n_lags = u.trajectory.buffer_size |
| 192 | + self.msd_sums = np.zeros(self.n_lags) |
| 193 | + self.msd_counts = np.zeros(self.n_lags, dtype=int) |
| 194 | + self.msd_counts[0] = 1 |
| 195 | + if self.show_particle_msds: |
| 196 | + self.particle_msd_sums = np.zeros((self.n_lags, self.n_atoms)) |
| 197 | + self.particle_msd_counts = np.zeros((self.n_lags, self.n_atoms), dtype=int) |
| 198 | + self.particle_msd_counts[0, :] = 1 |
| 199 | + |
| 200 | + def _parse_msd_type(self): |
| 201 | + """Sets up the desired dimensionality of the MSD.""" |
| 202 | + keys = { |
| 203 | + "x": [0], |
| 204 | + "y": [1], |
| 205 | + "z": [2], |
| 206 | + "xy": [0, 1], |
| 207 | + "xz": [0, 2], |
| 208 | + "yz": [1, 2], |
| 209 | + "xyz": [0, 1, 2], |
| 210 | + } |
| 211 | + self._dim = keys[self.msd_type.lower()] |
| 212 | + |
| 213 | + def run(self, parallel: bool = False) -> tuple: |
| 214 | + """Run MSD for the current window""" |
| 215 | + |
| 216 | + n = len(self.u.trajectory) # buffer / window might not be full yet |
| 217 | + positions_current = self.ag.positions[:, self._dim] |
| 218 | + for i in range(n - 1): |
| 219 | + lag = n - 1 - i |
| 220 | + _ = self.u.trajectory[i] # set the buffered trajectory frame |
| 221 | + disp = positions_current - self.ag.positions[:, self._dim] |
| 222 | + squared_disp = np.sum(disp**2, axis=1) |
| 223 | + msd = np.mean(squared_disp) |
| 224 | + self.msd_sums[lag] += msd |
| 225 | + self.msd_counts[lag] += 1 |
| 226 | + if self.show_particle_msds: |
| 227 | + self.particle_msd_sums[lag, :] += squared_disp |
| 228 | + self.particle_msd_counts[lag, :] += 1 |
| 229 | + |
| 230 | + # We will have at least 2 frames by the time we are here. |
| 231 | + # frame_dt will ensure the delta_t is correct even if we have step |
| 232 | + # value (other than 1) configured in the universe configuration |
| 233 | + frame_dt = round(self.u.trajectory[1].time - self.u.trajectory[0].time, 2) |
| 234 | + delta_t_values = np.arange(n) * frame_dt |
| 235 | + avg_msds = self.msd_sums[:n] / self.msd_counts[:n] |
| 236 | + msds_by_particle_lines = None |
| 237 | + if self.show_particle_msds: |
| 238 | + msds_by_particle_array = ( |
| 239 | + self.particle_msd_sums[:n, :] / self.particle_msd_counts[:n, :] |
| 240 | + ) |
| 241 | + msds_by_particle_lines = np.empty((self.n_atoms, n, 2)) |
| 242 | + msds_by_particle_lines[:, :, 0] = delta_t_values |
| 243 | + msds_by_particle_lines[:, :, 1] = msds_by_particle_array.T |
| 244 | + |
| 245 | + return ( |
| 246 | + delta_t_values, |
| 247 | + avg_msds, |
| 248 | + msds_by_particle_lines, |
| 249 | + ( |
| 250 | + self.msd_sums, |
| 251 | + self.msd_counts, |
| 252 | + self.particle_msd_sums if self.show_particle_msds else None, |
| 253 | + self.particle_msd_counts if self.show_particle_msds else None, |
| 254 | + ) |
| 255 | + if parallel |
| 256 | + else (None,) * 4, |
| 257 | + ) |
0 commit comments