Source code for pydna_epbd.simulation.mc_simulation

from math import log, exp
from random import random, normalvariate

from pydna_epbd.simulation.dna import DNA
from pydna_epbd.monitors.all_monitors import Monitors
from pydna_epbd.simulation.constants import ro, beta1_div_sqrt_two, one_div_sqrt2


[docs]class Simulation: def __init__(self, dna: DNA) -> None: """Init Simulation object. Args: dna (DNA): A DNA object to run simulation on it. """ super(Simulation, self).__init__() self.dna = dna self.coords_state_saved = [[0.0] * 2 for _ in range(self.dna.n_nt_bases)] # https://en.wikipedia.org/wiki/Boltzmann_constant in eV/K self.KB = 0.0000861733034 self.ACCEPT_CUTOFF = 25.00
[docs] def init_temp(self, temperature): """Init simulation temperature. Args: temperature (float): Temperature in Kelvin. """ self.beta_div_one = self.KB * temperature
[docs] def execute(self, monitors: Monitors, total_steps, preheating_steps): """Execute the simulation for total_steps (preheating+post_preheating). Args: monitors (Monitors): Record different aspects of MCMC simulation of pyDNA-EPBD model. total_steps (int): Preheating steps + Post preheating steps. preheating_steps (int): Number of preheating steps, when mostly monitors do not collect anything. """ for step_no in range(total_steps): # self.__one_move() for i in range(self.dna.n_nt_bases): # a major drawback self.__move_bp() if step_no < preheating_steps: monitors.collect_at_step_preheat(step_no) else: monitors.collect_at_step(step_no)
def __move_bp(self): """Moves a randomly selected bp and updates new DNA state and corresponding energy.""" n = int( self.dna.n_nt_bases * random() ) # selecting random n-th base-pair, get_random_displacement() # n_next = (n + 1) % self.dna.n_nt_bases # n_previous = (n - 1 + self.dna.n_nt_bases) % self.dna.n_nt_bases n_next = n + 1 if n_next >= self.dna.n_nt_bases: n_next = 0 n_previous = n - 1 if n_previous < 0: n_previous = self.dna.n_nt_bases - 1 # changing the n-th base pair's coordinate to the direction self.__do_random_displacement(n) # recalculating the energy etotal = ( self.dna.total_energy - self.dna.bp_energies[n][0] - self.dna.bp_energies[n][1] - self.dna.bp_energies[n_next][1] ) u2 = self.__Umors(n) w1, w3 = self.__Wstack(n_previous, n, n_next) etotal += u2 + w1 + w3 # checking energy and Metropolis criteria if self.dna.total_energy == etotal: # equal energy exit the function return if etotal < self.dna.total_energy: # we are reducing the energy of the system self.__assign_new_state(n, n_next, u2, w1, w3, etotal) else: # kind of exploration DELTA_E = self.dna.total_energy - etotal # diff of previous energy and current energy if DELTA_E > self.beta_div_one * log(random()): # if Metropolis criteria is OK self.__assign_new_state(n, n_next, u2, w1, w3, etotal) else: self.__revert_old_state(n) def __assign_new_state(self, n, n_next, u2, w1, w3, etotal): """Assign new DNA state by updating coordinates, coordinate distance and bps Morse and stacking potentials. Args: n (int): n-th bp. n_next (int): Next of n-th bp. Can be circular. u2 (float): Morse potential of n-th bp. w1 (float): Stacking potential between n and n_previous bps. w3 (float): Stacking potential between n and n_next bps. etotal (float): New energy due to the bp movement. """ # coordinates self.coords_state_saved[n][0] = self.dna.coords_state[n][0] self.coords_state_saved[n][1] = self.dna.coords_state[n][1] # energy self.dna.total_energy = etotal self.dna.bp_energies[n][0] = u2 self.dna.bp_energies[n][1] = w1 self.dna.bp_energies[n_next][1] = w3 # y_n self.dna.coords_dist[n] = ( self.dna.coords_state[n][0] - self.dna.coords_state[n][1] ) * one_div_sqrt2 def __revert_old_state(self, n): """Revert to old state. Args: n (int): n-th bp. """ self.dna.coords_state[n][0] = self.coords_state_saved[n][0] self.dna.coords_state[n][1] = self.coords_state_saved[n][1] # bp_energies and total_energy are calculated but not updated, so do not need to revert def __Umors(self, n): """Computes Morse potentials at n-th bp. Args: n (int): n-th bp. Returns: float: Computed Morse potential. """ y_n = self.dna.coords_state[n][0] - self.dna.coords_state[n][1] Dn = self.dna.DA[n][0] An_div_sqrt_two = self.dna.DA_div_sqrt_two[n][1] return Dn * (exp(-An_div_sqrt_two * y_n) - 1) ** 2 def __Wstack(self, n_previous, n, n_next): """Computes stacking potentials. The relation between n_previous and n_next with n-th bp can be circular. Args: n_previous (int): Previous bp of n-th bp. n (int): n-th bp. n_next (int): Next bp of n-th bp. Returns: float, float: Stacking potentials between n- and n-previous bps, and n- and n-next bps. """ y_n_previous = ( self.dna.coords_state[n_previous][0] - self.dna.coords_state[n_previous][1] ) y_n = self.dna.coords_state[n][0] - self.dna.coords_state[n][1] y_n_next = self.dna.coords_state[n_next][0] - self.dna.coords_state[n_next][1] Kn_div_four = self.dna.kn_div_four[n] w1 = ( Kn_div_four * (1 + ro * exp(-beta1_div_sqrt_two * (y_n_previous + y_n))) * (y_n_previous - y_n) ** 2 ) Kn_div_four = self.dna.kn_div_four[n_next] w3 = ( Kn_div_four * (1 + ro * exp(-beta1_div_sqrt_two * (y_n_next + y_n))) * (y_n_next - y_n) ** 2 ) return w1, w3 def __do_random_displacement(self, n): """This changes the new coordinate state and keep intact of the coorditate state. Args: n (int): n-th bp. """ dx = normalvariate(mu=0.0, sigma=1.0) # get_gasdev() if random() > 0.5: # LEFT, get_l_or_r() self.dna.coords_state[n][0] += dx if ( abs(self.dna.coords_state[n][0] - self.coords_state_saved[n][1]) > self.ACCEPT_CUTOFF ): self.dna.coords_state[n][0] -= dx # reverting the change else: # RIGHT self.dna.coords_state[n][1] += dx if ( abs(self.dna.coords_state[n][1] - self.coords_state_saved[n][0]) > self.ACCEPT_CUTOFF ): self.dna.coords_state[n][1] -= dx # reverting the change