class AsymmetricMolecule: """ Python interface to Brandon's fast rigid rotor simulator. This provides a way to control the program to a certain degree, with the main user input being what the rotational constants of the molecule are, and what degree of verbosity in the program output. The main methods a user will call are `simulate` and `format_result`: the former will call the routines to produce a catalog, and the latter will format the results of the catalog (which are ctypes) into a more user-friendly data type, either as NumPy arrays or Pandas Dataframes. """ def __init__(self, **kwargs): self.seed = None self.niter = 100 self.constants = [10000., 5000., 3000.] self.temperature = c_double(2.0) self.dipoles = [1.0,1.0,1.0] self.verbose = False # These are static paths that a user can overwrite module_path = Path(__file__).parent self.lib_path = module_path.joinpath("Fitter.so") self.et_path = module_path.joinpath("etau.dat") self.cat_dict_path = module_path.joinpath("base_cat_dict.txt") self.cat_path = module_path.joinpath("base_cat.txt") # Update the parameters with user defined settings self.__dict__.update(**kwargs) # Seed the random number generator, unless user provides a value np.random.seed(self.seed) # Set up a whole bunch of Ctypes stuff self._load_library() self._init_pointers() self._load_tables() def _load_tables(self): """ Private method to load the precomputed tables into memory. """ names = ["etau", "catdict", "catalog"] self.string_buffers = dict() for name, path in zip(names, [self.et_path, self.cat_dict_path, self.cat_path]): check_path = Path(path) if not check_path.exists(): raise Exception(f"{path} table not found!") self.string_buffers[name] = create_string_buffer(bytes(check_path)) # Load the base catalog dictionary self.FitterLib.Load_Base_Catalog_Dictionary( self.string_buffers["catdict"], byref(self.levels), self._verbose ) # Load the base catalog self._statecount = self.FitterLib.Load_Base_Catalog( self.string_buffers["catalog"], byref(self.catalog), self._verbose ) # Load Etau table self.FitterLib.Load_ETau_File2( self.string_buffers["etau"], byref(self.et), byref(self._etstatecount), self.verbose ) if (self._etstatecount != self._statecount): print ("Warning: Catalog and Dictionary have different ") def _load_library(self): """ Private method to use ctypes to load in Brandon's program as a static library to access its functions. The way that this function is written is more verbose, but it is intended to give useful errors when the library is not found/missing. """ if not self.lib_path.exists(): raise Exception("Fitter static library not found; is it compiled?") else: # Load the statically compiled library self.FitterLib = CDLL(self.lib_path) def _init_pointers(self): """ Private method to set up the ctypes and pointers prior to spinning up the simulations. """ self._statepoints = c_int(0) self._statecount = c_int(0) self._etstatecount = c_int(0) self._verbose = c_int(int(self.verbose)) self._constantsType = c_double * 3 self.levels = POINTER(Level)() self.catalog = POINTER(Transition)() self.et = ETauStruct() def get_Intensity(self): """ Method to set up the ctypes and pointers prior to spinning up the simulations. """ dipoles = self._constantsType(self.dipoles) self.FitterLib.Calculate_Intensities ( BaseCatalog, #CatalogTransitions, BaseDict, 3.0, Dipoles);( self.catalog, self._statecount, self.levels, self.temperature, dipoles )
class AsymmetricMolecule: """ Python interface to Brandon's fast rigid rotor simulator. This provides a way to control the program to a certain degree, with the main user input being what the rotational constants of the molecule are, and what degree of verbosity in the program output. The main methods a user will call are `simulate` and `format_result`: the former will call the routines to produce a catalog, and the latter will format the results of the catalog (which are ctypes) into a more user-friendly data type, either as NumPy arrays or Pandas Dataframes. """ def __init__(self, **kwargs): self.seed = None self.niter = 100 self.constants = [10000., 5000., 3000.] self.temperature = c_double(2.0) self.dipoles = [1.0, 1.0, 1.0] self.verbose = False # These are static paths that a user can overwrite module_path = Path(__file__).parent self.lib_path = module_path.joinpath("Fitter.so") self.et_path = module_path.joinpath("etau.dat") self.cat_dict_path = module_path.joinpath("base_cat_dict.txt") self.cat_path = module_path.joinpath("base_cat.txt") # Update the parameters with user defined settings self.__dict__.update(**kwargs) # Seed the random number generator, unless user provides a value np.random.seed(self.seed) # Set up a whole bunch of Ctypes stuff self._load_library() self._init_pointers() self._load_tables() def _load_tables(self): """ Private method to load the precomputed tables into memory. """ names = ["etau", "catdict", "catalog"] self.string_buffers = dict() for name, path in zip( names, [self.et_path, self.cat_dict_path, self.cat_path]): check_path = Path(path) if not check_path.exists(): raise Exception(f"{path} table not found!") self.string_buffers[name] = create_string_buffer(bytes(check_path)) # Load the base catalog dictionary self._statecount = self.FitterLib.Load_Base_Catalog_Dictionary( self.string_buffers["catdict"], byref(self.levels), self._verbose) # Load the base catalog self._transitioncount = self.FitterLib.Load_Base_Catalog( self.string_buffers["catalog"], byref(self.catalog), self._verbose) # Load Etau table self.FitterLib.Load_ETau_File2(self.string_buffers["etau"], byref(self.et), byref(self._etstatecount), self.verbose) if (self._etstatecount.value != self._statecount): print( "Warning: Catalog and Dictionary have different numbers of states" ) print(self._etstatecount, self._statecount) def _load_library(self): """ Private method to use ctypes to load in Brandon's program as a static library to access its functions. The way that this function is written is more verbose, but it is intended to give useful errors when the library is not found/missing. """ if not self.lib_path.exists(): raise Exception("Fitter static library not found; is it compiled?") else: # Load the statically compiled library self.FitterLib = CDLL(self.lib_path) def _init_pointers(self): """ Private method to set up the ctypes and pointers prior to spinning up the simulations. """ self._statepoints = c_int(0) self._transitioncount = c_int(0) self._etstatecount = c_int(0) self._verbose = c_int(int(self.verbose)) self._constantsType = c_double * 3 self.levels = POINTER(Level)() self.catalog = POINTER(Transition)() self.et = ETauStruct() def get_Intensity(self): """ Method to set up the ctypes and pointers prior to spinning up the simulations. """ dipoles = self._constantsType(self.dipoles) self.FitterLib.Calculate_Intensities( BaseCatalog, #CatalogTransitions, BaseDict, 3.0, Dipoles);( self.catalog, self._transitioncount, self.levels, self.temperature, dipoles) def simulate(self, constants=None): """ Function to run the program to generate a catalog based on a set of rigid rotor constants. Parameters ---------- constants : float or None, optional 3-element iterable consisting of floats, corresponds to the rotational constants in MHz. """ if constants is None: constants = self.constants # Make sure exactly three rotational constants are given assert len(constants) == 3 # Ensure that the values are sorted in the correct order constants = list(np.sort(constants))[::-1] if self.verbose: print(f"Simulating with A, B, C: {constants}") print("Debugging information:") print(f"StateCount: {self._transitioncount}") c_constants = self._constantsType(*constants) # Run the catalog simulation self.FitterLib.Get_Catalog(self.catalog, c_constants, self._transitioncount, self._verbose, self.et, self.levels) def simulate_batch(self, niter=None, pandas_dataframe=True, **kwargs): """ Function to run a batch of simulations. The constants are randomly generated between runs, although the random seed is only changed when this class is created so be aware when making comparisons. Kwargs are passed into the constant generation, and so a user can provide upper and lower boundary values for A, B, C. Parameters ---------- niter : int or None, optional If a value is provided by the user, sets the number of simulations to run. pandas_dataframe : bool, optional If True, the results that are returned are organized as Pandas dataframes. kwargs Kwargs are passed into the random constants generation. Returns ------- results : list Nested list of two-tuple, corresponding to the constants used """ if not niter: niter = self.niter results = list() for _ in range(niter): constants = self.random_constants(**kwargs) self.simulate(constants) results.append((constants, self.format_results(pandas_dataframe))) return results def random_constants(self, lower=1000., upper=10000.): """ Function to generate three rotational constants with upper and lower boundary values, based on a uniform distribution. The ordering of the constants returned is A, B, C Parameters ---------- lower, upper : float, optional Lower and upper limits to the random number generation, in MHz. Returns ------- constants : NumPy 1D array """ constants = np.random.uniform(lower, upper, 3) constants = np.sort(constants)[::-1] return constants def format_results(self, pandas_table=True): """ Function to convert the C-types catalog into Python data types, either as NumPy arrays or as a Pandas dataframe. If the NumPy array is returned, the headings go as: frequency, upper-index, lower-index, type J', Ka', Kc', J'', Ka'', Kc'' Parameters ---------- pandas_table : bool, optional If True (default), the output returned is a Pandas dataframe. Returns ------- If pandas_table is True, a pandas dataframe, otherwise a NumPy array. """ table = list() # This loop is explicitly done non-Pythonically; iterating over a # Pointer will continue until it breaks itself in a segfault, and # the attribute `_transitioncount` holds the correct number of transitions. for index in range(self._transitioncount): transition = self.catalog[index] ustate = self.levels[transition.Upper] lstate = self.levels[transition.Lower] data = [ float(transition.Frequency), int(transition.Upper), int(transition.Lower), int(transition.Type), int(ustate.J), int(ustate.Ka), int(ustate.Kc), int(lstate.J), int(lstate.Ka), int(lstate.Kc) ] table.append(data) if pandas_table: return pd.DataFrame(table, columns=[ "frequency", "upper-index", "lower-index", "type", "J'", "Ka'", "Kc'", "J''", "Ka''", "Kc''" ]) else: return np.array(table)