"""A module for the Metropolis-Hastings Markov chain Monte Carlo method.This module consist of the class ``MHMCMC`` which containsattributes and methods that allow single-chain quantitiesto be computed using the Metropolis-Hastings Markov chain Monte Carlo method.Example: Import and instantiate the class with a single-chain model: >>> from ufjc import uFJC >>> from ufjc.monte_carlo import MHMCMC >>> class_instance = MHMCMC(uFJC())"""# Import internal modulesfrom.utilityimportBasicUtility# Import external modulesimportnumpyasnpimportnumpy.linalgaslafromsysimportplatformifplatform=="linux"orplatform=="linux2":importmultiprocessingasmp
[docs]classMHMCMC(BasicUtility):"""The Metropolis-Hastings Markov chain Monte Carlo class. This class contains attributes and methods used to compute statistical thermodynamic quantities using the Metropolis-Hastings Markov chain Monte Carlo method :cite:`monte_carlo-haile1992molecular` for a given single-chain model. Attributes: N_b: The number of chain links. Inherited from ``single_chain_model``. beta_U_config: The configurational potential energy. Inherited from ``single_chain_model``. beta_Pi_config: The configurational total potential energy. Inherited from ``single_chain_model``. init_config: The default initial configuration. Inherited from ``single_chain_model``. """def__init__(self,single_chain_model):"""Initializes the ``MHMCMC`` class. Initialize and inherit all attributes and methods from a ``BasicUtility`` class instance, and certain attributes and methods from the given single-chain model. Args: single_chain_model(object): The single-chain model instance. """BasicUtility.__init__(self)# Inherit necessary attributes from the single-chain modelself.N_b=single_chain_model.N_bself.init_config=single_chain_model.init_configself.beta_U_config=single_chain_model.beta_U_configself.beta_Pi_config=single_chain_model.beta_Pi_config
[docs]deftrial_config(self,prev_config,**kwargs):"""Generates trial configurations given a previous configuration. This function returns a trial configuration given a previous starting configuration. The possibility of rigid-body translation is prevented by fixing the first coordinate at the origin. In the isometric ensemble, the last coordinate is left unchanged in order to keep the end-to-end vector fixed. Args: prev_config (numpy.ndarray): The previous configuration. **kwargs: Arbitrary keyword arguments. Returns: numpy.ndarray: The generated trial configuration. Example: Generate a trial configuration in the isometric ensemble and ensure that the end-to-end remains unchanged: >>> from ufjc import uFJC >>> from ufjc.monte_carlo import MHMCMC >>> model = uFJC(N_b=8) >>> prev_config = model.init_config >>> trial_config = MHMCMC(model).trial_config(prev_config, ... ensemble='isometric') >>> (prev_config == trial_config)[(0,-1),:] array([[ True, True, True], [ True, True, True]]) """cov_config=kwargs.get('transition_variance',1e-2)*np.eye(3)delta_config=np.random.multivariate_normal(np.zeros(3),cov_config,len(prev_config))delta_config[0,:]=0ifkwargs.get('ensemble','isotensional')=='isometric':delta_config[-1,:]=0returnprev_config+delta_config
[docs]defmh_next_config(self,prev_config,**kwargs):"""Generates accepted configurations given a previous configuration. This function returns an accepted configuration given a previous configuration. Trial configurations are generated and repeatedly rejected until accepted as the next configuration based on the Metropolis-Hastings algorithm. Args: prev_config (numpy.ndarray): The previous configuration. **kwargs: Arbitrary keyword arguments. Passed to ``trial_config``. Returns: numpy.ndarray: The accepted (next) configuration. """next_config=Noneurng=kwargs.get('urng',np.random.uniform)whilenext_configisNone:trial_config=self.trial_config(prev_config,**kwargs)delta_bEc=self.__bEc(trial_config)-self.__bEc(prev_config)if(delta_bEc<0)or(np.exp(-delta_bEc)>urng()):next_config=trial_configreturnnext_config
[docs]defmh_next_config_append(self,prev_configs,**kwargs):"""Append next configuration onto a history of previous configurations. This function calls ``mh_next_config`` and appends the resulting next configuration onto a given history of previous configurations. Args: prev_configs (numpy.ndarray): The previous history of configurations. **kwargs: Arbitrary keyword arguments. Passed to ``mh_next_config``. Returns: numpy.ndarray: The updated history of configurations. """returnnp.append(prev_configs,np.array([self.mh_next_config(prev_configs[-1],**kwargs)]),axis=0)
[docs]defburn_in(self,init_config,**kwargs):"""Burn-in (throw away samples) until in the effective sampling region. This function runs a Monte Carlo calculation without utilizing samples, instead throwing them away until a desired number have been thrown away or a desired tolerance has been reached. This is typically done to obtain a burned-in configuration to use as the initial configuration for an actual Monte Carlo calculation. Args: init_config (numpy.ndarray): The initial configuration. **kwargs: Arbitrary keyword arguments. Passed to ``mh_next_config_append``. Returns: numpy.ndarray: The final, burned-in configuration. """config_history=np.array([init_config])prev_rmc=init_config# Generate new configurations until certain criterion are mettol=kwargs.get('tol',np.inf)num_samples_burn_in=int(kwargs.get('num_samples_burn_in',int(1e3)))n_res=tol+1while(n_res>tol)or(len(config_history)<num_samples_burn_in):# Generate an accepted configurationconfig_history=self.mh_next_config_append(config_history,**kwargs)# Compute the normalized residual in mean configurationnext_rmc=np.mean(config_history,axis=0)n_res=la.norm(next_rmc-prev_rmc)/la.norm(prev_rmc)prev_rmc=next_rmc# Return the configuration at the end of the burn-in periodreturnconfig_history[-1]
[docs]defparallel_calculation(self,serial_fun,**kwargs):"""Monte Carlo calculation averaged over several parallel processes. This function performs several Monte Carlo calculations in parallel and returns the average result from all processes. Each Monte Carlo calculation is performed using ``serial_fun``. The default is to utilize all available processors. Args: serial_fun(function): The function for a single Monte Carlo calculation. **kwargs: Arbitrary keyword arguments. Passed to ``serial_fun``. Returns: float: The process-averaged result. Note: To avoid issues with ``multiprocessing`` and MacOS/Windows, ``num_processes`` is set to 1 for anything but Linux. """burned_in_config=self.burn_in(self.init_config,**kwargs)num_processes=1ifplatform=="linux"orplatform=="linux2":num_processes=kwargs.get('num_processes',mp.cpu_count())ifnum_processes>1:# Create an output queueoutput=mp.Queue()# Set up the function run by each processdeffun(seed,output):output.put(serial_fun(burned_in_config,urng=np.random.RandomState(seed).random,**kwargs))# Create the processes, each with a random seedprocesses=[mp.Process(target=fun,args=(seed,output))forseedinnp.random.randint(88,size=num_processes)]# Run each processforpinprocesses:p.start()# Exit each process after running them allforpinprocesses:p.join()# Get the results from each process from the output queueprocess_results=[output.get()forpinprocesses]# Calculate and return the mean over all processesreturnnp.mean(process_results)# Otherwise do a single serial calculationelse:returnserial_fun(burned_in_config,**kwargs)
[docs]defgamma_isotensional_MHMCMC_serial(self,config,**kwargs):r"""Serial Monte Carlo calculation function for the isotensional :math:`\gamma(\eta)`. This function computes the nondimensional end-to-end length using a Monte Carlo calculation given an initial configuration. Args: config (numpy.ndarray): The initial configuration, typically already burned-in. **kwargs: Arbitrary keyword arguments. Passed to ``mh_next_config``. Returns: float: The nondimensional end-to-end length. Example: Calculate the nondimensional end-to-end length under a nondimensional force of 23 and ensure that it is within one percent of the exact result: >>> import numpy as np >>> from ufjc import uFJC >>> from ufjc.monte_carlo import MHMCMC >>> gamma_MHMCMC = MHMCMC(uFJC()). \ ... gamma_isotensional_MHMCMC(23, num_processes=1) >>> gamma_exact = uFJC().gamma(23, approach='exact') >>> np.abs(gamma_exact - gamma_MHMCMC)/gamma_exact < 1e-2 array([ True]) """gamma_vector=0num_samples=int(kwargs.get('num_samples',int(1e4)))forcounterinrange(1,1+num_samples):gamma_vector_next=(config[-1,:]-config[0,:])/self.N_bgamma_vector+=(gamma_vector_next-gamma_vector)/counterconfig=self.mh_next_config(config,**kwargs)# Return gamma as a scalarreturnla.norm(gamma_vector)
[docs]defgamma_isotensional_MHMCMC(self,eta,**kwargs):r"""Parallel Monte Carlo calculation function for the isotensional :math:`\gamma(\eta)`. Given a nondimensional force, this function computes the nondimensional end-to-end length using the average of several Monte Carlo calculations run in parallel. Given multiple nondimensional forces, this function does the above in serial for each nondimensional force. Args: eta (array_like): The nondimensional force(s). **kwargs: Arbitrary keyword arguments. Passed to ``parallel_calculation``. Returns: numpy.ndarray: The nondimensional end-to-end length(s). Example: Calculate the nondimensional end-to-end length under a nondimensional force of 23 and ensure that it is within one percent of the exact result: >>> from ufjc import uFJC >>> from ufjc.monte_carlo import MHMCMC >>> gamma_MHMCMC = MHMCMC(uFJC()).gamma_isotensional_MHMCMC(23) >>> gamma_exact = uFJC().gamma(23, approach='exact') >>> np.abs(gamma_exact - gamma_MHMCMC)/gamma_exact < 1e-2 array([ True]) """eta=self.np_array(eta)gamma=np.zeros(eta.shape)forindexinrange(len(eta)):# Assume eta is aligned with the z-axis, without loss of generalityeta_vector=np.array([0,0,eta[index]])# Use the total potential energy Pi (isotensional ensemble)self.__bEc=lambdaconfig:self.beta_Pi_config(config,eta_vector)# Calculate gamma using an ensemble averagegamma[index]=self.parallel_calculation(self.gamma_isotensional_MHMCMC_serial,**kwargs)# Return the resulting averaged gammareturngamma