|
33 | 33 |
|
34 | 34 | #------------------------------------------------------------------#
|
35 | 35 | # Helper functions
|
36 |
| - |
| 36 | +def compute_acf_general_theory(i, j, t, a, zeta=1.0): |
| 37 | + """ |
| 38 | + Numerically compute the autocorrelation function (ACF) for monomers i, j |
| 39 | + using the connectivity matrix `a`. Returns both the time-dependent |
| 40 | + ACF and the corresponding MSD for each time point. |
| 41 | +
|
| 42 | + Parameters |
| 43 | + ---------- |
| 44 | + i, j : int |
| 45 | + Indices of the monomers for which to calculate the correlation function. |
| 46 | + t : array_like |
| 47 | + A 1D array of time points (lag times). |
| 48 | + a : np.ndarray |
| 49 | + The connectivity (or "Laplacian") matrix for the polymer/chain. |
| 50 | + zeta : float |
| 51 | + Friction coefficient (if part of the model). Default is 1.0. |
| 52 | +
|
| 53 | + Returns |
| 54 | + ------- |
| 55 | + output : np.ndarray |
| 56 | + 2D array. First column is time `t`, second column is the ACF values at each time. |
| 57 | + msd : np.ndarray |
| 58 | + 1D array of the same length as `t`, giving the mean-square displacement |
| 59 | + inferred from the ACF. This is returned as a second column alongside `t`. |
| 60 | + """ |
| 61 | + eigvalue, eigvector = scipy.linalg.eigh(a) |
| 62 | + eigvalue_inv = 1.0 / eigvalue |
| 63 | + |
| 64 | + # difference in eigenvector components for monomers i and j |
| 65 | + vpi_vpj = eigvector[i, :] - eigvector[j, :] |
| 66 | + |
| 67 | + # normal_modes_square_mean = -(1 / eigenvalue) but filter out any inf |
| 68 | + normal_modes_square_mean = - np.nan_to_num(eigvalue_inv, posinf=0.0, neginf=0.0) |
| 69 | + |
| 70 | + # Expand time dimension for broadcast |
| 71 | + t_reshaped = np.expand_dims(t, axis=-1) |
| 72 | + # Effective relaxation times |
| 73 | + tau_p = - zeta / eigvalue |
| 74 | + decay_factor = np.exp(-t_reshaped / tau_p) |
| 75 | + |
| 76 | + # ACF(t) |
| 77 | + res = 3.0 * np.sum(vpi_vpj**2 * decay_factor * normal_modes_square_mean, axis=-1) |
| 78 | + # Equilibrium part |
| 79 | + res_eq = 3.0 * np.sum(vpi_vpj**2 * normal_modes_square_mean, axis=-1) |
| 80 | + |
| 81 | + # Combine results: first column is time, second is res |
| 82 | + two_point_acf = np.column_stack((t, res)) |
| 83 | + |
| 84 | + # The mean-square displacement from the correlation function |
| 85 | + two_point_msd = 2.0 * (res_eq - two_point_acf[:, 1]) |
| 86 | + two_point_msd = np.column_stack((t, two_point_msd)) |
| 87 | + |
| 88 | + return two_point_acf, two_point_msd |
| 89 | + |
| 90 | +def compute_m1_general_theory(i, t, a, zeta=1.0): |
| 91 | + """ |
| 92 | + Compute the single-monomer mean-square displacement (MSD) for monomer i, |
| 93 | + given the connectivity matrix `a`. |
| 94 | +
|
| 95 | + Parameters |
| 96 | + ---------- |
| 97 | + i : int |
| 98 | + Index of the monomer. |
| 99 | + t : array_like |
| 100 | + A 1D array of time points (lag times). |
| 101 | + a : np.ndarray |
| 102 | + The connectivity (or "Laplacian") matrix for the polymer/chain. |
| 103 | + zeta : float |
| 104 | + Friction coefficient. Default is 1.0. |
| 105 | +
|
| 106 | + Returns |
| 107 | + ------- |
| 108 | + msd : np.ndarray |
| 109 | + 2D array. First column is time `t`, second column is the MSD for |
| 110 | + monomer i at those times. |
| 111 | + """ |
| 112 | + eigvalue, eigvector = scipy.linalg.eigh(a) |
| 113 | + eigvalue_inv = 1.0 / eigvalue |
| 114 | + vpi = eigvector[i, :] |
| 115 | + |
| 116 | + # Filter out infinities |
| 117 | + normal_modes_square_mean = - np.nan_to_num(eigvalue_inv, posinf=0.0, neginf=0.0) |
| 118 | + |
| 119 | + # Expand time dimension for broadcast |
| 120 | + t_reshaped = np.expand_dims(t, axis=-1) |
| 121 | + tau_p = - zeta / eigvalue |
| 122 | + decay_factor = np.exp(-t_reshaped / tau_p) |
| 123 | + |
| 124 | + # The time-dependent part |
| 125 | + res = 3.0 * np.sum(vpi**2 * decay_factor * normal_modes_square_mean, axis=-1) |
| 126 | + # Equilibrium radius |
| 127 | + r2_eq = 3.0 * np.sum(vpi**2 * normal_modes_square_mean, axis=-1) |
| 128 | + # MSD |
| 129 | + msd_data = 2.0 * (r2_eq - res) |
| 130 | + |
| 131 | + # Combine time with MSD |
| 132 | + msd = np.column_stack((t, msd_data)) |
| 133 | + return msd |
| 134 | + |
| 135 | +def Ornstein_Uhlenbeck_update(x, dt, k, zeta, beta, b = 0.0, method='euler-maruyama'): |
| 136 | + """ |
| 137 | + Update variable x for a Ornstein Uhlenbeck process |
| 138 | + x: Array for value of x of each degree of freedom |
| 139 | + k: Array for spring constant for each degree of freedom |
| 140 | + zeta: one value |
| 141 | + beta: one value |
| 142 | + """ |
| 143 | + if isinstance(x, np.ndarray): |
| 144 | + rand_noise = np.random.randn(*x.shape) |
| 145 | + else: |
| 146 | + rand_noise = np.random.randn() |
| 147 | + |
| 148 | + if method == 'euler-maruyama': |
| 149 | + dx = - k[:, np.newaxis] * x * dt / zeta + b * dt / zeta + np.sqrt(2.0 * dt / (zeta * beta)) * rand_noise |
| 150 | + x_new = x + dx |
| 151 | + elif method == 'exact': |
| 152 | + theta = k[:, np.newaxis] / zeta |
| 153 | + sigma = (2. / (zeta * beta)) ** .5 |
| 154 | + mu = np.exp(- theta * dt) |
| 155 | + x_new = x * mu + np.nan_to_num(np.sqrt((sigma ** 2. / (2. * theta)) * (1. - mu ** 2.))) * rand_noise |
| 156 | + return x_new |
37 | 157 |
|
38 | 158 | def construct_connectivity_matrix_rouse(n, k):
|
39 | 159 | """
|
@@ -244,8 +364,6 @@ def objective_func(rc, A_mtx, cmap_exp):
|
244 | 364 | return res
|
245 | 365 |
|
246 | 366 | # FUNCTION TO CONVERT CMAP TO DMAP
|
247 |
| - |
248 |
| - |
249 | 367 | def cmap2dmap_core(cmap_exp, rc, alpha, not_normalize, norm_max=1.0, mode='log'):
|
250 | 368 | # rc is the prefactor
|
251 | 369 | # norm_max is the maximum contact probability
|
@@ -445,6 +563,123 @@ def run(self, epoch, general_method='optimization', **kwargs):
|
445 | 563 |
|
446 | 564 | return loss_array, dmap_maxent, self.A
|
447 | 565 |
|
| 566 | +class Dynamics: |
| 567 | + def __init__(self, input, M=None, k=None, model=None): |
| 568 | + if isinstance(input, int) and M is None and k is not None: |
| 569 | + if not isinstance(k, int) and not isinstance(k, float): |
| 570 | + sys.stdout.write('Spring constant should be a number') |
| 571 | + sys.exit(0) |
| 572 | + if model != 'rouse' and model is not None: |
| 573 | + sys.stdout.write("Please specify model to be 'rouse'") |
| 574 | + sys.exit(0) |
| 575 | + |
| 576 | + self.A = construct_connectivity_matrix_rouse(input, k) |
| 577 | + self.eigvalue, self.eigvector = scipy.linalg.eigh(self.A) |
| 578 | + self.N = input |
| 579 | + elif isinstance(input, int) and M is not None and k is not None: |
| 580 | + if not isinstance(k, int) and not isinstance(k, float): |
| 581 | + sys.stdout.write('Spring constant should be a number') |
| 582 | + sys.exit(0) |
| 583 | + if not isinstance(M, int): |
| 584 | + sys.stdout.write('Number of random cross links need to be an integer') |
| 585 | + sys.exit(0) |
| 586 | + if model != 'random': |
| 587 | + sys.stdout.write("Please specify model to be 'random'") |
| 588 | + sys.exit(0) |
| 589 | + self.A = construct_connectivity_matrix_random(input, M, k) |
| 590 | + self.eigvalue, self.eigvector = scipy.linalg.eigh(self.A) |
| 591 | + self.N = input |
| 592 | + elif isinstance(input, np.ndarray) and M is None and k is None: |
| 593 | + if len(input.shape) !=2 or input.shape[0] != input.shape[1]: |
| 594 | + sys.stdout.write('The connectivity matrix should be a square matrix') |
| 595 | + sys.exit(0) |
| 596 | + if not np.allclose(input, input.T): |
| 597 | + sys.stdout.write('The connectivity matrix should be a symmetrix real matrix') |
| 598 | + sys.exit(0) |
| 599 | + |
| 600 | + self.A = input |
| 601 | + self.eigvalue, self.eigvector = scipy.linalg.eigh(self.A) |
| 602 | + self.N = input.shape[0] |
| 603 | + |
| 604 | + def generateXYZ(self, force_positive_definite = False): |
| 605 | + self.xyz = a2xyz(self.A, force_positive_definite = force_positive_definite) |
| 606 | + self.modes = self.eigvector.T @ self.xyz |
| 607 | + |
| 608 | + def initialize(self, dt, zeta, beta): |
| 609 | + if not isinstance(dt, int) and not isinstance(dt, float): |
| 610 | + sys.stdout.write('Time step should be a number') |
| 611 | + sys.exit(0) |
| 612 | + if not isinstance(zeta, int) and not isinstance(zeta, float): |
| 613 | + sys.stdout.write('Friction coefficient step should be a number') |
| 614 | + sys.exit(0) |
| 615 | + if not isinstance(beta, int) and not isinstance(beta, float): |
| 616 | + sys.stdout.write('Temperature step should be a number') |
| 617 | + sys.exit(0) |
| 618 | + elif beta <= 0.0: |
| 619 | + sys.stdout.write('Temperature should be positive') |
| 620 | + sys.exit(0) |
| 621 | + |
| 622 | + self.zeta = zeta |
| 623 | + self.beta = beta |
| 624 | + self.dt = dt |
| 625 | + |
| 626 | + def updateModes(self, method='euler-maruyama'): |
| 627 | + try: |
| 628 | + self.zeta |
| 629 | + self.beta |
| 630 | + self.dt |
| 631 | + except AttributeError: |
| 632 | + sys.stdout.write('Please run initialize() first') |
| 633 | + sys.exit(0) |
| 634 | + |
| 635 | + self.modes = Ornstein_Uhlenbeck_update(self.modes, self.dt, - self.eigvalue, self.zeta, self.beta, method=method) |
| 636 | + # self.modes = OU.OU(self.modes, self.dt, - self.eigvalue, self.zeta, self.beta) |
| 637 | + |
| 638 | + def updateXYZ(self): |
| 639 | + self.xyz = self.eigvector @ self.modes |
| 640 | + |
| 641 | + def run(self, T, update=1, every=1, initial_conformation=None, method='euler-maruyama'): |
| 642 | + """ |
| 643 | + T: number of timesteps |
| 644 | + update: update x,y,z positions every this many timesteps |
| 645 | + every: save x,y,z positions to the trajectory every this many timesteps |
| 646 | + initial_conformation: initial conformation of the simulation |
| 647 | + """ |
| 648 | + if not isinstance(T, int): |
| 649 | + sys.stdout.write('Number of steps should be an integer') |
| 650 | + sys.exit(0) |
| 651 | + |
| 652 | + if initial_conformation is None: |
| 653 | + try: |
| 654 | + self.xyz |
| 655 | + self.modes |
| 656 | + except AttributeError: |
| 657 | + self.generateXYZ() |
| 658 | + else: |
| 659 | + if initial_conformation.shape[0] != self.N: |
| 660 | + sys.stdout.write('Number of particles is not correct') |
| 661 | + sys.exit(0) |
| 662 | + if initial_conformation.shape[1] != 3: |
| 663 | + sys.stdout.write('The dimension should be three') |
| 664 | + sys.exit(0) |
| 665 | + self.xyz = initial_conformation |
| 666 | + |
| 667 | + self.traj = [] |
| 668 | + for t in tqdm(range(T)): |
| 669 | + if t % update == 0: |
| 670 | + self.updateXYZ() |
| 671 | + if t % every == 0: |
| 672 | + self.updateXYZ() |
| 673 | + self.traj.append(self.xyz) |
| 674 | + #sys.stdout.write('\rTimestep {}'.format(t+1)) |
| 675 | + #sys.stdout.flush() |
| 676 | + self.updateModes(method=method) |
| 677 | + |
| 678 | + self.traj = np.array(self.traj) |
| 679 | + |
| 680 | + def reset(self): |
| 681 | + self.generateXYZ() |
| 682 | + |
448 | 683 |
|
449 | 684 | @click.command()
|
450 | 685 | @click.argument('input', nargs=1)
|
|
0 commit comments