# -*- coding: utf-8 -*-
# Copyright (c) 2023, PyRETIS Development Team.
# Distributed under the LGPLv2.1+ License. See LICENSE for more info.
"""This module handles the creation of simulations from settings.
The different simulations are defined as objects which inherit
from the base :py:class:`.Simulation` class defined in
:py:mod:`pyretis.simulation.simulation`. Here, we are treating
each simulation with a special case.
Important methods defined here
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
create_simulation (:py:func:`.create_simulation`)
Method for creating a simulation object from settings.
create_ensemble (:py:func:`.create_ensemble`)
Method for creating an ensemble dictionary from settings.
create_ensembles (:py:func:`.create_ensembles`)
Method for creating a list of ensemble from settings.
create_nve_simulation (:py:func:`.create_mve_simulation`)
Method for creating a nve simulation object from settings.
create_mdflux_simulation (:py:func:`.create_mdflux_simulation`)
Method for creating a mdflux simulation object from settings.
create_umbrellaw_simulation (:py:func:`.create_umbrellaw_simulation`)
Method for creating a umbrellaw simulation object from settings.
create_tis_simulation (:py:func:`.create_tis_simulation`)
Method for creating a tis simulation object from settings.
create_retis_simulation (:py:func:`.create_retis_simulation`)
Method for creating a retis simulation object from settings.
prepare_system (:py:func:`.prepare_system`)
Method for creating a system object from settings.
prepare_engine (:py:func:`.prepare_engine`)
Method for creating an engine object from settings.
"""
import logging
import os
from pyretis.core.pathensemble import get_path_ensemble_class
from pyretis.setup.createforcefield import create_force_field
from pyretis.inout import print_to_screen
from pyretis.inout.settings import (add_default_settings,
add_specific_default_settings,
settings_from_restart)
from pyretis.setup.common import create_orderparameter
from pyretis.core.units import units_from_settings
from pyretis.core.random_gen import create_random_generator
from pyretis.simulation.md_simulation import (
SimulationMD,
SimulationNVE,
SimulationMDFlux,
)
from pyretis.simulation.mc_simulation import UmbrellaWindowSimulation
from pyretis.simulation.path_simulation import (
SimulationTIS,
SimulationRETIS
)
from pyretis.setup import (
create_system,
create_engine
)
from pyretis.inout.checker import (
check_engine,
check_ensemble,
)
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
logger.addHandler(logging.NullHandler())
__all__ = ['create_simulation', 'create_ensemble', 'create_ensembles',
'create_nve_simulation', 'create_mdflux_simulation',
'create_umbrellaw_simulation', 'create_tis_simulation',
'create_retis_simulation', 'prepare_system', 'prepare_engine']
[docs]def create_ensemble(settings):
"""Create the path ensemble from (ensemble) simulation settings.
Parameters
----------
settings : dict
This dict contains the settings needed to create the path
ensemble.
Returns
-------
ensemble : dict of
objects that contains all the information needed in the ensemble.
"""
i_ens = settings['tis']['ensemble_number']
logtxt = f'\nCREATING ENSEMBLE {i_ens}\n====================='
print_to_screen(logtxt, level='message')
logger.info(logtxt)
rgen_ens = create_random_generator(settings['tis'])
rgen_path = create_random_generator(settings['system'])
system = prepare_system(settings)
engine = prepare_engine(settings)
klass = get_path_ensemble_class(settings['particles']['type'])
interfaces = settings['simulation']['interfaces']
exe_dir = settings['simulation'].get('exe_path', os.path.abspath('.'))
path_ensemble = klass(i_ens, interfaces, rgen=rgen_path, exe_dir=exe_dir)
# for PPRETIS / PPTIS: correct the starting condition of the paths
if settings['simulation']['task'] in ['pptis', 'repptis']:
change_start_cond = True
# but skip this change if we are dealing with the [0-] ensemble
if i_ens == 0: # the first ensemble
if settings['simulation'].get('flux', False) or \
settings['simulation'].get('zero_left', False):
change_start_cond = False
if change_start_cond:
# adapt the starting condition of the paths
logger.info("adapted start_condition to R/L in ensemble %s", i_ens)
print('booger')
path_ensemble.start_condition = ['R', 'L']
path_ensemble.must_cross_M = True # used in the shooting move
order_function = create_orderparameter(settings)
# code for computing the permeability
if i_ens == 0 and settings['simulation'].get("permeability", False):
# the starting condition in [0-'] can be R or L
path_ensemble.start_condition = ['R', 'L']
# check mirror move settings
if hasattr(order_function, 'mirror_pos'):
offset = getattr(order_function, 'offset', 0)
moved_interfaces = [i - offset for i in interfaces]
left, right = moved_interfaces[0], moved_interfaces[2]
correct_mirror = (left+right)/2.
if abs(order_function.mirror_pos-correct_mirror) > 1E-5:
msg = "Order function should have a mirror at "
msg += f"{correct_mirror}, found one at "
msg += f"{order_function.mirror_pos} instead."
raise ValueError(msg)
# Add check to see if mirror makes sense
engine.can_use_order_function(order_function)
ensemble = {'engine': engine,
'system': system,
'order_function': order_function,
'interfaces': interfaces,
'exe_path': exe_dir,
'path_ensemble': path_ensemble,
'rgen': rgen_ens}
ensemble['system'].order = engine.calculate_order(ensemble)
return ensemble
[docs]def create_ensembles(settings):
"""Create a list of dictionary from ensembles simulation settings.
Parameters
----------
settings : dict
This dict contains the settings needed to create the path
ensemble.
Returns
-------
ensembles : list of dicts
List of ensembles.
"""
# Example:
# ensembles, assuming len(interfaces) = 3
# (RE)TIS: flux=False, zero_ensemble=False [1+]
# (RE)TIS: flux=False, zero_ensemble=True [0+], [1+]
# (RE)TIS: flux=True, zero_ensemble=True [0-], [0+], [1+]
# / : flux=True, zero_ensemble=False doesn't make sense
# so number of ensembles can be 1, 2, or 3
ensembles = []
intf = settings['simulation']['interfaces']
# for PPTIS or REPPTIS: memory
mem = settings.get(settings['simulation']['task'], {}).get('memory', 1)
j_ens = 0
# add [0-] if it exists
if settings['simulation'].get('flux', False) or \
settings['simulation'].get('zero_left', False):
reactant, middle, product = float('-inf'), intf[0], intf[0]
if settings['simulation'].get('zero_left', False):
reactant = settings['simulation']['zero_left']
if settings['simulation'].get('permeability', False):
middle = (settings['simulation']['zero_left'] + intf[0]) / 2
settings['ensemble'][j_ens]['simulation']['interfaces'] =\
[reactant, middle, product]
j_ens += 1
# add [0+] if it exists
if settings['simulation'].get('zero_ensemble', True):
reactant, middle, product = intf[0], intf[0], intf[-1]
# for PPTIS or REPPTIS: overwrite 'product' interface
if settings['simulation']['task'] in ['pptis', 'repptis']:
# mem is an integer, DEFAULT is 1
# intf cannot be more to the right than intf[-1]
product = intf[min(mem, len(intf)-1)]
settings['ensemble'][j_ens]['simulation']['interfaces'] = \
[reactant, middle, product]
j_ens += 1
# j_ens is the number of ensembles that is skipped
# set interfaces and set detect for [1+], [2+], ...
reactant, product = intf[0], intf[-1]
for i, i_ens in enumerate(range(j_ens, len(settings['ensemble']))):
middle = intf[i + 1] # the lambda_i interface
# for PPTIS / REPPTIS: overwrite 'reactant' and 'product' intf
if settings['simulation']['task'] in ['pptis', 'repptis']:
# mem is an integer, DEFAULT is 1
# intf cannot be more to the left than intf[0]
reactant = intf[max(0, i + 1 - mem)]
# intf cannot be more to the right than intf[-1]
product = intf[min(i + 1 + mem, len(intf)-1)]
# middle is not changed, actually
settings['ensemble'][i_ens]['simulation']['interfaces'] = \
[reactant, middle, product]
settings['ensemble'][i_ens]['tis']['detect'] = intf[i + 2] # next intf
# create all ensembles
for i in range(len(settings['ensemble'])):
ensembles.append(create_ensemble(settings['ensemble'][i]))
return ensembles
[docs]def create_nve_simulation(settings):
"""Set up and create a NVE simulation.
Parameters
----------
settings : dict
The settings needed to set up the simulation.
Returns
-------
SimulationNVE : object like :py:class:`.SimulationNVE`
The object representing the simulation to run.
"""
ensemble = {'engine': prepare_engine(settings),
'system': prepare_system(settings),
'rgen': create_random_generator(settings['simulation'])}
key_check('steps', settings)
controls = {'steps': settings['simulation']['steps'],
'startcycle': settings['simulation'].get('startcycle', 0)}
return SimulationNVE(ensemble, settings, controls)
def create_md_simulation(settings):
"""Set up and create a generic MD simulation.
Parameters
----------
settings : dict
The settings needed to set up the simulation.
Returns
-------
out : object like :py:class:`.SimulationMD`
The object representing the simulation to run.
"""
ensemble = {'engine': prepare_engine(settings),
'system': prepare_system(settings),
'order_function': create_orderparameter(settings)}
key_check('steps', settings)
controls = {'steps': settings['simulation']['steps'],
'startcycle': settings['simulation'].get('startcycle', 0)}
return SimulationMD(ensemble, settings, controls)
[docs]def create_mdflux_simulation(settings):
"""Set up and create a MD FLUX simulation.
Parameters
----------
settings : dict
The settings needed to set up the simulation.
Returns
-------
SimulationMDFlux : object like :py:class:`.SimulationMDFlux`
The object representing the simulation to run.
"""
engine = prepare_engine(settings)
order_function = create_orderparameter(settings)
ensemble = {'system': prepare_system(settings),
'engine': engine,
'order_function': order_function,
'rgen': create_random_generator(settings['simulation'])}
engine.can_use_order_function(order_function)
for key in ('steps', 'interfaces'):
key_check(key, settings)
controls = {'steps': settings['simulation']['steps'],
'startcycle': settings['simulation'].get('startcycle', 0)}
return SimulationMDFlux(ensemble, settings, controls)
[docs]def create_umbrellaw_simulation(settings):
"""Set up and create a Umbrella Window simulation.
Parameters
----------
settings : dict
The settings needed to set up the simulation.
Note that mincycle is trasmitted as steps to the object,
but is has a different meaning than the other
simulations.
Returns
-------
UmbrellaWindowSimulation : obj like :py:class:`.UmbrellaWindowSimulation`
The object representing the simulation to run.
"""
ensemble = {'system': prepare_system(settings),
'rgen': create_random_generator(settings['simulation'])}
for key in ('umbrella', 'overlap', 'maxdx', 'mincycle'):
key_check(key, settings)
controls = {'steps': settings['simulation']['mincycle'],
'startcycle': settings['simulation'].get('startcycle', 0)}
return UmbrellaWindowSimulation(ensemble, settings, controls)
[docs]def create_tis_simulation(settings):
"""Set up and create a single TIS simulation.
Parameters
----------
settings : dict
The settings needed to set up the simulation.
Returns
-------
SimulationTIS : object like :py:class:`.SimulationSingleTIS`
The object representing the simulation to run.
"""
check_ensemble(settings)
ensembles = create_ensembles(settings)
key_check('steps', settings)
controls = {'rgen': create_random_generator(settings['simulation']),
'steps': settings['simulation']['steps'],
'startcycle': settings['simulation'].get('startcycle', 0)}
return SimulationTIS(ensembles, settings, controls)
[docs]def create_retis_simulation(settings):
"""Set up and create a RETIS simulation.
Parameters
----------
settings : dict
This dictionary contains the settings for the simulation.
Returns
-------
SimulationRETIS : object like :py:class:`.SimulationRETIS`
The object representing the simulation to run.
"""
check_ensemble(settings)
ensembles = create_ensembles(settings)
key_check('steps', settings)
controls = {'rgen': create_random_generator(settings['simulation']),
'steps': settings['simulation']['steps'],
'startcycle': settings['simulation'].get('startcycle', 0)}
return SimulationRETIS(ensembles, settings, controls)
[docs]def prepare_system(settings):
"""Create a system from given settings.
Parameters
----------
settings : dict
This dictionary contains the settings for the simulation.
Returns
-------
syst : object like :py:class:`.syst`
This object will correspond to the selected simulation type.
"""
if settings.get('system', {}).get('obj', False):
return settings['system']['obj']
logtxt = 'Initializing unit system.'
print_to_screen(logtxt, level='info')
logger.info(logtxt)
units_from_settings(settings)
logtxt = 'Creating system from settings.'
print_to_screen(logtxt, level='info')
logger.info(logtxt)
system = create_system(settings)
logtxt = 'Creating force field.'
print_to_screen(logtxt, level='info')
logger.info(logtxt)
system.forcefield = create_force_field(settings)
system.particles.vpot = system.evaluate_potential()
system.extra_setup()
return system
[docs]def prepare_engine(settings):
"""Create an engine from given settings.
Parameters
----------
settings : dict
This dictionary contains the settings for the simulation.
Returns
-------
engine : object like :py:class:`.engine`
This object will correspond to the selected simulation type.
"""
if settings.get('engine', {}).get('obj', False):
return settings['engine']['obj']
logtxt = units_from_settings(settings)
print_to_screen(logtxt, level='info')
logger.info(logtxt)
check_engine(settings)
engine = create_engine(settings)
logtxt = f'Created engine "{engine}" from settings.'
print_to_screen(logtxt, level='info')
logger.info(logtxt)
return engine
def key_check(key, settings):
"""Check for the presence of a key in settings."""
# todo These checks shall be done earlier, when cleaning the input.
if key not in settings['simulation']:
msgtxt = 'Simulation setting "{}" is missing!'.format(key)
logger.critical(msgtxt)
raise ValueError(msgtxt)
[docs]def create_simulation(settings):
"""Create simulation(s) from given settings.
This function will set up some common simulation types.
It is meant as a helper function to automate some very common set-up
task. It will here check what kind of simulation we are to perform
and then call the appropriate function for setting that type of
simulation up.
Parameters
----------
settings : dict
This dictionary contains the settings for the simulation.
Returns
-------
simulation : object like :py:class:`.Simulation`
This object will correspond to the selected simulation type.
"""
sim_type = settings['simulation']['task'].lower()
sim_map = {
'md': create_md_simulation,
'md-nve': create_nve_simulation,
'md-flux': create_mdflux_simulation,
'umbrellawindow': create_umbrellaw_simulation,
'make-tis-files': create_tis_simulation,
'tis': create_tis_simulation,
'explore': create_tis_simulation,
'retis': create_retis_simulation,
'pptis': create_tis_simulation,
'repptis': create_retis_simulation,
}
# Improve setting quality
add_default_settings(settings)
add_specific_default_settings(settings)
if settings['simulation'].get('restart', False):
settings, info_restart = settings_from_restart(settings)
if sim_type not in sim_map: # TODO put in check_sim_type
msgtxt = 'Unknown simulation task {}'.format(sim_type)
logger.error(msgtxt)
raise ValueError(msgtxt)
simulation = sim_map[sim_type](settings)
msgtxt = '{}'.format(simulation)
logger.info('Created simulation:\n%s', msgtxt)
if settings['simulation'].get('restart', False):
simulation.load_restart_info(info_restart)
return simulation