def units_from_snapshot(snapshot): """ Returns a dict of simtk.unit.Unit instances that represent the used units in the snapshot Parameters ---------- snapshot : Snapshot the snapshot to be used Returns ------- units : dict of {str : simtk.unit.Unit } representing a dict of string representing a dimension ('length', 'velocity', 'energy') pointing the the simtk.unit.Unit to be used """ units = {} if snapshot.coordinates is not None: if hasattr(snapshot.coordinates, 'unit'): units['length'] = snapshot.coordinates.unit else: units['length'] = u.Unit({}) if snapshot.potential_energy is not None: if hasattr(snapshot.potential_energy, 'unit'): units['energy'] = snapshot.potential_energy.unit else: units['energy'] = u.Unit({}) if snapshot.velocities is not None: if hasattr(snapshot.velocities, 'unit'): units['velocity'] = snapshot.velocities.unit else: units['velocity'] = u.Unit({}) return units
def __init__(self, filename, mode=None, fallback=None): """ Create a storage for complex objects in a netCDF file Parameters ---------- filename : string filename of the netcdf file to be used or created mode : str the mode of file creation, one of 'w' (write), 'a' (append) or 'r' (read-only) None, which will append any existing files (equal to append), is the default. fallback : :class:`openpathsampling.Storage` the _fallback_ storage to be loaded from if an object is not present in this storage. By default you will not try to resave objects that could be found in the fallback. Note that the fall back does only work if `use_uuid` is enabled Notes ----- A single file can be opened by multiple storages, but only one can be used for writing """ if mode is None: mode = 'a' self.mode = mode exists = os.path.isfile(filename) if exists and mode == 'a': logger.info( "Open existing netCDF file '%s' for appending - " "appending existing file", filename) elif exists and mode == 'w': logger.info( "Create new netCDF file '%s' for writing - " "deleting existing file", filename) elif not exists and mode == 'a': logger.info( "Create new netCDF file '%s' for appending - " "appending non-existing file", filename) elif not exists and mode == 'w': logger.info( "Create new netCDF file '%s' for writing - " "creating new file", filename) elif not exists and mode == 'r': logger.info( "Open existing netCDF file '%s' for reading - " "file does not exist", filename) raise RuntimeError("File '%s' does not exist." % filename) elif exists and mode == 'r': logger.info( "Open existing netCDF file '%s' for reading - " "reading from existing file", filename) self.filename = filename self.fallback = fallback # this can be set to false to re-store objects present in the fallback self.exclude_from_fallback = True # this can be set to false to re-store proxies from other stores self.exclude_proxy_from_other = False # call netCDF4-python to create or open .nc file super(NetCDFPlus, self).__init__(filename, mode) self._setup_class() if mode == 'w': logger.info("Setup netCDF file and create variables") self.setncattr('format', 'netcdf+') self.setncattr('ncplus_version', self._netcdfplus_version_) self.write_meta() # add shared scalar dimension for everyone self.create_dimension('scalar', 1) self.create_dimension('pair', 2) self.setncattr('use_uuid', 'True') self._create_simplifier() # create the store that holds stores store_stores = NamedObjectStore(ObjectStore) store_stores.name = 'stores' self.register_store('stores', store_stores) self.stores.initialize() self.stores.set_caching(True) self.update_delegates() # now create all storages in subclasses self._create_storages() self.create_store('attributes', PseudoAttributeStore()) # call the subclass specific initialization self._initialize() # this will create all variables in the storage for all new # added stores this is often already call inside of _initialize. # If not we just make sure self.finalize_stores() logger.info("Finished setting up netCDF file") self.sync() elif mode == 'a' or mode == 'r+' or mode == 'r': logger.debug("Restore the dict of units from the storage") self.check_version() # self.reference_by_uuid = hasattr(self, 'use_uuid') # self.reference_by_uuid = True self._create_simplifier() # open the store that contains all stores self.register_store('stores', NamedObjectStore(ObjectStore)) self.stores.set_caching(True) self.create_variable_delegate('stores_json') self.create_variable_delegate('stores_name') self.create_variable_delegate('stores_uuid') self.stores.restore() # Create a dict of simtk.Unit() instances for all netCDF.Variable() for variable_name in self.variables: variable = self.variables[variable_name] if self.support_simtk_unit: import simtk.unit as u if hasattr(variable, 'unit_simtk'): unit_dict = self.simplifier.from_json( getattr(variable, 'unit_simtk')) if unit_dict is not None: unit = self.simplifier.unit_from_dict(unit_dict) else: unit = self.simplifier.unit_from_dict(u.Unit({})) self.units[str(variable_name)] = unit # register all stores that are listed in self.stores for store in self.stores: if store is not None: logger.debug("Register store %s in the storage" % store.name) self.register_store(store.name, store) store.register(self, store.name) self.update_delegates() self._restore_storages() # only if we have a new style file if hasattr(self, 'attributes'): for attribute, store in zip( self.attributes, self.attributes.vars['cache'] ): if store is not None: key_store = self.attributes.key_store(attribute) key_store.attribute_list[attribute] = store # call the subclass specific restore in case there is more stuff # to prepare self._restore() self.set_auto_mask(False)
def create_variable(self, var_name, var_type, dimensions, description=None, chunksizes=None, simtk_unit=None, maskable=False): """ Create a new variable in the netCDF storage. This is just a helper function to structure the code better and add some convenience to creating more complex variables Parameters ========== var_name : str The name of the variable to be created var_type : str The string representing the type of the data stored in the variable. Allowed are strings of native python types in which case the variables will be treated as python or a string of the form 'numpy.type' which will refer to the numpy data types. Numpy is preferred sinec the api to netCDF uses numpy and thus it is faster. Possible input strings are `int`, `float`, `long`, `str`, `numpy.float32`, `numpy.float64`, `numpy.int8`, `numpy.int16`, `numpy.int32`, `numpy.int64`, `json`, `obj.<store>`, `lazyobj.<store>` dimensions : str or tuple of str A tuple representing the dimensions used for the netcdf variable. If not specified then the default dimension of the storage is used. If the last dimension is `'...'` then it is assumed that the objects are of variable length. In netCDF this is usually referred to as a VLType. We will treat is just as another dimension, but it can only be the last dimension. description : str A string describing the variable in a readable form. chunksizes : tuple of int A tuple of ints per number of dimensions. This specifies in what block sizes a variable is stored. Usually for object related stuff we want to store everything of one object at once so this is often (1, ..., ...) simtk_unit : str A string representing the units used for this variable. Can be used with all var_types although it makes sense only for numeric ones. maskable : bool, default: False If set to `True` the values in this variable can only partially exist and if they have not yet been written they are filled with a fill_value which is treated as a non-set variable. The created variable will interpret this values as `None` when returned """ ncfile = self if type(dimensions) is str: dimensions = [dimensions] dimensions = list(dimensions) new_dimensions = dict() for ix, dim in enumerate(dimensions): if type(dim) is int: dimensions[ix] = var_name + '_dim_' + str(ix) new_dimensions[dimensions[ix]] = dim if dimensions[-1] == '...': # last dimension is simply [] so we allow arbitrary length # and remove the last dimension variable_length = True dimensions = dimensions[:-1] else: variable_length = False if var_type == 'obj' or var_type == 'lazyobj': dimensions.append('pair') if chunksizes is not None: chunksizes = tuple(list(chunksizes) + [2]) nc_type = self.var_type_to_nc_type(var_type) for dim_name, size in new_dimensions.items(): ncfile.create_dimension(dim_name, size) dimensions = tuple(dimensions) # if chunk sizes are strings then replace it by # the actual size of the dimension if chunksizes is not None: chunksizes = list(chunksizes) for ix, dim in enumerate(chunksizes): if dim == -1: chunksizes[ix] = len(ncfile.dimensions[dimensions[ix]]) if type(dim) is str: chunksizes[ix] = len(ncfile.dimensions[dim]) chunksizes = tuple(chunksizes) if variable_length: vlen_t = ncfile.createVLType(nc_type, var_name + '_vlen') ncvar = ncfile.createVariable( var_name, vlen_t, dimensions, chunksizes=chunksizes ) setattr(ncvar, 'var_vlen', 'True') else: ncvar = ncfile.createVariable( var_name, nc_type, dimensions, chunksizes=chunksizes, ) setattr(ncvar, 'var_type', var_type) if self.support_simtk_unit and simtk_unit is not None: import simtk.unit as u if isinstance(simtk_unit, u.Unit): unit_instance = simtk_unit symbol = unit_instance.get_symbol() elif isinstance(simtk_unit, u.BaseUnit): unit_instance = u.Unit({simtk_unit: 1.0}) symbol = unit_instance.get_symbol() elif type(simtk_unit) is str and hasattr(u, simtk_unit): unit_instance = getattr(u, simtk_unit) symbol = unit_instance.get_symbol() else: raise NotImplementedError( 'Unit by abbreviated string representation ' 'is not yet supported') json_unit = self.simplifier.unit_to_json(unit_instance) # store the unit in the dict inside the Storage object self.units[var_name] = unit_instance # Define units for a float variable setattr(ncvar, 'unit_simtk', json_unit) setattr(ncvar, 'unit', symbol) if maskable: setattr(ncvar, 'maskable', 'True') if description is not None: if type(dimensions) is str: dim_names = [dimensions] else: dim_names = ['#ix{0}:{1}'.format(*p) for p in enumerate(dimensions)] idx_desc = '[' + ']['.join(dim_names) + ']' description = var_name + idx_desc + ' is ' + \ description.format(idx=dim_names[0], ix=dim_names) # Define long (human-readable) names for variables. setattr(ncvar, "long_str", description) self.update_delegates() return ncvar
def unit_from_dict(unit_dict): unit = units.Unit({}) for unit_name, unit_multiplication in unit_dict.items(): unit *= getattr(units, unit_name)**unit_multiplication return unit
def testCompositeUnits(self): """ Tests the creation of a composite unit """ mps = u.Unit({u.meter_base_unit : 1.0, u.second_base_unit : -1.0}) self.assertTrue(u.is_unit(mps)) self.assertEqual(str(mps), 'meter/second')
class DynamicsEngine(StorableNamedObject): """ Wraps simulation tool (parameters, storage, etc.) Attributes ---------- on_nan : str set the behaviour of the engine when `NaN` is detected. Possible is 1. `fail` will raise an exception `EngineNaNError` 2. `retry` will rerun the trajectory in engine.generate, these moves do not satisfy detailed balance on_error : str set the behaviour of the engine when an exception happens. Possible is 1. `fail` will raise an exception `EngineError` 2. `retry` will rerun the trajectory in engine.generate, these moves do not satisfy detailed balance on_max_length : str set the behaviour if the trajectory length is `n_frames_max`. If `n_frames_max == 0` this will be ignored and nothing happens. Possible is 1. `fail` will raise an exception `EngineMaxLengthError` 2. `stop` will stop and return the max length trajectory (default) 3. `retry` will rerun the trajectory in engine.generate, these moves do not satisfy detailed balance retries_when_nan : int, default: 2 the number of retries (if chosen) before an exception is raised retries_when_error : int, default: 2 the number of retries (if chosen) before an exception is raised retries_when_max_length : int, default: 0 the number of retries (if chosen) before an exception is raised on_retry : str or callable the behaviour when a try is started. Since you have already generated some trajectory you might not restart completely. Possibilities are 1. `full` will restart completely and use the initial frames (default) 2. `50%` will cut the existing in half but keeping at least the initial 3. `remove_interval` will remove as many frames as the `interval` 4. a callable will be used as a function to generate the new from the old trajectories, e.g. `lambda t: t[:10]` would restart with the first 10 frames Notes ----- Should be considered an abstract class: only its subclasses can be instantiated. """ FORWARD = 1 BACKWARD = -1 _default_options = { 'n_frames_max': None, 'on_max_length': 'fail', 'on_nan': 'fail', 'retries_when_nan': 2, 'retries_when_error': 0, 'retries_when_max_length': 0, 'on_retry': 'full', 'on_error': 'fail' } units = { 'length': u.Unit({}), 'velocity': u.Unit({}), 'energy': u.Unit({}) } base_snapshot_type = BaseSnapshot def __init__(self, options=None, descriptor=None): """ Create an empty DynamicsEngine object Notes ----- The purpose of an engine is to create trajectories and keep track of the results. The main method is 'generate' to create a trajectory, which is a list of snapshots and then can store the in the associated storage. In the initialization this storage is created as well as the related Trajectory and Snapshot classes are initialized. """ super(DynamicsEngine, self).__init__() self.descriptor = descriptor self._check_options(options) @property def current_snapshot(self): return None @current_snapshot.setter def current_snapshot(self, snap): pass def to_dict(self): return {'options': self.options, 'descriptor': self.descriptor} def _check_options(self, options=None): """ This will register all variables in the options dict as a member variable if they are present in either the `DynamicsEngine.default_options` or this classes default_options, no multiple inheritance is supported! It will use values with the priority in the following order - DynamicsEngine.default_options - self.default_options - self.options (usually not used) - options (function parameter) Parameters are only registered if 1. the variable name is present in the defaults 2. the type matches the one in the defaults 3. for variables with units also the units need to be compatible Parameters ---------- options : dict of { str : value } A dictionary Notes ----- Options are what is necessary to recreate the engine, but not runtime variables or independent variables like the actual initialization status, the runners or an attached storage. If there are non-default options present they will be ignored (no error thrown) """ # start with default options from a dynamics engine my_options = {} okay_options = {} # self.default_options overrides default ones from DynamicsEngine for variable, value in self.default_options.iteritems(): my_options[variable] = value if hasattr(self, 'options') and self.options is not None: # self.options overrides default ones for variable, value in self.options.iteritems(): my_options[variable] = value if options is not None: # given options override even default and already stored ones for variable, value in options.iteritems(): my_options[variable] = value if my_options is not None: for variable, default_value in self.default_options.iteritems(): # create an empty member variable if not yet present if not hasattr(self, variable): okay_options[variable] = None if variable in my_options: if type(my_options[variable]) is type(default_value): if type(my_options[variable]) is u.Unit: if my_options[variable].unit.is_compatible( default_value): okay_options[variable] = my_options[variable] else: raise ValueError( 'Unit of option "' + str(variable) + '" (' + str(my_options[variable].unit) + ') not compatible to "' + str(default_value.unit) + '"') elif type(my_options[variable]) is list: if isinstance(my_options[variable][0], type(default_value[0])): okay_options[variable] = my_options[variable] else: raise \ ValueError( 'List elements for option "' + str(variable) + '" must be of type "' + str(type(default_value[0])) + '"') else: okay_options[variable] = my_options[variable] elif isinstance(my_options[variable], type(default_value)): okay_options[variable] = my_options[variable] elif default_value is None: okay_options[variable] = my_options[variable] else: raise ValueError('Type of option "' + str(variable) + '" (' + str(type(my_options[variable])) + ') is not "' + str(type(default_value)) + '"') self.options = okay_options else: self.options = {} def __getattr__(self, item): # first, check for errors that might be shadowed in properties if item in self.__class__.__dict__: # we should have this attribute p = self.__class__.__dict__[item] if isinstance(p, property): # re-run, raise the error inside the property try: result = p.fget(self) except: raise else: # alternately, trust the fixed result with # return result # miraculously fixed raise AttributeError( "Unknown problem occurred in property" + str(p.fget.func_name) + ": Second attempt returned" + str(result)) # for now, items in dict that fail with AttributeError will just # give the default message; to change, add something here like: # raise AttributeError("Something went wrong with " + str(item)) # see, if the attribute is actually a dimension if self.descriptor is not None: if item in self.descriptor.dimensions: return self.descriptor.dimensions[item] # fallback is to look for an option and return it's value try: return self.options[item] except KeyError: # convert KeyError to AttributeError default_msg = "'{0}' has no attribute '{1}'" raise AttributeError( (default_msg + ", nor does its options dictionary").format( self.__class__.__name__, item)) @property def dimensions(self): if self.descriptor is None: return {} else: return self.descriptor.dimensions def set_as_default(self): import openpathsampling as p p.EngineMover.engine = self @property def default_options(self): default_options = {} default_options.update(DynamicsEngine._default_options) default_options.update(self._default_options) return default_options # def strip_units(self, item): # """Remove units and set in the standard unit set for this engine. # Each engine needs to know how to do its own unit system. The default # assumes there is no unit system. # Parameters # ---------- # item : object with units # the input with units # Returns # ------- # float or iterable # the result without units, in the engine's specific unit system # """ # return item def start(self, snapshot=None): if snapshot is not None: self.current_snapshot = snapshot def stop(self, trajectory): """Nothing special needs to be done for direct-control simulations when you hit a stop condition.""" pass def stop_conditions(self, trajectory, continue_conditions=None, trusted=True): """ Test whether we can continue; called by generate a couple of times, so the logic is separated here. Parameters ---------- trajectory : :class:`openpathsampling.trajectory.Trajectory` the trajectory we've generated so far continue_conditions : (list of) function(Trajectory) callable function of a 'Trajectory' that returns True or False. If one of these returns False the simulation is stopped. trusted : bool If `True` (default) the stopping conditions are evaluated as trusted. Returns ------- bool true if the dynamics should be stopped; false otherwise """ stop = False if continue_conditions is not None: if isinstance(continue_conditions, list): for condition in continue_conditions: can_continue = condition(trajectory, trusted) stop = stop or not can_continue else: stop = not continue_conditions(trajectory, trusted) return stop def generate(self, snapshot, running=None, direction=+1): r""" Generate a trajectory consisting of ntau segments of tau_steps in between storage of Snapshots. Parameters ---------- snapshot : :class:`openpathsampling.snapshot.Snapshot` initial coordinates and velocities in form of a Snapshot object running : (list of) function(:class:`openpathsampling.trajectory.Trajectory`) callable function of a 'Trajectory' that returns True or False. If one of these returns False the simulation is stopped. direction : -1 or +1 (DynamicsEngine.FORWARD or DynamicsEngine.BACKWARD) If +1 then this will integrate forward, if -1 it will reversed the momenta of the given snapshot and then prepending generated snapshots with reversed momenta. This will generate a _reversed_ trajectory that effectively ends in the initial snapshot Returns ------- trajectory : :class:`openpathsampling.trajectory.Trajectory` generated trajectory of initial conditions, including initial coordinate set Notes ----- If the returned trajectory has length n_frames_max it can still happen that it stopped because of the stopping criterion. You need to check in that case. """ trajectory = None it = self.iter_generate(snapshot, running, direction, intervals=0, max_length=self.options['n_frames_max']) for trajectory in it: pass return trajectory def iter_generate(self, initial, running=None, direction=+1, intervals=10, max_length=0): r""" Return a generator that will generate a trajectory, returning the current trajectory in given intervals Parameters ---------- initial : :class:`openpathsampling.Snapshot` or :class:`openpathsampling.Trajectory` initial coordinates and velocities in form of a Snapshot object or a trajectory running : (list of) function(:class:`openpathsampling.trajectory.Trajectory`) callable function of a 'Trajectory' that returns True or False. If one of these returns False the simulation is stopped. direction : -1 or +1 (DynamicsEngine.FORWARD or DynamicsEngine.BACKWARD) If +1 then this will integrate forward, if -1 it will reversed the momenta of the given snapshot and then prepending generated snapshots with reversed momenta. This will generate a _reversed_ trajectory that effectively ends in the initial snapshot intervals : int number steps after which the current status is returned. If `0` it will run until the end or a keyboard interrupt is detected max_length : int will limit the simulation length to a number of steps. Default is `0` which will run unlimited Yields ------ trajectory : :class:`openpathsampling.trajectory.Trajectory` generated trajectory of initial conditions, including initial coordinate set Notes ----- If the returned trajectory has length n_frames_max it can still happen that it stopped because of the stopping criterion. You need to check in that case. """ if direction == 0: raise RuntimeError( 'direction must be positive (FORWARD) or negative (BACKWARD).') try: iter(running) except TypeError: running = [running] if hasattr(initial, '__iter__'): initial = Trajectory(initial) else: initial = Trajectory([initial]) valid = False attempt_nan = 0 attempt_error = 0 attempt_max_length = 0 trajectory = initial final_error = None errors = [] while not valid and final_error is None: if attempt_nan + attempt_error > 1: # let's get a new initial trajectory the way the user wants to if self.on_retry == 'full': trajectory = initial elif self.on_retry == 'remove_interval': trajectory = \ trajectory[:max( len(initial), len(trajectory) - intervals)] elif self.on_retry == 'keep_half': trajectory = \ trajectory[:min( int(len(trajectory) * 0.9), max( len(initial), len(trajectory) / 2))] elif hasattr(self.on_retry, '__call__'): trajectory = self.on_retry(trajectory) if direction > 0: self.current_snapshot = trajectory[-1] elif direction < 0: # backward simulation needs reversed snapshots self.current_snapshot = trajectory[0].reversed logger.info("Starting trajectory") self.start() frame = 0 # maybe we should stop before we even begin? stop = self.stop_conditions(trajectory=trajectory, continue_conditions=running, trusted=False) log_rate = 10 has_nan = False has_error = False while not stop: if intervals > 0 and frame % intervals == 0: # return the current status logger.info("Through frame: %d", frame) yield trajectory elif frame % log_rate == 0: logger.info("Through frame: %d", frame) # Do integrator x steps snapshot = None try: with DelayedInterrupt(): snapshot = self.generate_next_frame() # if self.on_nan != 'ignore' and \ if not self.is_valid_snapshot(snapshot): has_nan = True break except KeyboardInterrupt as e: # make sure we will report the last state for logger.info('Keyboard interrupt. Shutting down simulation') final_error = e break except: # any other error we start a retry e = sys.exc_info() errors.append(e) se = str(e).lower() if 'nan' in se and \ ('particle' in se or 'coordinates' in se): # this cannot be ignored because we cannot continue! has_nan = True break else: has_error = True break frame += 1 # Store snapshot and add it to the trajectory. # Stores also final frame the last time if direction > 0: trajectory.append(snapshot) elif direction < 0: trajectory.insert(0, snapshot.reversed) if 0 < max_length < len(trajectory): # hit the max length criterion on = self.on_max_length del trajectory[-1] if on == 'fail': final_error = EngineMaxLengthError( 'Hit maximal length of %d frames.' % self.options['n_frames_max'], trajectory) break elif on == 'stop': logger.info('Trajectory hit max length. Stopping.') # fail gracefully stop = True elif on == 'retry': attempt_max_length += 1 if attempt_max_length > self.retries_when_max_length: if self.on_nan == 'fail': final_error = EngineMaxLengthError( 'Failed to generate trajectory without ' 'hitting max length after %d attempts' % attempt_max_length, trajectory) break if stop is False: # Check if we should stop. If not, continue simulation stop = self.stop_conditions(trajectory=trajectory, continue_conditions=running) if has_nan: on = self.on_nan if on == 'fail': final_error = EngineNaNError('`nan` in snapshot', trajectory) elif on == 'retry': attempt_nan += 1 if attempt_nan > self.retries_when_nan: final_error = EngineNaNError( 'Failed to generate trajectory without `nan` ' 'after %d attempts' % attempt_error, trajectory) elif has_error: on = self.on_nan if on == 'fail': final_error = errors[-1][1] del errors[-1] elif on == 'retry': attempt_error += 1 if attempt_error > self.retries_when_error: final_error = EngineError( 'Failed to generate trajectory without `nan` ' 'after %d attempts' % attempt_error, trajectory) elif stop: valid = True self.stop(trajectory) if errors: logger.info('Errors occurred during generation :') for no, e in enumerate(errors): logger.info('[#%d] %s' % (no, repr(e[1]))) if final_error is not None: yield trajectory logger.info("Through frame: %d", len(trajectory)) raise final_error logger.info("Finished trajectory, length: %d", len(trajectory)) yield trajectory def generate_next_frame(self): raise NotImplementedError('Next frame generation must be implemented!') def generate_n_frames(self, n_frames=1): """Generates n_frames, from but not including the current snapshot. This generates a fixed number of frames at once. If you desire the reversed trajectory, you can reverse the returned trajectory. Parameters ---------- n_frames : integer number of frames to generate Returns ------- paths.Trajectory() the `n_frames` of the trajectory following (and not including) the initial `current_snapshot` """ self.start() traj = Trajectory( [self.generate_next_frame() for i in range(n_frames)]) self.stop(traj) return traj @staticmethod def is_valid_snapshot(snapshot): """ Test the snapshot to be valid. Usually not containing nan Returns ------- bool : True returns `True` if the snapshot is okay to be used """ return True @classmethod def check_snapshot_type(cls, snapshot): if not isinstance(snapshot, cls.base_snapshot_type): logger.warning( ('This engine is intended for "%s" and derived classes. ' 'You are using "%s". Make sure that this is intended.') % (cls.base_snapshot_type.__name__, snapshot.__class__.__name__))
def __init__(self, filename, mode=None, units=None): """ Create a storage for complex objects in a netCDF file Parameters ---------- filename : string filename of the netcdf file to be used or created mode : string, default: None the mode of file creation, one of 'w' (write), 'a' (append) or 'r' (read-only) None, which will append any existing files. units : dict of {str : simtk.unit.Unit } or None representing a dict of string representing a dimension ('length', 'velocity', 'energy') pointing to the simtk.unit.Unit to be used. If not `None` it overrides the standard units used Notes ----- A single file can be opened by multiple storages, but only one can be used for writing """ if mode is None: mode = 'a' exists = os.path.isfile(filename) if exists and mode == 'a': logger.info( "Open existing netCDF file '%s' for appending - appending existing file", filename) elif exists and mode == 'w': logger.info( "Create new netCDF file '%s' for writing - deleting existing file", filename) elif not exists and mode == 'a': logger.info( "Create new netCDF file '%s' for appending - appending non-existing file", filename) elif not exists and mode == 'w': logger.info( "Create new netCDF file '%s' for writing - creating new file", filename) elif not exists and mode == 'r': logger.info( "Open existing netCDF file '%s' for reading - file does not exist", filename) raise RuntimeError("File '%s' does not exist." % filename) elif exists and mode == 'r': logger.info( "Open existing netCDF file '%s' for reading - reading from existing file", filename) self.filename = filename # call netCDF4-python to create or open .nc file super(NetCDFPlus, self).__init__(filename, mode) self._setup_class() if units is not None: self.dimension_units.update(units) self._register_storages() self.simplifier.update_class_list() if mode == 'w': logger.info("Setup netCDF file and create variables") # add shared scalar dimension for everyone self.create_dimension('scalar', 1) self._initialize() logger.info("Finished setting up netCDF file") elif mode == 'a' or mode == 'r+' or mode == 'r': logger.debug("Restore the dict of units from the storage") # Create a dict of simtk.Unit() instances for all netCDF.Variable() for variable_name in self.variables: variable = self.variables[variable_name] if self.support_simtk_unit: import simtk.unit as u if hasattr(variable, 'unit_simtk'): unit_dict = self.simplifier.from_json( getattr(variable, 'unit_simtk')) if unit_dict is not None: unit = self.simplifier.unit_from_dict(unit_dict) else: unit = self.simplifier.unit_from_dict(u.Unit({})) self.units[str(variable_name)] = unit self.update_delegates() # After we have restored the units we can load objects from the storage self._restore() self.sync()
class DynamicsEngine(StorableNamedObject): ''' Wraps simulation tool (parameters, storage, etc.) Notes ----- Should be considered an abstract class: only its subclasses can be instantiated. ''' FORWARD = 1 BACKWARD = -1 _default_options = {'n_frames_max': None, 'timestep': None} units = { 'length': u.Unit({}), 'velocity': u.Unit({}), 'energy': u.Unit({}) } def __init__(self, options=None, template=None): ''' Create an empty DynamicsEngine object Notes ----- The purpose of an engine is to create trajectories and keep track of the results. The main method is 'generate' to create a trajectory, which is a list of snapshots and then can store the in the associated storage. In the initialization this storage is created as well as the related Trajectory and Snapshot classes are initialized. ''' super(DynamicsEngine, self).__init__() self.template = template # Trajectories need to know the engine as a hack to get the topology. # Better would be a link to the topology directly. This is needed to create # mdtraj.Trajectory() objects # TODO: Remove this and put the logic outside of the engine. The engine in trajectory is only # used to get the solute indices which should depend on the topology anyway # Trajectory.engine = self self._check_options(options) # as default set a newly generated engine as the default engine self.set_as_default() def _check_options(self, options=None): """ This will register all variables in the options dict as a member variable if they are present in either the DynamicsEngine.default_options or this classes default_options, no multiple inheritance is supported! It will use values with the priority in the following order - DynamicsEngine.default_options - self.default_options - self.options (usually not used) - options (function parameter) Parameters are only registered if 1. the variable name is present in the defaults 2. the type matches the one in the defaults 3. for variables with units also the units need to be compatible Parameters ---------- options : dict of { str : value } A dictionary Notes ----- Options are what is necessary to recreate the engine, but not runtime variables or independent variables like the actual initialization status, the runners or an attached storage. If there are non-default options present they will be ignored (no error thrown) """ # start with default options from a dynamics engine my_options = {} okay_options = {} # self.default_options overrides default ones from DynamicsEngine for variable, value in self.default_options.iteritems(): my_options[variable] = value if hasattr(self, 'options') and self.options is not None: # self.options overrides default ones for variable, value in self.options.iteritems(): my_options[variable] = value if options is not None: # given options override even default and already stored ones for variable, value in options.iteritems(): my_options[variable] = value if my_options is not None: for variable, default_value in self.default_options.iteritems(): # create an empty member variable if not yet present if not hasattr(self, variable): okay_options[variable] = None if variable in my_options: if type(my_options[variable]) is type(default_value): if type(my_options[variable]) is u.Unit: if my_options[variable].unit.is_compatible( default_value): okay_options[variable] = my_options[variable] else: raise ValueError( 'Unit of option "' + str(variable) + '" (' + str(my_options[variable].unit) + ') not compatible to "' + str(default_value.unit) + '"') elif type(my_options[variable]) is list: if type(my_options[variable][0]) is type( default_value[0]): okay_options[variable] = my_options[variable] else: raise ValueError('List elements for option "' + str(variable) + '" must be of type "' + str(type(default_value[0])) + '"') else: okay_options[variable] = my_options[variable] elif isinstance(type(my_options[variable]), type(default_value)): okay_options[variable] = my_options[variable] elif default_value is None: okay_options[variable] = my_options[variable] else: raise ValueError('Type of option "' + str(variable) + '" (' + str(type(my_options[variable])) + ') is not "' + str(type(default_value)) + '"') self.options = okay_options else: self.options = {} def __getattr__(self, item): # default is to look for an option and return it's value return self.options[item] @property def topology(self): return self.template.topology @property def n_atoms(self): return self.topology.n_atoms @property def n_spatial(self): return self.topology.n_spatial def to_dict(self): return {'options': self.options, 'template': self.template} def set_as_default(self): paths.EngineMover.engine = self @property def default_options(self): default_options = {} default_options.update(DynamicsEngine._default_options) default_options.update(self._default_options) return default_options def start(self, snapshot=None): if snapshot is not None: self.current_snapshot = snapshot def stop(self, trajectory): """Nothing special needs to be done for direct-control simulations when you hit a stop condition.""" pass def stop_conditions(self, trajectory, continue_conditions=None, trusted=True): """ Test whether we can continue; called by generate a couple of times, so the logic is separated here. Parameters ---------- trajectory : Trajectory the trajectory we've generated so far continue_conditions : list of function(Trajectory) callable function of a 'Trajectory' that returns True or False. If one of these returns False the simulation is stopped. Returns ------- boolean: true if the dynamics should be stopped; false otherwise """ stop = False if continue_conditions is not None: for condition in continue_conditions: can_continue = condition(trajectory, trusted) stop = stop or not can_continue return stop def generate_forward(self, snapshot, ensemble): """ Generate a potential trajectory in ensemble simulating forward in time """ return self.generate(snapshot, ensemble.can_append, direction=+1) def generate_backward(self, snapshot, ensemble): """ Generate a potential trajectory in ensemble simulating forward in time """ return self.generate(snapshot, ensemble.can_prepend, direction=-1) def generate(self, snapshot, running=None, direction=+1): r""" Generate a trajectory consisting of ntau segments of tau_steps in between storage of Snapshots. Parameters ---------- snapshot : Snapshot initial coordinates and velocities in form of a Snapshot object running : (list of) function(Trajectory) callable function of a 'Trajectory' that returns True or False. If one of these returns False the simulation is stopped. direction : -1 or +1 (DynamicsEngine.FORWARD or DynamicsEngine.BACKWARD) If +1 then this will integrate forward, if -1 it will reversed the momenta of the given snapshot and then prepending generated snapshots with reversed momenta. This will generate a _reversed_ trajectory that effectively ends in the initial snapshot Returns ------- trajectory : Trajectory generated trajectory of initial conditions, including initial coordinate set Notes ----- If the returned trajectory has length n_frames_max it can still happen that it stopped because of the stopping criterion. You need to check in that case. """ if direction == 0: raise RuntimeError( 'direction must be positive (FORWARD) or negative (BACKWARD).') try: iter(running) except: running = [running] trajectory = paths.Trajectory() if direction > 0: self.current_snapshot = snapshot elif direction < 0: # backward simulation needs reversed snapshots self.current_snapshot = snapshot.reversed self.start() # Store initial state for each trajectory segment in trajectory. trajectory.append(snapshot) frame = 0 # maybe we should stop before we even begin? stop = self.stop_conditions(trajectory=trajectory, continue_conditions=running, trusted=False) logger.info("Starting trajectory") log_freq = 10 # TODO: set this from a singleton class while stop == False: if self.options.get('n_frames_max', None) is not None: if len(trajectory) >= self.options['n_frames_max']: break # Do integrator x steps snapshot = self.generate_next_frame() frame += 1 if frame % log_freq == 0: logger.info("Through frame: %d", frame) # Store snapshot and add it to the trajectory. Stores also # final frame the last time if direction > 0: trajectory.append(snapshot) elif direction < 0: # We are simulating forward and just build in backwards order trajectory.prepend(snapshot.reversed) # Check if we should stop. If not, continue simulation stop = self.stop_conditions(trajectory=trajectory, continue_conditions=running) # exit the while loop once we must stop, so we call the engine's # stop function (which should manage any end-of-trajectory # cleanup) self.stop(trajectory) logger.info("Finished trajectory, length: %d", frame) return trajectory def generate_next_frame(self): raise NotImplementedError('Next frame generation must be implemented!')
def get_unit(self, dimension): """ Return a simtk.Unit instance from the unit_system the is of the specified dimension, e.g. length, time """ return u.Unit( {self.unit_system.base_units[u.BaseDimension(dimension)]: 1.0})