Source code for pimms.keyfile_parser

## ...........................................................................
## 
## PIMMS (Polymer Interactions in Multicomponent Mixtures)
## Alex Holehouse, Pappu Lab, Holehouse Lab
## Copyright 2015 - 2026
## ...........................................................................

##
## keyfile_parser
##
## The KeyFileParser object includes all the functionality necessary for parsing keyfiles
## for running PIMMS simulation
##
##
### Derived keywords
##
## EQUILIBRIUM_TEMPERATURE : Is set to the temperature that the simulation treats as the final equilibrium temperature.


import random
import sys
import os.path
import numpy as np

from . import IO_utils
from . import latticeExceptions
from .latticeExceptions import KeyFileException, RestartException
from . import file_utilities
from . import restart
from . import pimmslogger
from . import CONFIG
from . data_structures import FreezeFile






# ===================================================================================================
#
#
[docs] class KeyFileParser: """ KeyFileParser is essentially where all the logic that deals with input information is defined. Specifically, a KeyFileParser object can read a keyfile and extract any/all pertinant information there in. It then sets any default values which can be estimated but weren't defined explicitly. Finally, it sanity checks all the input to ensure we're doing something sensible. """ #----------------------------------------------------------------- # def __init__(self, filename, parse_only=False): """ Function which initializes the keyfile parser object by defining the expected keywords and required keywords. These words serve different purposes: EXPECTED_KEYWORDS are words which we define default values for, and are expected to be included in the keyfile. REQUIRED_KEYWORDS are words which MUST be defined in the keyfile - i.e. we cannot make a reasonable guess without essentially making a design decision about the simulation being run The REQUIRED_KEYWORDS are a subset of the EXPECTED_KEYWORDS. ONLY EXPECTED_KEYWORDS are correctly parsed - i.e. if an unexpected keyword is identified an exception is raised. This is a slightly over the top behaviour, but is deliberate to avoid the scenario where you mis-type a keyword, don't realize, and the system over-writes with a default without you knowing. Parameters ----------------- filename : str The only argument required for this object is the location of a keyfile which is to be parsed by the KeyFileParser object. parse_only : bool Optional keyword which - if set to true - means the keyfile is read in but nothing moreis done (i.e. no defaults set, no santization performed etc. This is useful if keyfile_parsers() is used in reading in pre-run keyfiles where rigerous assessment is not needed """ # expected keywords contains a list of possible keywords which can be read form the keyfile. Importantly if these # keywords are *not* included then they are set to their default values self.expected_keywords = CONFIG.EXPECTED_KEYWORDS # required keywords are those which MUST be included in the keyfile - i.e. PIMMS can't set default values for these # keywords. Note that required_keywords is a subset of expected_keywords self.required_keywords = CONFIG.REQUIRED_KEYWORDS # list of keywords that can support multiple entries in a keyfile self.keywords_with_multiple_entries = ['CHAIN', 'EXTRA_CHAIN', 'ANA_RESIDUE_PAIRS'] self.keyword_lookup = {} # every keyword encountered during parsing is recorded here (independent # of whether its handler ends up writing to keyword_lookup) so that # duplicate keywords are ALWAYS detected - see parse(). self._seen_keywords = set() self.DEFAULTS = {} ## IF PARSE ONLY mode if parse_only: self.parse(filename) return IO_utils.horizontal_line(hzlen=40, linechar='*') IO_utils.status_message("Parsing keyfile [%s]" %(filename),'startup') IO_utils.status_message("Default values set are explicitly announced below:", 'startup') print("") self.parse(filename) # parse the keyfile (i.e. read in and deal with the file) self.assign_default() # assigns default values to the internal system (though not YET to this keyfile) self.set_defaults() # finally any values missing from the keyfile get set to the default values self.set_dynamic_defaults() # AND FINALLY update any values that depend on keyfile-derived parameters self.run_sanity_checks() # run some sanity checks self.add_derived_keywords() # some keywords are not explicitly included in the keyfile but are derived from # the keyfile words. These are set here IO_utils.horizontal_line(hzlen=40, linechar='*') # initialize logging... pimmslogger.initialize() #----------------------------------------------------------------- # def __repr__(self): """ Return the developer/`repr` representation of the parser. This simply defers to :meth:`__str__`, so the representation is the same human-readable summary of all parsed keyword/value pairs. Returns ------- str A formatted string listing each keyword and its current value. """ return str(self) #----------------------------------------------------------------- # def __str__(self): """ Return a human-readable summary of all parsed keyword/value pairs. Builds a multi-line string in which each line shows one entry from the internal ``keyword_lookup`` dictionary as ``keyword => value``. Returns ------- str A formatted, multi-line string describing the parsed keyfile state. """ msg = '\n............................\n' msg = msg + 'PIMMS Keyfile object:\n' msg = msg + '............................\n' for i in self.keyword_lookup: msg = msg + '%s => %s\n' %(i, str(self.keyword_lookup[i])) msg = msg + '............................\n' return msg #----------------------------------------------------------------- # def __check_experimental_features(self, kw): """ Should ONLY be used after the full keyfile has been parsed (i.e. during the sanity check section). Parameters -------------- kw : str Name of the keyword to be checked (i.e. a keyword where we required EXPERIMENTAL_FEATURE to be set to True for this parameter to be useable. Returns ---------- None No return type, but if EXPERIMENTAL_FEATURES is False this raises a KeyfileException with an appropriate error message """ if self.keyword_lookup['EXPERIMENTAL_FEATURES'] == False: raise KeyFileException(f'\n\nExperimental or non-supported keyword [{kw}] being proposed but EXPERIMENTAL_FEATURES is False.\n') #----------------------------------------------------------------- # # Typed-value helpers. These convert a raw keyfile string into the expected # type and raise a clear, user-facing KeyFileException on malformed input # (instead of an opaque ValueError traceback). Using them everywhere is part # of sanity-checking every keyword input at startup. # def _kw_int(self, keyword, value): """ Convert a raw keyfile string into an integer. Parameters ---------- keyword : str Name of the keyword being parsed (used only for error messaging). value : str The raw value associated with the keyword in the keyfile. Returns ------- int The value cast to an integer. Raises ------ KeyFileException If ``value`` cannot be interpreted as an integer. """ try: return int(value) except (ValueError, TypeError): raise KeyFileException(latticeExceptions.message_preprocess( "Keyword [%s] expects an integer value, but got [%s]" % (keyword, value))) def _kw_float(self, keyword, value): """ Convert a raw keyfile string into a floating-point number. Parameters ---------- keyword : str Name of the keyword being parsed (used only for error messaging). value : str The raw value associated with the keyword in the keyfile. Returns ------- float The value cast to a float. Raises ------ KeyFileException If ``value`` cannot be interpreted as a numeric value. """ try: return float(value) except (ValueError, TypeError): raise KeyFileException(latticeExceptions.message_preprocess( "Keyword [%s] expects a numeric value, but got [%s]" % (keyword, value))) def _kw_bool(self, keyword, value): """ Convert a raw keyfile string into a boolean. The comparison is case-insensitive and whitespace-insensitive: the strings ``TRUE`` and ``FALSE`` (in any case) map to ``True`` and ``False`` respectively. Parameters ---------- keyword : str Name of the keyword being parsed (used only for error messaging). value : str The raw value associated with the keyword in the keyfile. Returns ------- bool ``True`` if ``value`` is ``TRUE``, ``False`` if it is ``FALSE``. Raises ------ KeyFileException If ``value`` is neither ``TRUE`` nor ``FALSE``. """ v = str(value).strip().upper() if v == 'TRUE': return True if v == 'FALSE': return False raise KeyFileException(latticeExceptions.message_preprocess( "Keyword [%s] expects TRUE or FALSE, but got [%s]" % (keyword, value))) def _kw_int_list(self, keyword, value): """ Convert a space-separated keyfile string into a list of integers. Parameters ---------- keyword : str Name of the keyword being parsed (used only for error messaging). value : str The raw value associated with the keyword; expected to be a whitespace-separated sequence of integers (e.g. box dimensions). Returns ------- list of int The parsed integers, in the order they appear in ``value``. Raises ------ KeyFileException If any whitespace-separated token cannot be cast to an integer. """ try: return [int(i) for i in value.split()] except (ValueError, TypeError): raise KeyFileException(latticeExceptions.message_preprocess( "Keyword [%s] expects a space-separated list of integers, but got [%s]" % (keyword, value))) #----------------------------------------------------------------- #
[docs] def parse(self, filename, verbose=True): """ Main function reads in a keyfile and extracts out the relevant details based on the keywords. Keywoerds are assigned to the self.keywords_lookup Keywords must be defined as KEYWORD : VALUE # comment This function reads through all the lines in the keyfile and in a keyword-specific manner parses each keyword and assigns it to the self.keyword_lookup dictionary. Any keywords that are expected but NOT defined in the keyfile are then assigned as default values later. Parameters -------------- filename : str Name of the keyfile to be read verbose : bool (default = True) Flag which determines the level of warning messages to print. Returns --------- None No return type but assigns values to the self.keyword_lookup dictionary, which itself is a key-value pair fo simulations keywords """ # parse line-by-line to avoid loading large keyfiles into memory. with open(filename, 'r') as fh: for line in fh: # if it's a comment line skip the whole line if file_utilities.is_comment_line(line): continue # remove comment section (at end of line) un_comment = file_utilities.remove_comments(line) # split once so values can legitimately contain ':' (e.g. paths). splitline = un_comment.split(':', 1) # if we didn't find a keyword separator if len(splitline) < 2: raise KeyFileException(latticeExceptions.message_preprocess('On line [%s] - no keyword separator found...' % line)) # if get here must have keyword/keyvalue putative_keyword = splitline[0].strip().upper() putative_value = splitline[1].strip() if putative_keyword in self.expected_keywords: ## ** CHECK TO ENSURE WE DON'T OVERWRITE KEYWORDS ** # check if we've seen this keyword before - if we're trying to overwrite raise an exception. # NOTE: we track this in self._seen_keywords (NOT keyword_lookup) so that a duplicate is # always caught even for keywords whose handler may not write to keyword_lookup. if putative_keyword in self._seen_keywords: if putative_keyword in self.keywords_with_multiple_entries: # this is OK - we can have multiple chains! pass else: raise KeyFileException(latticeExceptions.message_preprocess('Found a second occurence of the [%s] keyword. Please correct your keyfile and retry' % putative_keyword)) self._seen_keywords.add(putative_keyword) ## >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # CHAIN KEYWORD if putative_keyword == "CHAIN": chainSplit = putative_value.split() if len(chainSplit) != 2: raise KeyFileException(latticeExceptions.message_preprocess(f'Invalid CHAIN keyword format [{putative_value}]. Expected: CHAIN : <count> <sequence>')) try: number_of_chains = int(chainSplit[0]) except ValueError: raise KeyFileException(latticeExceptions.message_preprocess(f'Invalid CHAIN keyword format [{putative_value}]. Expected integer chain count')) chain_sequence = chainSplit[1].strip() if putative_keyword in self.keyword_lookup: self.keyword_lookup['CHAIN'].append([number_of_chains, chain_sequence]) else: self.keyword_lookup['CHAIN'] = [[number_of_chains, chain_sequence]] ## >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> # EXTRA_CHAIN KEYWORD elif putative_keyword == "EXTRA_CHAIN": chainSplit = putative_value.split() if len(chainSplit) != 2: raise KeyFileException(latticeExceptions.message_preprocess(f'Invalid EXTRA_CHAIN keyword format [{putative_value}]. Expected: EXTRA_CHAIN : <count> <sequence>')) try: number_of_chains = int(chainSplit[0]) except ValueError: raise KeyFileException(latticeExceptions.message_preprocess(f'Invalid EXTRA_CHAIN keyword format [{putative_value}]. Expected integer chain count')) chain_sequence = chainSplit[1].strip() if putative_keyword in self.keyword_lookup: self.keyword_lookup['EXTRA_CHAIN'].append([number_of_chains, chain_sequence]) else: self.keyword_lookup['EXTRA_CHAIN'] = [[number_of_chains, chain_sequence]] # DIMENSIONS KEYWORD (define if simulation is 2D or 3D) elif putative_keyword == 'DIMENSIONS': self.keyword_lookup['DIMENSIONS'] = self._kw_int_list(putative_keyword, putative_value) if not len(self.keyword_lookup['DIMENSIONS']) == 2 and not len(self.keyword_lookup['DIMENSIONS']) == 3: raise KeyFileException(latticeExceptions.message_preprocess('Unexpected number of dimensions [%s] ' % line)) # conversion factor for PDB file writing elif putative_keyword == 'LATTICE_TO_ANGSTROMS': self.keyword_lookup['LATTICE_TO_ANGSTROMS'] = self._kw_float(putative_keyword, putative_value) # Dimensions of compressed equilibration box elif putative_keyword == 'RESIZED_EQUILIBRATION': self.keyword_lookup['RESIZED_EQUILIBRATION'] = self._kw_int_list(putative_keyword, putative_value) # Dimensions of manual offset elif putative_keyword == 'EQUILIBRATION_OFFSET': self.keyword_lookup['EQUILIBRATION_OFFSET'] = self._kw_int_list(putative_keyword, putative_value) # CASE_INSENSITIVE_CHAINS elif putative_keyword == "CASE_INSENSITIVE_CHAINS": self.keyword_lookup['CASE_INSENSITIVE_CHAINS'] = self._kw_bool(putative_keyword, putative_value) # HARDWALL elif putative_keyword == "HARDWALL": self.keyword_lookup['HARDWALL'] = self._kw_bool(putative_keyword, putative_value) # AUTOCENTER elif putative_keyword == "AUTOCENTER": self.keyword_lookup['AUTOCENTER'] = self._kw_bool(putative_keyword, putative_value) # EXPERIMENTAL_FEATURES elif putative_keyword == "EXPERIMENTAL_FEATURES": self.keyword_lookup['EXPERIMENTAL_FEATURES'] = self._kw_bool(putative_keyword, putative_value) # TEMPERATURE elif putative_keyword == "TEMPERATURE": self.keyword_lookup['TEMPERATURE'] = self._kw_float(putative_keyword, putative_value) # N_STEPS elif putative_keyword == "N_STEPS": self.keyword_lookup['N_STEPS'] = self._kw_int(putative_keyword, putative_value) # equilibration elif putative_keyword == "EQUILIBRATION": self.keyword_lookup['EQUILIBRATION'] = self._kw_int(putative_keyword, putative_value) # energy parameter elif putative_keyword == "PARAMETER_FILE": self.keyword_lookup['PARAMETER_FILE'] = str(putative_value) # PRINT_FREQUENCY elif putative_keyword == "PRINT_FREQ": self.keyword_lookup['PRINT_FREQ'] = self._kw_int(putative_keyword, putative_value) # REDUCED_PRINTING elif putative_keyword == "REDUCED_PRINTING": self.keyword_lookup['REDUCED_PRINTING'] = self._kw_bool(putative_keyword, putative_value) # XTC_FREQUENCY elif putative_keyword == "XTC_FREQ": self.keyword_lookup['XTC_FREQ'] = self._kw_int(putative_keyword, putative_value) # EN_FREQUENCY elif putative_keyword == "EN_FREQ": self.keyword_lookup['EN_FREQ'] = self._kw_int(putative_keyword, putative_value) # SEED elif putative_keyword == "SEED": self.keyword_lookup['SEED'] = self._kw_int(putative_keyword, putative_value) # ENERGY_CHECK elif putative_keyword == "ENERGY_CHECK": self.keyword_lookup['ENERGY_CHECK'] = self._kw_int(putative_keyword, putative_value) # RESTART_FREQ elif putative_keyword == "RESTART_FREQ": self.keyword_lookup['RESTART_FREQ'] = self._kw_int(putative_keyword, putative_value) # RESTART FILE elif putative_keyword == "RESTART_FILE": self.keyword_lookup['RESTART_FILE'] = str(putative_value) # RESTART OVERRIDE DIMENSIONS elif putative_keyword == "RESTART_OVERRIDE_DIMENSIONS": self.keyword_lookup["RESTART_OVERRIDE_DIMENSIONS"] = self._kw_bool(putative_keyword, putative_value) # RESTART OVERRIDE HARDWALL elif putative_keyword == "RESTART_OVERRIDE_HARDWALL": self.keyword_lookup["RESTART_OVERRIDE_HARDWALL"] = self._kw_bool(putative_keyword, putative_value) # FREEZE_FILE elif putative_keyword == 'FREEZE_FILE': self.keyword_lookup['FREEZE_FILE'] = str(putative_value) ## >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ## QUENCHING keywords elif putative_keyword == "QUENCH_RUN": self.keyword_lookup['QUENCH_RUN'] = self._kw_bool(putative_keyword, putative_value) elif putative_keyword == "QUENCH_AS_EQUILIBRATION": self.keyword_lookup['QUENCH_AS_EQUILIBRATION'] = self._kw_bool(putative_keyword, putative_value) elif putative_keyword == "QUENCH_FREQ": self.keyword_lookup['QUENCH_FREQ'] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == "QUENCH_STEPSIZE": self.keyword_lookup['QUENCH_STEPSIZE'] = abs(self._kw_float(putative_keyword, putative_value)) elif putative_keyword == "QUENCH_START": self.keyword_lookup['QUENCH_START'] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == "QUENCH_END": self.keyword_lookup['QUENCH_END'] = self._kw_float(putative_keyword, putative_value) ## >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ## TSMMC keywords elif putative_keyword == 'TSMMC_JUMP_TEMP': self.keyword_lookup['TSMMC_JUMP_TEMP'] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == 'TSMMC_STEP_MULTIPLIER': self.keyword_lookup['TSMMC_STEP_MULTIPLIER'] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == 'TSMMC_NUMBER_OF_POINTS': self.keyword_lookup['TSMMC_NUMBER_OF_POINTS'] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == 'TSMMC_FIXED_OFFSET': self.keyword_lookup['TSMMC_FIXED_OFFSET'] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == 'TSMMC_INTERPOLATION_MODE': self.keyword_lookup['TSMMC_INTERPOLATION_MODE'] = str(putative_value).upper().strip() if self.keyword_lookup['TSMMC_INTERPOLATION_MODE'] not in ['LINEAR']: raise KeyFileException(latticeExceptions.message_preprocess('Tried to set TSMMC_INTERPOLATION_MODE with unexpected keyword [%s]' % (self.keyword_lookup['TSMMC_INTERPOLATION_MODE']))) ## >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ## CRANKSHAFT keywords elif putative_keyword == "CRANKSHAFT_SUBSTEPS": self.keyword_lookup['CRANKSHAFT_SUBSTEPS'] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == "SLITHER_SUBSTEPS": self.keyword_lookup['SLITHER_SUBSTEPS'] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == "PULL_SUBSTEPS": self.keyword_lookup['PULL_SUBSTEPS'] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == "VMMC_MAX_DISPLACEMENT": self.keyword_lookup['VMMC_MAX_DISPLACEMENT'] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == "VMMC_MAX_CLUSTER": self.keyword_lookup['VMMC_MAX_CLUSTER'] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == "CRANKSHAFT_MODE": # THIS IS HACKY BUT DON'T WANT PEOPLE/ME TO THINK THIS IS WORKING RN raise Exception('CRANKSHAFT_MODE is currently obselete in this version of the code') elif putative_keyword == "NON_INTERACTING": self.keyword_lookup['NON_INTERACTING'] = self._kw_bool(putative_keyword, putative_value) elif putative_keyword == "ANGLES_OFF": self.keyword_lookup['ANGLES_OFF'] = self._kw_bool(putative_keyword, putative_value) ## >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ## Analysis keywords elif putative_keyword == "ANALYSIS_FREQ": self.keyword_lookup['ANALYSIS_FREQ'] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == 'ANA_POL': self.keyword_lookup[putative_keyword] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == 'ANA_INTSCAL': self.keyword_lookup[putative_keyword] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == 'ANA_DISTMAP': self.keyword_lookup[putative_keyword] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == 'ANA_ACCEPTANCE': self.keyword_lookup[putative_keyword] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == 'ANA_INTER_RESIDUE': self.keyword_lookup[putative_keyword] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == 'ANA_CLUSTER': self.keyword_lookup[putative_keyword] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == 'ANA_CLUSTER_THRESHOLD': self.keyword_lookup[putative_keyword] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == 'ANA_CUSTOM': self.keyword_lookup[putative_keyword] = self._kw_int(putative_keyword, putative_value) elif putative_keyword == 'ANALYSIS_MODULE': self.keyword_lookup[putative_keyword] = str(putative_value) elif putative_keyword == 'ANA_RESIDUE_PAIRS': split_residues = putative_value.split() if len(split_residues) != 2: raise KeyFileException(latticeExceptions.message_preprocess(f'Invalid ANA_RESIDUE_PAIRS format [{putative_value}]. Expected: ANA_RESIDUE_PAIRS : <res1> <res2>')) try: res1 = int(split_residues[0]) res2 = int(split_residues[1]) except ValueError: raise KeyFileException(latticeExceptions.message_preprocess(f'Invalid ANA_RESIDUE_PAIRS format [{putative_value}]. Residue indices must be integers')) if res1 > res2: tmp = res1 res1 = res2 res2 = tmp if putative_keyword in self.keyword_lookup: self.keyword_lookup['ANA_RESIDUE_PAIRS'].append([res1, res2]) else: self.keyword_lookup['ANA_RESIDUE_PAIRS'] = [[res1, res2]] elif putative_keyword == 'WRITE_CHAIN_TO_CHAINID': self.keyword_lookup[putative_keyword] = self._kw_bool(putative_keyword, putative_value) ## >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ## MOVESET keywords elif putative_keyword[0:4] == 'MOVE': if putative_keyword == 'MOVE_CRANKSHAFT': self.keyword_lookup[putative_keyword] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == 'MOVE_CHAIN_TRANSLATE': self.keyword_lookup[putative_keyword] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == 'MOVE_CHAIN_ROTATE': self.keyword_lookup[putative_keyword] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == 'MOVE_CHAIN_PIVOT': self.keyword_lookup[putative_keyword] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == 'MOVE_HEAD_PIVOT': self.keyword_lookup[putative_keyword] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == 'MOVE_SLITHER': self.keyword_lookup[putative_keyword] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == 'MOVE_CLUSTER_TRANSLATE': self.keyword_lookup[putative_keyword] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == 'MOVE_CLUSTER_ROTATE': self.keyword_lookup[putative_keyword] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == 'MOVE_CTSMMC': self.keyword_lookup[putative_keyword] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == 'MOVE_MULTICHAIN_TSMMC': self.keyword_lookup[putative_keyword] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == 'MOVE_PULL': self.keyword_lookup[putative_keyword] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == 'MOVE_VMMC': self.keyword_lookup[putative_keyword] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == 'MOVE_SYSTEM_TSMMC': self.keyword_lookup[putative_keyword] = self._kw_float(putative_keyword, putative_value) elif putative_keyword == 'MOVE_JUMP_AND_RELAX': self.keyword_lookup[putative_keyword] = self._kw_float(putative_keyword, putative_value) ## >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ## SAVING keywords elif putative_keyword == 'SAVE_AT_END': self.keyword_lookup['SAVE_AT_END'] = self._kw_bool(putative_keyword, putative_value) elif putative_keyword == 'SAVE_EQ': self.keyword_lookup['SAVE_EQ'] = self._kw_bool(putative_keyword, putative_value) elif putative_keyword == 'TRAJECTORY_PBC_UNWRAP': self.keyword_lookup['TRAJECTORY_PBC_UNWRAP'] = self._kw_bool(putative_keyword, putative_value) # PARALLELIZE - run the crankshaft move on the parallel kernel elif putative_keyword == 'PARALLELIZE': self.keyword_lookup['PARALLELIZE'] = self._kw_bool(putative_keyword, putative_value) # PARALLEL_THREADS - number of OpenMP threads (0 => auto) elif putative_keyword == 'PARALLEL_THREADS': self.keyword_lookup['PARALLEL_THREADS'] = self._kw_int(putative_keyword, putative_value) else: raise KeyFileException(latticeExceptions.message_preprocess('Fail to deal with a supported keyword - [%s] - this is a bug! ' % putative_keyword)) else: raise KeyFileException(latticeExceptions.message_preprocess('Found an unsupported keyword - [%s]. Valid supported keywords are\n%s ' % (putative_keyword, self.expected_keywords)))
[docs] def update_keyfile(self, update_dictionary): """ Function that takes an update dictionary of key : value pairs and updates the current key:value pairs with these new keywords. Parameters ----------------------- update_dictionary : dictionary key-value where keys are keywords and values are the values to change to. Any key value pair will be added Returns ----------------------- No return value, but updates the self.keyword_lookup dictionary """ self.keyword_lookup.update(update_dictionary)
[docs] def write_keyfile(self, output_filename, PADDING=10): """ Write the key-value pairs from the ``keyword_lookup`` dictionary to a file. Each keyword and its value are written on a single line in ``KEY : value`` format, with padding inserted between the key and the value for readability. Parameters ---------- output_filename : str The name (path) of the file to write the key-value pairs to. PADDING : int, optional The minimum number of spaces to use for padding between the key and value. Default is 10. Returns ------- None No return value; the keyword/value pairs are written to disk. Examples -------- >>> parser = KeyFileParser('input.kf') >>> parser.write_keyfile('output.txt') """ with open(output_filename, 'w') as fh: for key, value in self.keyword_lookup.items(): padding = ' ' * max((PADDING - len(key), 1)) fh.write(f' {key} : {padding} {value} \n')
#----------------------------------------------------------------- #
[docs] def run_sanity_checks(self): """ Function which, once the keyword has been parsed, will run an arbitrary number of sanity checks to ensure things make sense. Adding new checks is simply a case of adding a new 'CHECK' section in the code. This also sets a number of 'internal' keywords - keywords which are derived from the manually defined keywords but are NOT explicitly read in. This lets the system set certain configurations by deducing things from the manual keyfile that aid in decision making. To keep things clear a list of the 'internal' keywords are below. Internal keywords always begin with a '__' __TSMMC_USED # BOOLEAN --> set to true or false depending on if any TSMMC are being used. Specific details of each sanity check are provided as code blocks below. Before adding additional checks please read through these! Note also that restart file sanity checks are done in their own function which gets called from in here Returns ------- None No return value, but ``self.keyword_lookup`` is updated in place (e.g. derived ``__`` keywords are set, chains may be upper-cased, restart/freeze files are loaded, and quench parameters normalised). Raises ------ KeyFileException If any sanity check fails (e.g. out-of-range numerical values, move fractions that do not sum to 1.0, missing/invalid parameter or restart files, or incompatible box/hardwall/experimental settings). """ ## ---------------------------------------------------------------------------------------- ## First we case chain and extra chain keywords, and if yes cast all chains to upper case # (both CHAIN and EXTRA_CHAIN) # if self.keyword_lookup['CASE_INSENSITIVE_CHAINS']: new_chains = [] for entry in self.keyword_lookup['CHAIN']: new_chains.append([entry[0], entry[1].upper()]) self.keyword_lookup['CHAIN'] = new_chains new_chains = [] for entry in self.keyword_lookup['EXTRA_CHAIN']: new_chains.append([entry[0], entry[1].upper()]) self.keyword_lookup['EXTRA_CHAIN'] = new_chains ## ------------------------------------------------------------------ ## check values we think must be bigger than 0 (or can be unset) # for c in ['TEMPERATURE', 'N_STEPS', 'PRINT_FREQ', 'XTC_FREQ', 'EN_FREQ', 'SEED', 'ENERGY_CHECK', 'RESTART_FREQ', 'QUENCH_STEPSIZE', 'QUENCH_START', 'QUENCH_END', 'TSMMC_STEP_MULTIPLIER', 'TSMMC_NUMBER_OF_POINTS', 'CRANKSHAFT_SUBSTEPS', 'SLITHER_SUBSTEPS', 'PULL_SUBSTEPS', 'VMMC_MAX_DISPLACEMENT', 'VMMC_MAX_CLUSTER', 'ANALYSIS_FREQ', 'ANA_POL', 'ANA_DISTMAP', 'ANA_ACCEPTANCE', 'ANA_INTER_RESIDUE', 'ANA_CLUSTER', 'ANA_CUSTOM']: try: # we have to check this unset because several keywords default to 'UNSET' which triggers an error if we try and compare to an int if self.keyword_lookup[c] != 'UNSET': if self.keyword_lookup[c] <= 0: raise KeyFileException(latticeExceptions.message_preprocess(f'Numerical error when parsing keyfile. Expected {c} to be larger than 0')) except TypeError: # the comparison failed because the value is a non-numeric type raise KeyFileException(latticeExceptions.message_preprocess( f'Keyword {c} could not be checked to be greater than 0 - its value [{self.keyword_lookup[c]}] is not numeric')) ## ------------------------------------------------------------------ ## check values with think must be bigger than or equal to zero # for c in ['ANA_CLUSTER_THRESHOLD', 'EQUILIBRATION', 'PARALLEL_THREADS']: try: if self.keyword_lookup[c] < 0: raise KeyFileException(latticeExceptions.message_preprocess(f'Numerical error when parsing keyfile. Expected {c} to be larger than 0')) except TypeError: # the comparison failed because the value is a non-numeric type raise KeyFileException(latticeExceptions.message_preprocess( f'Keyword {c} could not be checked to be >= 0 - its value [{self.keyword_lookup[c]}] is not numeric')) ## ------------------------------------------------------------------ ## CHAIN CHECKS # if we have more than 26 unique chains print a warning about this tmp = [] for c in self.keyword_lookup['CHAIN']: tmp.append(c[1]) if len(set(tmp)) > 26: print(f"[ WARNING ] : Found {len(set(tmp))} unique chains (more than 26). This means the chain IDs for chains after 'Z' will all be set to 'Z'") ## --------------------------------------------------------- ## MOVESET CHECK # check moveset frequencies add up to 1 running_total = 0.0 message = '' # message defines a summary of all moves for easy debugging for i in self.expected_keywords: # for each MOVE keyword [note this dynamically finds them so if new MOVE_ keywords are added this will just work :-) ] if i[0:4] == "MOVE": # an individual move fraction must be a sensible probability (>= 0); # a negative fraction is meaningless even if the total still sums to 1 if self.keyword_lookup[i] < 0: raise KeyFileException(latticeExceptions.message_preprocess( 'Move fraction %s cannot be negative (got %s)' % (i, self.keyword_lookup[i]))) running_total = running_total+self.keyword_lookup[i] message = message+'%s : %1.8f\n' % (i, self.keyword_lookup[i]) if abs(running_total - 1.0) > 0.0000001 : raise KeyFileException(latticeExceptions.message_preprocess('Moveset keywords do not add up to 1.0 (instead = %2.5e) - see below for specific details:\n%s' % (running_total, message))) ## --------------------------------------------------------- ## --------------------------------------------------------- ## QUENCH-RUN CHECKS # IF we're running a quench all the quench keywords must be included if self.keyword_lookup['QUENCH_RUN']: # first check all the keywords are there for i in ['QUENCH_FREQ', 'QUENCH_STEPSIZE', 'QUENCH_START', 'QUENCH_END', 'QUENCH_AS_EQUILIBRATION']: if 'UNSET' == self.keyword_lookup[i]: raise KeyFileException('Trying to run a simulation with a quench but no [%s] defined in the keyfile' % i) # the stepsize but be greater than zero! if not self.keyword_lookup['QUENCH_STEPSIZE'] > 0: raise KeyFileException('Trying to use a stepsize of zero for quenching simulation...') # check that the temperature distance being traversed isn't smaller than the temperature # step size... dT = abs(self.keyword_lookup['QUENCH_START'] - self.keyword_lookup['QUENCH_END']) if dT < self.keyword_lookup['QUENCH_STEPSIZE']: raise KeyFileException('A single quench step overshoots the desired temperature: suggests an in the logfile') steps_for_quench = (1+(int(dT) / float(self.keyword_lookup['QUENCH_STEPSIZE']))) * self.keyword_lookup['QUENCH_FREQ'] if steps_for_quench >= self.keyword_lookup['N_STEPS']: raise KeyFileException('This quench will not complete as the quench period [%i] is longer than the number of steps in the simulation [%i]' %(steps_for_quench, self.keyword_lookup['N_STEPS'] )) # If we're doing a heating simulation (so start is lower than end) then stepsize must become negative # as the temperature update operation is CURRENT - STEPSIZE if self.keyword_lookup['QUENCH_START'] < self.keyword_lookup['QUENCH_END']: self.keyword_lookup['QUENCH_STEPSIZE'] = -(self.keyword_lookup['QUENCH_STEPSIZE']) # Update the equilibration period so the quench process is enveloped by the quench (i.e. all # output is only at the target temperature) if self.keyword_lookup['QUENCH_AS_EQUILIBRATION'] : # EQUILIBRATION must be an integer number of steps (it is used in # integer/range contexts downstream); round up so the equilibration # period fully envelopes the quench. equilibration_steps = int(np.ceil(steps_for_quench)) print("UPDATING EQUILIBRATION TO [%i] (temperature quench period is equilibration)" % equilibration_steps) self.keyword_lookup['EQUILIBRATION'] = equilibration_steps if not self.keyword_lookup['TEMPERATURE'] == self.keyword_lookup['QUENCH_START']: print("[ WARNING ] : Resetting the starting temperature to the QUENCH_START value [%i]"%self.keyword_lookup['QUENCH_START']) self.keyword_lookup['TEMPERATURE'] = self.keyword_lookup['QUENCH_START'] ## --------------------------------------------------------- ## TSMMC check (sets the __TSMMC_ON keyword if self.keyword_lookup['MOVE_CTSMMC'] > 0 or self.keyword_lookup['MOVE_MULTICHAIN_TSMMC'] > 0 or self.keyword_lookup['MOVE_SYSTEM_TSMMC'] > 0: # self.keyword_lookup['__TSMMC_USED'] = True # check if we didn't use a fixed OFFSET that the jumps make sense... if (self.keyword_lookup['TSMMC_JUMP_TEMP'] <= self.keyword_lookup['TEMPERATURE']) and self.keyword_lookup['TSMMC_FIXED_OFFSET'] is False: raise KeyFileException(latticeExceptions.message_preprocess('\n\nThe TSMMC jump temperature [%3.2f] is less than or equal to the actual simulation temperature [%3.2f], which will mean the TSMMC moves will at best hurt performance and at worst reduce sampling. Please correct your keyfile appropriately' % (self.keyword_lookup['TSMMC_JUMP_TEMP'], self.keyword_lookup['TEMPERATURE']))) else: self.keyword_lookup['__TSMMC_USED'] = False ## ------------------------------------------------------------ ## residue distance pairs check - make sure non of the pair-pair ## analysis distances fall outside of the chain length for pair in self.keyword_lookup['ANA_RESIDUE_PAIRS']: for chain in self.keyword_lookup['CHAIN']: if pair[1] >= len(chain[1]): raise KeyFileException('Residue-residue distance analysis pair (%i) is outside the chain length (%i)' % (pair[1], len(chain[1]))) ## ------------------------------------------------------------ ## Non-cubic / non-square boxes are fully supported (2D and 3D, hardwall OR ## periodic): the engine wraps and evaluates every axis independently ## (per-axis minimum image / grid.shape[i]), verified move-by-move against a ## from-scratch energy recompute (ENERGY_CHECK). The ONE restriction is ## cluster ROTATION under periodic boundaries. ## ## A cluster move rotates a whole (isolated) cluster rigidly and is applied as ## an ENERGY-NEUTRAL move (no energy recomputation). A cardinal 90/270 degree ## rotation swaps two axes; under periodic boundaries on a box whose axes have ## different periods, that swap changes the minimum-image distance of any ## intra-cluster pair separated by more than half a period on a swapped axis, ## so the cluster's internal energy actually changes while the move assumes it ## did not - silently corrupting the tracked energy. It is fine on a ## cube/square (the rotation is an exact box symmetry) and fine under HARDWALL ## (no periodic wrapping) - both verified via ENERGY_CHECK. ## ## The check covers EVERY box the run uses under PBC: the production box ## (DIMENSIONS) AND, when a resized equilibration is requested, the smaller ## equilibration box (RESIZED_EQUILIBRATION) - cluster rotation runs in both ## phases, so a non-cubic box in either phase is caught. dims = self.keyword_lookup['DIMENSIONS'] resized = self.keyword_lookup['RESIZED_EQUILIBRATION'] # False, or a list of ints if (not self.keyword_lookup['HARDWALL']) and self.keyword_lookup['MOVE_CLUSTER_ROTATE'] > 0: offending_box = None if len(set(dims)) != 1: offending_box = ('production (DIMENSIONS)', dims) elif resized and len(set(resized)) != 1: offending_box = ('resized-equilibration (RESIZED_EQUILIBRATION)', resized) if offending_box is not None: which, box = offending_box raise KeyFileException( f'MOVE_CLUSTER_ROTATE cannot be used with a non-cubic/non-square {which} box ' f'= {box} under periodic boundaries (HARDWALL : False). A cluster rotation is a rigid ' '90/180/270 degree rotation applied as an energy-neutral move; a 90/270 degree rotation ' 'swaps two axes, and under periodic boundaries on unequal axes that swap changes ' 'intra-cluster minimum-image distances, so the rotation is no longer energy-preserving and ' 'the tracked energy would drift from the true energy. Cluster rotation runs during BOTH ' 'equilibration and production, so every box the run uses (including any ' 'RESIZED_EQUILIBRATION box) must be cubic/square when HARDWALL is off. To use cluster ' 'rotation, either (a) make all boxes cubic/square, (b) turn HARDWALL on (a rotation is a ' 'valid isometry when there is no periodic wrapping), or (c) set MOVE_CLUSTER_ROTATE : 0.') ## Crash if dimensions are < 7 in ## dims = self.keyword_lookup['DIMENSIONS'] if len(dims) == 2: if dims[0] < 7 or dims[1] < 7: raise KeyFileException('Box size is too small to correctly support super-long range interactions, must be > 7 ') else: if dims[0] < 7 or dims[1] < 7 or dims[2] < 7: raise KeyFileException('Box size is too small to correctly support super-long range interactions, must be > 7 ') ## Check out resized equilibrium variables and fix as needed ## if self.keyword_lookup['RESIZED_EQUILIBRATION']: if len(self.keyword_lookup['RESIZED_EQUILIBRATION']) != len(self.keyword_lookup['DIMENSIONS']): raise KeyFileException('Number of dimensions for compressed equilibration and final simulation are not the same') # resized_equilibration for (real, eq) in zip(self.keyword_lookup['DIMENSIONS'], self.keyword_lookup['RESIZED_EQUILIBRATION']): if eq > real: raise KeyFileException('Resized equilibration dimension is larger than final dimension - not yet supported (only smaller)') if self.keyword_lookup['EQUILIBRATION'] == 0: print("[WARNING]: using RESIZED_EQUILIBRATION without an equilibration period makes no sense. Deactivating RESIZED_EQUILIBRATION") self.keyword_lookup['RESIZED_EQUILIBRATION'] = False if self.keyword_lookup['EQUILIBRATION_OFFSET']: if not self.keyword_lookup['RESIZED_EQUILIBRATION']: raise KeyFileException('RESIZED_EQUILIBRATION MUST be turned on if EQUILIBRATION_OFFSET is specified') if len(self.keyword_lookup['RESIZED_EQUILIBRATION']) != len(self.keyword_lookup['EQUILIBRATION_OFFSET']): raise KeyFileException('Number of dimensions for compressed equilibration and offset are not the same') for (real, eq, offset) in zip(self.keyword_lookup['DIMENSIONS'], self.keyword_lookup['RESIZED_EQUILIBRATION'], self.keyword_lookup['EQUILIBRATION_OFFSET']): if eq + offset > real: raise KeyFileException('Equilibration dimension + offset is larger than final dimension') ## ## if analysis code is provided check it can be loaded # first check if the file even exists if not os.path.isfile(self.keyword_lookup['PARAMETER_FILE']): raise KeyFileException('Unable to find parameter file at location [%s]. Please verify the file exists.' %(self.keyword_lookup['PARAMETER_FILE'])) # check if we can load as a python module... ## ## if custom analysis code is provided, load and validate it now (fail fast ## at parse time rather than part-way through a run). The loader raises a ## clear KeyFileException on any problem and, on success, returns the ## validated analysis_function callable - which we store in place of the ## path so the rest of PIMMS holds a ready-to-call function. if self.keyword_lookup['ANALYSIS_MODULE']: original_path = self.keyword_lookup['ANALYSIS_MODULE'] self.keyword_lookup['ANALYSIS_MODULE'] = file_utilities.custom_analysis_module_import(original_path) print("Loaded custom analysis code from [%s]" % original_path) ## ## Check restart file and then read in if self.keyword_lookup['RESTART_FILE']: ## ---------------------------------------------------------------------------------------------------- ## This block of code here is where 100% of the restart file sanity checking is going to happen # first see if we can even find the restart file... if not os.path.isfile(self.keyword_lookup['RESTART_FILE']): raise KeyFileException('Unable to find restart file. Passed filename is: %s. If this is a relative path Please verify the file exists.' %(self.keyword_lookup['RESTART_FILE'])) # if yes initialize and then construct restart_obj = restart.RestartObject() restart_obj.build_from_file(self.keyword_lookup['RESTART_FILE']) print("Loaded restart information from: %s" % (self.keyword_lookup['RESTART_FILE'])) self.keyword_lookup['RESTART_FILE'] = restart_obj # finally we ask is if any EXTRA_CHAIN were provided, and if yes add these to the newly generated # RestartObject. Note that the RestartObject sanity checks the new chains as they are added, so if # there is an issue with the new chains this will be caught and raised as a RestartException # which we catch and re-raise as a KeyFileException to keep things consistent for the user. if len(self.keyword_lookup['EXTRA_CHAIN']) > 0: try: # recall the self.keyword_lookup['EXTRA_CHAIN'] is a list where each element has two # elements, [0]= number of chains [1] = chain sequence for extra_chain in self.keyword_lookup['EXTRA_CHAIN']: self.keyword_lookup['RESTART_FILE'].add_extra_chains(extra_chain) except RestartException as e: raise KeyFileException(f'\n\nError when parsing EXTRA_CHAIN line. Full error below: {e}') # finally using the restart file sanity check input WRT the current keyfile to make sure everything # seems OK... self.sanity_check_and_update_with_restart_file() ## ---------------------------------------------------------------------------------------------------- ## else if we DID not pass a restart file, check we didn't have else: if len(self.keyword_lookup['EXTRA_CHAIN']) > 0: raise KeyFileException('\n\nEXTRA_CHAIN keyword defined but no restart file was provided. This is not supported. Please provide a restart file or remove the EXTRA_CHAIN line.') ## ## Check for and parse the freeze file if self.keyword_lookup['FREEZE_FILE']: # note all sanity checking is done in the FreezeFile class self.keyword_lookup['FREEZE_FILE'] = FreezeFile(self.keyword_lookup['FREEZE_FILE']) ## --------------------------------------------------------- # Check for missuse of experimental features... for kw in CONFIG.EXPERIMENTAL_KEYWORDS: # if a keyword included in the EXPERIMENTAL_KEYWORDS list is NOT set to the default value if self.keyword_lookup[kw] != self.DEFAULTS[kw]: self.__check_experimental_features(kw) ## ask if experimental features was set` #if self.keyword_lookup['EXPERIMENTAL_FEATURES'] == False: # raise KeyFileException('\n\nExperimental or non-supported feature ({kw}) being proposed but EXPERIMENTAL_FEATURES is not set (or set to False).\n') ## --------------------------------------------------------- # check simulation is not longer than eq period if self.keyword_lookup['EQUILIBRATION'] >= self.keyword_lookup['N_STEPS']: raise KeyFileException(f"Simulation will equilibrate for longer than the total number of steps; equilibration steps = {self.keyword_lookup['EQUILIBRATION']} while total steps = {self.keyword_lookup['N_STEPS']}")
#----------------------------------------------------------------- #
[docs] def set_defaults(self): """ Assign default values from ``self.DEFAULTS`` to any unset keywords. First the presence of every required keyword is validated (and at least one of ``CHAIN`` or ``RESTART_FILE`` must be present). Then every expected keyword that was not explicitly supplied in the keyfile is set to its default value from ``self.DEFAULTS``, announcing each default as it is applied. Returns ------- None No return value, but ``self.keyword_lookup`` is updated in place with default values for any keywords that were not explicitly defined. Raises ------ KeyFileException If a required keyword is missing, or if neither ``CHAIN`` nor ``RESTART_FILE`` was provided. """ # first check all required keywords were set for KW in self.required_keywords: if KW not in list(self.keyword_lookup.keys()): raise KeyFileException('ERROR: Keyfile does not define [%s] ' % KW) if ('CHAIN' not in list(self.keyword_lookup.keys())) and ('RESTART_FILE' not in list(self.keyword_lookup.keys())): raise KeyFileException('No CHAIN keyword nor RESTART_FILE provided - no information on system provided!') # now cycle through ALL the keywords in the expected keyword dictionary and # assign using the defaults if they haven't already been asigned for KW in self.expected_keywords: if KW not in list(self.keyword_lookup.keys()): # ONE edge case, we want to print the random seed being used at all costs if KW == "SEED": print("Using random seed [%s]" % (self.DEFAULTS[KW])) else: print("No %s set - using default [%s]" % (KW, self.DEFAULTS[KW])) self.keyword_lookup[KW] = self.DEFAULTS[KW]
#----------------------------------------------------------------- #
[docs] def set_dynamic_defaults(self): """ This function allows final default values to update such that 'defaults' can respond to passed parameters. Parameters ----------------------- No parameters, but updates the self.keyword_lookup dictionary with new default values as needed Returns ----------------------- No return value, but updates the self.keyword_lookup dictionary with new default values as needed """ # if no analysis module was passed set custom analysis to more steps than we're running if self.keyword_lookup['ANALYSIS_MODULE'] is False: self.keyword_lookup['ANA_CUSTOM'] = self.keyword_lookup['N_STEPS'] + 10 # if any of the analysis keywords have been set to 0 or -1 or anything less than 1 take this # to mean this analysis should not be performed, so set the frequency to a number that is larger than the # total number of steps for tmpkw in ['ANALYSIS_FREQ','ANA_POL', 'ANA_INTSCAL', 'ANA_DISTMAP', 'ANA_ACCEPTANCE', 'ANA_INTER_RESIDUE', 'ANA_CLUSTER', 'ANA_CUSTOM', 'ENERGY_CHECK']: if self.keyword_lookup[tmpkw] < 1: self.keyword_lookup[tmpkw] = self.keyword_lookup['N_STEPS'] + 10 if self.keyword_lookup['RESTART_FREQ'] == "Every 10th-percentile": self.keyword_lookup['RESTART_FREQ'] = int(self.keyword_lookup['N_STEPS'] / 10) # deliberate effective floor being used here..
#----------------------------------------------------------------- #
[docs] def print_summary(self): """ Print a full human-readable summary of the parsed simulation setup. Reports the system overview (step counts, temperatures, expected number of frames), box dimensions and resulting occupied volume fraction / solute concentration, quench settings, freeze-file settings, simulation components (chains), and output frequencies. A subset of these values are also written to the PIMMS logfile via :mod:`pimmslogger`. Returns ------- None No return value; the summary is printed to standard output (and key values are logged). """ def section(msg): """ Print a formatted section header for the summary output. Parameters ---------- msg : str The section title to display in the header. Returns ------- None No return value; the header is printed to standard output. """ IO_utils.newline() IO_utils.horizontal_line(hzlen=40, linechar='*') print("--> %s"%(msg)) IO_utils.newline() IO_utils.status_message("KEYFILE SUMMARY",'major') ## caclualte expected number of steps if self.keyword_lookup['SAVE_EQ']: expected_number_of_frames = int(np.floor(self.keyword_lookup['N_STEPS']/self.keyword_lookup['XTC_FREQ']))+1 else: expected_number_of_frames = int(np.floor((self.keyword_lookup['N_STEPS'] - self.keyword_lookup['EQUILIBRATION'])/self.keyword_lookup['XTC_FREQ'])) + 1 ## print the system overview print("--> System Overview") print("Total number of steps : %i" % self.keyword_lookup['N_STEPS']) print("Num. equilibration steps : %i" % self.keyword_lookup['EQUILIBRATION']) print(f"Save equilibration frames : {str(self.keyword_lookup['SAVE_EQ'])}") print(f"Expected number of frames : {expected_number_of_frames}") print("Start temperature : %3.2f" % self.keyword_lookup['TEMPERATURE']) print("Final temperature : %3.2f" % self.keyword_lookup['EQUILIBRIUM_TEMPERATURE']) print("Lattice-to-Angstroms : %5.2f" % self.keyword_lookup['LATTICE_TO_ANGSTROMS']) print(f"Autocenter : {str(self.keyword_lookup['AUTOCENTER'])}") print(f"Save at end only : {str(self.keyword_lookup['SAVE_AT_END'])}") # log key things pimmslogger.log_status("NUMBER OF STEPS : %i" % self.keyword_lookup['N_STEPS'], timestamp=False) pimmslogger.log_status("EQUIL. STEPS : %i" % self.keyword_lookup['EQUILIBRATION'], timestamp=False) pimmslogger.log_status(f"SAVE EQ : {self.keyword_lookup['SAVE_EQ']}", timestamp=False) pimmslogger.log_status(f"XTC_OUT_FREQ : {self.keyword_lookup['XTC_FREQ']}", timestamp=False) pimmslogger.log_status(f"EXPECTED # FRAMES : {expected_number_of_frames}", timestamp=False) pimmslogger.log_status("START TEMPERATURE : %3.2f" % self.keyword_lookup['TEMPERATURE'], timestamp=False) pimmslogger.log_status("FINAL TEMPERATURE : %3.2f" % self.keyword_lookup['EQUILIBRIUM_TEMPERATURE'], timestamp=False) # count total occupied volume fraction total=0 chain_count = 0 for chain in self.keyword_lookup['CHAIN']: total = total + chain[0] * len(chain[1]) chain_count = chain_count+ chain[0] ## ## BOX DIMENSIONS SECTION ## section('Box Dimensions') ## If we are running an initial resized simulation, provide info on conc. for the equilibrations if self.keyword_lookup['RESIZED_EQUILIBRATION']: print("Equilibration box dimensions:") print("") if len(self.keyword_lookup['RESIZED_EQUILIBRATION']) == 2: print("Initial equilibration (eq.) box dimensions : %i x %i" % (self.keyword_lookup['RESIZED_EQUILIBRATION'][0],self.keyword_lookup['RESIZED_EQUILIBRATION'][1])) else: print("Initial equilibration (eq.) box dimensions : %i x %i x %i" % (self.keyword_lookup['RESIZED_EQUILIBRATION'][0], self.keyword_lookup['RESIZED_EQUILIBRATION'][1], self.keyword_lookup['RESIZED_EQUILIBRATION'][2])) if len(self.keyword_lookup['DIMENSIONS']) == 2: print("Total occupied volume fraction during eq. = %3.5f" % (float(total)/(self.keyword_lookup['RESIZED_EQUILIBRATION'][0]*self.keyword_lookup['RESIZED_EQUILIBRATION'][1]))) else: print("Total occupied volume fraction during eq. = %3.5f" % (float(total)/(self.keyword_lookup['RESIZED_EQUILIBRATION'][0]*self.keyword_lookup['RESIZED_EQUILIBRATION'][1]*self.keyword_lookup['RESIZED_EQUILIBRATION'][2]))) # assume a conversion of 1 lattice unit = 4 angstroms - *0.4 is *4 / 10 to get in units of nm v_in_nm_3 = self.keyword_lookup['RESIZED_EQUILIBRATION'][0]*self.keyword_lookup['LATTICE_TO_ANGSTROMS']*0.1 * self.keyword_lookup['RESIZED_EQUILIBRATION'][1]*self.keyword_lookup['LATTICE_TO_ANGSTROMS']*0.1 * self.keyword_lookup['RESIZED_EQUILIBRATION'][2]*self.keyword_lookup['LATTICE_TO_ANGSTROMS']*0.1 v_in_L = 1e-24*v_in_nm_3 conc = (chain_count/6.02e23)/v_in_L print("Total concentration of solute during equilibration = %10.12f (M)" % (conc)) # space between EQ and main simulation section print("") print("Main simulation box dimensions:") print("") # once we get here we're printing box information on the full simulation if len(self.keyword_lookup['DIMENSIONS']) == 2: print("BOX DIMENSIONS : %i x %i" % (self.keyword_lookup['DIMENSIONS'][0],self.keyword_lookup['DIMENSIONS'][1])) else: print("BOX DIMENSIONS : %i x %i x %i" % (self.keyword_lookup['DIMENSIONS'][0], self.keyword_lookup['DIMENSIONS'][1], self.keyword_lookup['DIMENSIONS'][2])) if len(self.keyword_lookup['DIMENSIONS']) == 2: print("Total occupied volume fraction : %3.5f" % (float(total)/(self.keyword_lookup['DIMENSIONS'][0]*self.keyword_lookup['DIMENSIONS'][1]))) else: print("Total occupied volume fraction : %3.5f" % (float(total)/(self.keyword_lookup['DIMENSIONS'][0]*self.keyword_lookup['DIMENSIONS'][1]*self.keyword_lookup['DIMENSIONS'][2]))) # assume a conversion of 1 lattice unit = 4 angstroms - *0.4 is *4 / 10 to get in units of nm v_in_nm_3 = self.keyword_lookup['DIMENSIONS'][0]*self.keyword_lookup['LATTICE_TO_ANGSTROMS']*0.1*self.keyword_lookup['DIMENSIONS'][1]*self.keyword_lookup['LATTICE_TO_ANGSTROMS']*0.1*self.keyword_lookup['DIMENSIONS'][2]*self.keyword_lookup['LATTICE_TO_ANGSTROMS']*0.1 v_in_L = 1e-24*v_in_nm_3 conc = (chain_count/6.02e23)/v_in_L print("Total conc. of solute(s) : %10.12f (M)" % (conc)) ## ## QUENCH SETTINGS SECTION ## section('Quench Settings') if self.keyword_lookup['QUENCH_RUN']: quenchsteps = (self.keyword_lookup['QUENCH_FREQ']/self.keyword_lookup['QUENCH_STEPSIZE'])*abs(self.keyword_lookup['QUENCH_START'] - self.keyword_lookup['QUENCH_END']) if self.keyword_lookup['QUENCH_AS_EQUILIBRATION']: print("Quench running as equilibration: TRUE") print("Equilibration length: %i" % self.keyword_lookup['EQUILIBRATION']) else: print("Quench running as equilibration: FALSE") print("Number of steps for quenching: %i" % quenchsteps) # Quick comment on this: quenchsteps vs./ the equilibration quench are actually different. quenchsteps is the number of steps # we spend quenching - once we've reached the target temperature we are by definition no longer quenching. HOWEVER, for # equilibration we continue equilibrating for QUENCH_STEPSIZE steps before equilibration is finished (i.e. we need to run # *some* simulation at the production temperature for the equilibration to actually equilibrate at that temperature. # Hence these two numbers differ by QUENCH_STEPSIZE steps. This is _not_ a bug!! print("QUENCH FREQ : %i" % self.keyword_lookup['QUENCH_FREQ']) print("QUENCH START : %3.2f " % self.keyword_lookup['QUENCH_START']) print("QUENCH STEP : %3.2f " % self.keyword_lookup['QUENCH_STEPSIZE']) print("QUENCH END : %3.2f " % self.keyword_lookup['QUENCH_END']) else: print("NO TEMPERATURE QUENCH IN EFFECT") ## ## FREEZE FILE SETTINGS SECTION ## if self.keyword_lookup['FREEZE_FILE']: section('Freeze File Settings') print(f"Freeze file : {self.keyword_lookup['FREEZE_FILE'].filename}") print(f"Chains to freeze : {self.keyword_lookup['FREEZE_FILE'].chains}") print("** NB: detailed sanity checking will happen after full initiation**") ## ## SIMULATIO COMPONENTS SECTION ## section('Simulation Components') # if we're running a non-interacting simulation if self.keyword_lookup['NON_INTERACTING']: print("") print("NOTE: This is a non-interacting simulation - all energy interactions other") print(" than excluded volume and angle potentials have been switched OFF") print("") if self.keyword_lookup['RESTART_FILE']: print('RESTART FILE PROVIDED: System components from restart file described above') chainGroup = 1 total=0 for chain in self.keyword_lookup['CHAIN']: print("Chain group %i:" % chainGroup) print(" %i copies of the following chain:" % chain[0]) print(" %s" % chain[1]) chainGroup = chainGroup+1 total = total + chain[0] * len(chain[1]) print("") ## ## OUTPUT SETTINGS SECTION ## section('Output Parameters') print("Print-to-screen frequency : %i" % self.keyword_lookup['PRINT_FREQ']) print("XTC out frequency : %i" % self.keyword_lookup['XTC_FREQ']) print("Energy out frequency : %i" % self.keyword_lookup['EN_FREQ']) print("Overal analysis frequency : %i" % self.keyword_lookup['ANALYSIS_FREQ']) print(f"Write chain to chainID file? : {self.keyword_lookup['WRITE_CHAIN_TO_CHAINID']}") IO_utils.newline(2) print("Keyfile fully parsed! Preparing to start the simulation...") IO_utils.horizontal_line()
#----------------------------------------------------------------- #
[docs] def assign_default(self): """ Build the ``self.DEFAULTS`` dictionary of default keyword values. The absolute reference default values are taken from ``CONFIG.DEFAULTS``. In reality these could live in a separate configuration file, but in the interest of keeping everything within this file we define those default values here. This basically builds up a self.DEFAULTS dictionary which defines default values for each keyword. Note that for a few of these the default can depend on the keywords parsed. Note that these defaults are used in two ways: 1. If keywords are not provided then they are used as the default. Obviously. 2. This can be used as a test to ask if a keyword has been set, because IF you pass a keyword and it changes the parsed keyword from the default this is the only time we care about a keyword being provided, hence functionaly this is how we define if a keyword is provided (or not). In particular, this is used for evaluating if EXPERIMENTAL keywords are being used. Returns ------- None No return value, but ``self.DEFAULTS`` is populated in place (including analysis-frequency defaults derived from ``ANALYSIS_FREQ`` and a freshly generated random ``SEED``). """ # set defaults from the CONFIG file for k in CONFIG.DEFAULTS: self.DEFAULTS[k] = CONFIG.DEFAULTS[k] # if we defined a standard analysis frequency... if 'ANALYSIS_FREQ' in self.keyword_lookup: anafreq = self.keyword_lookup['ANALYSIS_FREQ'] else: anafreq = self.DEFAULTS['ANALYSIS_FREQ'] # assign the analysis frequencies to the default value self.DEFAULTS['ANA_POL'] = anafreq self.DEFAULTS['ANA_INTSCAL'] = anafreq self.DEFAULTS['ANA_DISTMAP'] = anafreq self.DEFAULTS['ANA_ACCEPTANCE'] = anafreq self.DEFAULTS['ANA_INTER_RESIDUE'] = anafreq self.DEFAULTS['ANA_CLUSTER'] = anafreq # get a real random integer random.seed() self.DEFAULTS['SEED'] = random.randint(1,sys.maxsize-1)
[docs] def add_derived_keywords(self): """ Add keywords that are derived from already-parsed keyword values. This runs after parsing, default assignment and sanity checking. It sets the end-to-end-distance analysis frequency (``ANA_END_TO_END``) equal to the polymer-analysis frequency, and sets ``EQUILIBRIUM_TEMPERATURE`` to the final simulation temperature (the quench end temperature for a quench run, otherwise the standard ``TEMPERATURE``). Returns ------- None No return value, but ``self.keyword_lookup`` is updated in place with the derived keywords ``ANA_END_TO_END`` and ``EQUILIBRIUM_TEMPERATURE``. """ # we consider the end-to-end distance to be a polymeric property self.keyword_lookup['ANA_END_TO_END'] = self.keyword_lookup['ANA_POL'] # set the equilibrium temperature - if we're doing a temperature run use the final temperature # from the run (note 'TEMPERATURE' will have already been updated to match QUENCH_START at this # point), if not just used the default temperature if self.keyword_lookup['QUENCH_RUN']: self.keyword_lookup['EQUILIBRIUM_TEMPERATURE'] = self.keyword_lookup['QUENCH_END'] else: self.keyword_lookup['EQUILIBRIUM_TEMPERATURE'] = self.keyword_lookup['TEMPERATURE']
[docs] def sanity_check_and_update_with_restart_file(self): """ Run sanity checks and update the CHAIN information so the box dimensions/concentration info is correct. Note that we DON'T sanity check the restart file input against the keyfile-defined chain values. This means we can be agnostic about what's in a restart file, and allows restarts to be more permissive. This set of sanity checks is actually the last thing done, which means that we have to re-check some of the things that were already checked with updated information read from the restart file. Below is the general ruberic for how restart files are dealt with: - The restart file fully overwrites all chain information. Chain information in a keyfile is completely ignored if a restart file is provided. - By default we assume the keyfile provided dimensions and hardwall status are to be used by in the restart. However, this may actually not be compatible, in which case an exception is thrown. The RESTART_OVERRIDE_DIMENSIONS, RESTART_OVERRIDE_HARDWALL force the simulation to use the dimensions and hardwall values passed by the restart file. The one trick here is that if the keyfile requires an resized_equilibration then the restart file's dimensions will be used for the initial equilibration, and the production part of the simulation will be run using information from the keyfile. Returns ------- None No return value. ``self.keyword_lookup`` is updated in place (the ``CHAIN`` composition is rebuilt from the restart object, and ``HARDWALL`` / ``DIMENSIONS`` may be overridden), and the restart object's lattice dimensions may be recentred/resized. Returns early (without changes) if no restart file is set. Raises ------ RestartException If the restart file is incompatible with the keyfile (e.g. mismatched dimensionality, conflicting hardwall/PBC modes, or box dimensions that are smaller than the restart file's dimensions). """ if not self.keyword_lookup['RESTART_FILE']: return new_chains ={} # grab the restart object restart_object = self.keyword_lookup['RESTART_FILE'] print("") print("############################") print("# Reading Restart File #") print("############################") print("") print("Restart file dimensions: %s" % restart_object.dimensions) print("Restart file hardwall flag: %s" % restart_object.hardwall) print("") ## ........................................................................................................................ ## ## Part 1: Chains ## # extract the chains from the restart object into a dictionary indexed # by type, where the tuple is [ count, chain_sequence ] # Note that we do not worry about EXTRA_CHAINS here as they are built # de novo and need to be kept seperate from the CHAINS here. This entire # block of code just ensures that we can convert a RESTART file into data # that matches a <COUNT> <CHAIN SEQUENCE> format chain_type_dictionary ={} # Build chain composition from both restart chains and EXTRA_CHAIN entries, # so startup concentration/occupied-volume reporting reflects the full system. all_restart_chain_entries = {} all_restart_chain_entries.update(restart_object.chains) all_restart_chain_entries.update(restart_object.extra_chains) # this cycles through each individual chain in the restart object, where # chain_info[0] = chain_positions # chain_info[1] = chain sequence # chain_info[2] = chain type for chainID in all_restart_chain_entries: # for each chainID get the chain sequence and # the chain type c_seq = all_restart_chain_entries[chainID][1] c_type = all_restart_chain_entries[chainID][2] # if this is the first time we encounter this chain type create a new # entry in the chain_type_dictionary. if c_type not in chain_type_dictionary: chain_type_dictionary[c_type] = [0, c_seq] # next verify that a chain of type c_type matches the other chains of # type c_type that we've already seen so far ## NOTE - other places in the code would allow this; probably should address this at # some point for consistency... if not chain_type_dictionary[c_type][1] == c_seq: raise RestartException('When reading in the restart file, found a chain of type %i that did not match sequence of another chain of type %s. Chain sequences are\n: %s\n%s\n' % (c_type, c_type, chain_type_dictionary[c_type][1], c_seq)) # if all seems good increment chain_type_dictionary[c_type][0] = chain_type_dictionary[c_type][0] + 1 # when we get here chain_type_dictionary is a dictionary where keys # are unique chain types and the values are lists of the form [ count, chain_sequence ], # finally sort the types and reconstruct a chains list, that has # each type as a seperate entry (i.e. [[count_1, seq_1],[count_2, seq_2]] # and so on chain_types = list(chain_type_dictionary.keys()) chain_types.sort() chains = [] for CT in chain_types: chains.append(chain_type_dictionary[CT]) # at this point chains is a list of lists, where each element is of the form [ count, chain_sequence ], # and the order of the list is sorted by chain type. # Store restart-derived chain composition under the canonical CHAIN keyword. self.keyword_lookup['CHAIN'] = chains if len(chain_types) > 1: print("--> Read in %i different chain types from the restart file" % (len(chain_types))) else: print("--> Read in a single chain type from the restart file") print("--> Chain(s) read in from restart file are as follows:") for tmp in chains: if tmp[0] == 1: print(f" 1 copy of {tmp[1]}") else: print(f" {tmp[0]} copies of {tmp[1]}") print('') # note no need to actually check stuff, but, print things here... if len(restart_object.extra_chains) > 0: print("--> Also read in additiona; chains from EXTRA_CHAIN keyword") print("--> Chain(s) read in from EXTRA_CHAIN keyword are as follows:") for tmp in self.keyword_lookup['EXTRA_CHAIN']: if tmp[0] == 1: print(f" 1 copy of {tmp[1]}") else: print(f" {tmp[0]} copies of {tmp[1]}") print('') ## ........................................................................................................................ ## ## Part 2: Hardwall rules ## # if the keyfile says we should default to the hardwall rules associated with the restart file... if self.keyword_lookup['RESTART_OVERRIDE_HARDWALL']: print("Setting HARDWALL keyword based on restart file") self.keyword_lookup['HARDWALL'] = restart_object.hardwall # assume that the chains are consistent with the restart file mode (maybe should explicitly check this # in future versions...?) # if the restart object was a PBC simulation (i.e. not hardwall) elif not restart_object.hardwall: # if the restart file was a PBC simulation and we are trying to run a hardwall simulation - not compatible if self.keyword_lookup['HARDWALL']: raise RestartException('\n\nRestart file describes a periodic boundary simulation, but the keyfile is trying to run as a hardwall simulation. These options are incompatible with one another. Please set HARDWALL : False (or delete the HARDWALL keyword)\n') # if the restart file was a PBC simulation and we are trying to run a RESIZED_EQUILIBRIUM situation this doesn't work if self.keyword_lookup['RESIZED_EQUILIBRATION']: raise RestartException('\n\nRestart file describes a periodic boundary simulation, but the keyfile is trying to run as simulation that has a resized equilibration step (RESIZED_EQUILIBRATION : True). These options are incompatible with one another. The initial simulation box that is then resized MUST be a hardwall simulation.\n') # if the restart object was a hardwall simulation we can, from there, start either a PBC or a hardwall simulation. Don't need to do anything, # just wanted to explicitly include this case in the code to show it is considered! else: if not self.keyword_lookup['HARDWALL']: print("While restart file was generated by a hardwall simulation,\nthis simulation applies periodic boundary conditions\n") ## ........................................................................................................................ ## ## Part 3: Dimension Rules ## # if the restart file is being used to override everything if self.keyword_lookup['RESTART_OVERRIDE_DIMENSIONS']: print("Setting DIMENSIONS keyword based on restart file") self.keyword_lookup['DIMENSIONS'] = restart_object.dimensions if self.keyword_lookup['RESIZED_EQUILIBRATION']: raise RestartException("\n\nRESTART_OVERRIDE_DIMENSIONS is set to true, but the simulation also wants a resized equilibration. These options are incompatible. If the simulation being restarted is a hardwall simulation then you can use the following approach\n1) Set the RESIZED_EQUILIBRATION to equal (or bigger) than the restart file's dimensions\n2) Set the DIMENSIONS to the production dimensions desired\n3) Set RESTART_OVERRIDE_DIMENSIONS and RESTART_OVERRIDE_HARDWALL to False\n") else: print("Setting DIMENSIONS based on ...") dimchange_flag=False n_dims = len(self.keyword_lookup['DIMENSIONS']) # check if number of dimensions in keyfile matches number of dimensions in restart file - if not throw an error if n_dims != len(restart_object.dimensions): raise RestartException("\n\nRestart object has %i dimensions but kefile specifies %i dimensions\n" % (len(restart_object.dimensions), n_dims) ) # check that if the restart file was a PBC simulation, the new dimensions match (note we KNOW that if we're doing a PBC we're not running a resize equilibration # simulation because this is dealt with in part 2) if not restart_object.hardwall: for dim in range(0, n_dims): if not restart_object.dimensions[dim] == self.keyword_lookup['DIMENSIONS'][dim]: raise RestartException("\n\nThe restart file was a PBC simulation, so the box dimensions must match EXACTLY to avoid breaking PBC assumptions. The restart file dimensions are [%s] but the DIMENSIONS keyword in the keyfile is set to [%s]\n" %(restart_object.dimensions, self.keyword_lookup['DIMENSIONS'])) ### If the initial part of the simulation will be running on the RESIZED_EQUILIBRATION dimensions.. # if we're trying to use a restart file to run a RESIZED_EQUILIBRATION (note we now know that the restart file was a hardwall simulation # as this is checked in part 2) if self.keyword_lookup['RESIZED_EQUILIBRATION']: # must ensure that the resize file dimenions are equal to or smaller than the resized equilibration being used for dim in range(0, n_dims): if self.keyword_lookup['RESIZED_EQUILIBRATION']: if restart_object.dimensions[dim] > self.keyword_lookup['RESIZED_EQUILIBRATION'][dim]: raise RestartException("\n\nRESIZED_EQUILIBRATION dimensions [%s] are smaller than the restart file's dimensions [%s]. This is an incompatible situation.\n"%(self.keyword_lookup['RESIZED_EQUILIBRATION'][dim], restart_object.dimensions[dim])) # finally center lattice if needed for dim in range(0, n_dims): if not (restart_object.dimensions[dim] == self.keyword_lookup['RESIZED_EQUILIBRATION'][dim]): dimchange_flag=True if dimchange_flag: print("Dimensions in restart file were %s, but these have been updated to %s based on the keyfile (where RESIZED_EQUILIBRATION is set to %s)" % (restart_object.dimensions, self.keyword_lookup['RESIZED_EQUILIBRATION'],self.keyword_lookup['RESIZED_EQUILIBRATION'])) restart_object.update_lattice_dimensions(self.keyword_lookup['RESIZED_EQUILIBRATION']) else: print("Dimensions in restart file matched RESIZED_EQUILIBRATION keyword: %s" % (self.keyword_lookup['RESIZED_EQUILIBRATION'])) ### If the intial part of the simulation will be running on the normal DIMENSIONS keyword # finally, ensure that the dimensions are equal to or bigger than the restart file (this is a catch all but is true for # PBC and hardwall simulations alike) else: for dim in range(0, n_dims): if restart_object.dimensions[dim] > self.keyword_lookup['DIMENSIONS'][dim]: raise RestartException("\n\nDIMENSIONS dimensions [%s] are smaller than the restart file's dimensions [%s]. This is an incompatible situation.\n"%(self.keyword_lookup['DIMENSIONS'][dim], restart_object.dimensions[dim])) if not (restart_object.dimensions[dim] == self.keyword_lookup['DIMENSIONS'][dim]): dimchange_flag=True if dimchange_flag: print("Dimensions in restart file were %s, but these have been updated to %s based on the keyfile" % (restart_object.dimensions, self.keyword_lookup['DIMENSIONS'])) restart_object.update_lattice_dimensions(self.keyword_lookup['DIMENSIONS']) else: print("Dimensions in restart file matched keyfile: %s" % (self.keyword_lookup['DIMENSIONS'])) print("") print(".... Restart file processed") print("############################") print("")