diff --git a/tracking/monitors.py b/tracking/monitors.py index 5d6f6866b143feb61e5520b9b088ed531a3d1590..078c58a1ef972bb9202055e3e002e8a09577a67c 100644 --- a/tracking/monitors.py +++ b/tracking/monitors.py @@ -1,38 +1,126 @@ # -*- coding: utf-8 -*- """ This module defines the different monitor class which are used to save data -from tracking. +during tracking. @author: Alexis Gamelin -@date: 17/03/2020 +@date: 24/03/2020 + """ import numpy as np import h5py as hp from tracking.element import Element from tracking.particles import Bunch, Beam -from abc import ABCMeta, abstractmethod +from abc import ABCMeta +from mpi4py import MPI class Monitor(Element, metaclass=ABCMeta): """ + Abstract Monitor class used for subclass inheritance to define all the + different kind of monitors objects. + + The Monitor class is based on h5py module to be able to write data on + structured binary files. The class provides a common file where the + different Monitor subclass can write. + + Attributes + ---------- + file : HDF5 file + Common file where all monitors, Monitor subclass elements, write the + saved data. Based on class attribute _file_storage. + file_name : string + Name of the HDF5 file where the data is stored. Based on class + attribute _file_name_storage. + + Methods + ------- + monitor_init(group_name, save_every, buffer_size, total_size, + dict_buffer, dict_file, file_name=None, mpi_mode=True) + Method called to initialize Monitor subclass. + write() + Write data from buffer to the HDF5 file. + to_buffer(object_to_save) + Save data to buffer. + close() + Close the HDF5 file shared by all Monitor subclass, must be called + by at least an instance of a Montior subclass at the end of the + tracking. """ - file_name_storage = [] + _file_name_storage = [] + _file_storage = [] @property def file_name(self): + """Common file where all monitors, Monitor subclass elements, write the + saved data.""" + try: + return self._file_name_storage[0] + except IndexError: + print("The HDF5 file name for monitors is not set.") + raise ValueError + + @property + def file(self): + """Name of the HDF5 file where the data is stored.""" try: - return self.file_name_storage[0] + return self._file_storage[0] except IndexError: - print("File name for monitors not set.") + print("The HDF5 file to store data is not set.") raise ValueError - def monitor_init(self, group_name, save_every, total_size, - buffer_size, dict_buffer, dict_file, file_name=None): + def monitor_init(self, group_name, save_every, buffer_size, total_size, + dict_buffer, dict_file, file_name=None, mpi_mode=True): + """ + Method called to initialize Monitor subclass. + Parameters + ---------- + group_name : string + Name of the HDF5 group in which the data for the current monitor + will be saved. + save_every : int or float + Set the frequency of the save. The data is saved every save_every + call of the montior. + buffer_size : int or float + Size of the save buffer. + total_size : int or float + Total size of the save. The following relationships between the + parameters must exist: + total_size % buffer_size == 0 + number of call to track / save_every == total_size + dict_buffer : dict + Dictionary with keys as the attribute name to save and values as + the shape of the buffer to create to hold the attribute, like + (key.shape, buffer_size) + dict_file : dict + Dictionary with keys as the attribute name to save and values as + the shape of the dataset to create to hold the attribute, like + (key.shape, total_size) + file_name : string, optional + Name of the HDF5 where the data will be stored. Must be specified + the first time a subclass of Monitor is instancied and must be None + the following times. + mpi_mode : bool, optional + If True, open the HDF5 file in parallel mode, which is needed to + allow several cores to write in the same file at the same time. + If False, open the HDF5 file in standard mode. + """ + + # setup and open common file for all monitors if file_name is not None: - if len(self.file_name_storage) == 0: - self.file_name_storage.append(file_name + ".hdf5") + if len(self._file_name_storage) == 0: + self._file_name_storage.append(file_name + ".hdf5") + if len(self._file_storage) == 0: + if mpi_mode == True: + f = hp.File(self.file_name, "w", libver='latest', + driver='mpio', comm=MPI.COMM_WORLD) + else: + f = hp.File(self.file_name, "w", libver='latest') + self._file_storage.append(f) + else: + raise ValueError("File is already open.") else: raise ValueError("File name for monitors is already attributed.") @@ -46,17 +134,19 @@ class Monitor(Element, metaclass=ABCMeta): self.write_count = 0 self.track_count = 0 + # setup attribute buffers from values given in dict_buffer for key, value in dict_buffer.items(): self.__setattr__(key,np.zeros(value)) self.time = np.zeros((self.buffer_size,), dtype=int) + + # create HDF5 groups and datasets to save data from group_name and + # dict_file + self.g = self.file.require_group(self.group_name) + self.g.require_dataset("time", (self.total_size,), dtype=int) + for key, value in dict_file.items(): + self.g.require_dataset(key, value, dtype=float) - with hp.File(self.file_name, "a", libver='latest') as f: - g = f.require_group(self.group_name) - g.require_dataset("time", (self.total_size,), dtype=int) - for key, value in dict_file.items(): - g.require_dataset(key, value, dtype=float) - - + # create a dictionary which slice_dict = {} for key, value in dict_file.items(): slice_dict[key] = [] @@ -65,23 +155,27 @@ class Monitor(Element, metaclass=ABCMeta): self.slice_dict = slice_dict def write(self): - """ - """ + """Write data from buffer to the HDF5 file.""" - with hp.File(self.file_name, "a", libver='latest') as f: - f[self.group_name]["time"][self.write_count*self.buffer_size:( + self.file[self.group_name]["time"][self.write_count*self.buffer_size:( self.write_count+1)*self.buffer_size] = self.time - for key, value in self.dict_buffer.items(): - slice_list = list(self.slice_dict[key]) - slice_list.append(slice(self.write_count*self.buffer_size, - (self.write_count+1)*self.buffer_size)) - slice_tuple = tuple(slice_list) - - f[self.group_name][key][slice_tuple] = self.__getattribute__(key) + for key, value in self.dict_buffer.items(): + slice_list = list(self.slice_dict[key]) + slice_list.append(slice(self.write_count*self.buffer_size, + (self.write_count+1)*self.buffer_size)) + slice_tuple = tuple(slice_list) + self.file[self.group_name][key][slice_tuple] = self.__getattribute__(key) + self.write_count += 1 def to_buffer(self, object_to_save): """ + Save data to buffer. + + Parameters + ---------- + object_to_save : python object + Depends on the Monitor subclass, typically a Beam or Bunch object. """ self.time[self.buffer_count] = self.track_count for key, value in self.dict_buffer.items(): @@ -95,28 +189,72 @@ class Monitor(Element, metaclass=ABCMeta): self.write() self.buffer_count = 0 + def close(self): + """ + Close the HDF5 file shared by all Monitor subclass, must be called + by at least an instance of a Montior subclass at the end of the + tracking. + """ + try: + self.file.close() + except ValueError: + pass + class BunchMonitor(Monitor): """ - Monitor a + Monitor a single bunch and save attributes (mean, std, emit and current). + + Parameters + ---------- + bunch_number : int + Bunch to monitor + file_name : string, optional + Name of the HDF5 where the data will be stored. Must be specified + the first time a subclass of Monitor is instancied and must be None + the following times. + save_every : int or float, optional + Set the frequency of the save. The data is saved every save_every + call of the montior. + buffer_size : int or float, optional + Size of the save buffer. + total_size : int or float, optional + Total size of the save. The following relationships between the + parameters must exist: + total_size % buffer_size == 0 + number of call to track / save_every == total_size + mpi_mode : bool, optional + If True, open the HDF5 file in parallel mode, which is needed to + allow several cores to write in the same file at the same time. + If False, open the HDF5 file in standard mode. + + Methods + ------- + track(object_to_save) + Save data """ - def __init__(self, bunch_number, file_name=None, - save_every=5, buffer_size = 500, total_size = 1e5): + def __init__(self, bunch_number, file_name=None, save_every=5, + buffer_size=500, total_size=2e4, mpi_mode=True): self.bunch_number = bunch_number group_name = "BunchData_" + str(self.bunch_number) - dict_buffer = {"mean":(6,buffer_size), "std":(6,buffer_size), - "emit":(3,buffer_size), "current":(buffer_size,)} - dict_file = {"mean":(6,total_size), "std":(6,total_size), - "emit":(3,total_size), "current":(total_size,)} - self.monitor_init(group_name, save_every, total_size, - buffer_size, dict_buffer, dict_file, file_name) + dict_buffer = {"mean":(6, buffer_size), "std":(6, buffer_size), + "emit":(3, buffer_size), "current":(buffer_size,)} + dict_file = {"mean":(6, total_size), "std":(6, total_size), + "emit":(3, total_size), "current":(total_size,)} + self.monitor_init(group_name, save_every, buffer_size, total_size, + dict_buffer, dict_file, file_name, mpi_mode) self.dict_buffer = dict_buffer self.dict_file = dict_file def track(self, object_to_save): """ + Save data + + Parameters + ---------- + object_to_save : Bunch or Beam object """ if self.track_count % self.save_every == 0: if isinstance(object_to_save, Beam): @@ -134,25 +272,60 @@ class BunchMonitor(Monitor): class PhaseSpaceMonitor(Monitor): """ - Monitor a + Monitor a single bunch and save the full phase space. + + Parameters + ---------- + bunch_number : int + Bunch to monitor + mp_number : int or float + Number of macroparticle in the phase space to save. + file_name : string, optional + Name of the HDF5 where the data will be stored. Must be specified + the first time a subclass of Monitor is instancied and must be None + the following times. + save_every : int or float, optional + Set the frequency of the save. The data is saved every save_every + call of the montior. + buffer_size : int or float, optional + Size of the save buffer. + total_size : int or float, optional + Total size of the save. The following relationships between the + parameters must exist: + total_size % buffer_size == 0 + number of call to track / save_every == total_size + mpi_mode : bool, optional + If True, open the HDF5 file in parallel mode, which is needed to + allow several cores to write in the same file at the same time. + If False, open the HDF5 file in standard mode. + + Methods + ------- + track(object_to_save) + Save data """ - def __init__(self, bunch_number, mp_number, file_name=None, - save_every=1e4, buffer_size = 1, total_size = 100): + def __init__(self, bunch_number, mp_number, file_name=None, save_every=1e3, + buffer_size=10, total_size=100, mpi_mode=True): self.bunch_number = bunch_number self.mp_number = int(mp_number) group_name = "PhaseSpaceData_" + str(self.bunch_number) dict_buffer = {"particles":(self.mp_number, 6, buffer_size)} dict_file = {"particles":(self.mp_number, 6, total_size)} - self.monitor_init(group_name, save_every, total_size, - buffer_size, dict_buffer, dict_file, file_name) + self.monitor_init(group_name, save_every, buffer_size, total_size, + dict_buffer, dict_file, file_name, mpi_mode) self.dict_buffer = dict_buffer self.dict_file = dict_file def track(self, object_to_save): """ + Save data + + Parameters + ---------- + object_to_save : Bunch or Beam object """ if self.track_count % self.save_every == 0: @@ -167,3 +340,124 @@ class PhaseSpaceMonitor(Monitor): else: raise TypeError("object_to_save should be a Beam or Bunch object.") self.track_count += 1 + + +class BeamMonitor(Monitor): + """ + Monitor the full beam and save each bunch attributes (mean, std, emit and + current). + + Parameters + ---------- + file_name : string, optional + Name of the HDF5 where the data will be stored. Must be specified + the first time a subclass of Monitor is instancied and must be None + the following times. + save_every : int or float, optional + Set the frequency of the save. The data is saved every save_every + call of the montior. + buffer_size : int or float, optional + Size of the save buffer. + total_size : int or float, optional + Total size of the save. The following relationships between the + parameters must exist: + total_size % buffer_size == 0 + number of call to track / save_every == total_size + mpi_mode : bool, optional + If True, open the HDF5 file in parallel mode, which is needed to + allow several cores to write in the same file at the same time. + If False, open the HDF5 file in standard mode. + + Methods + ------- + track(beam) + Save data + """ + + def __init__(self, file_name=None, save_every=5, buffer_size=500, + total_size=2e4, mpi_mode=True): + + group_name = "Beam" + dict_buffer = {} + dict_file = {} + self.monitor_init(group_name, save_every, buffer_size, total_size, + dict_buffer, dict_file, file_name, mpi_mode) + + self.mean = np.zeros((6, self.buffer_size), dtype=float) + self.std = np.zeros((6, self.buffer_size), dtype=float) + self.emit = np.zeros((3, self.buffer_size), dtype=float) + self.current = np.zeros((self.buffer_size,), dtype=float) + + self.g.require_dataset("mean", (6, 416, self.total_size,), dtype=float) + self.g.require_dataset("std", (6, 416, self.total_size,), dtype=float) + self.g.require_dataset("emit", (3, 416, self.total_size,), dtype=float) + self.g.require_dataset("current", (416, self.total_size,), dtype=float) + + def track(self, beam): + """ + Save data + + Parameters + ---------- + beam : Beam object + """ + if self.track_count % self.save_every == 0: + if (beam.mpi_switch == True): + self.to_buffer(beam[beam.mpi.bunch_num], beam.mpi.bunch_num) + else: + raise NotImplementedError + + self.track_count += 1 + + def to_buffer(self, bunch, bunch_num): + """ + Save data to buffer. + + Parameters + ---------- + bunch : Bunch object + bunch_num : int + """ + + self.time[self.buffer_count] = self.track_count + self.mean[:, self.buffer_count] = bunch.mean + self.std[:, self.buffer_count] = bunch.std + self.emit[:, self.buffer_count] = bunch.emit + self.current[self.buffer_count] = bunch.current + + self.buffer_count += 1 + + if self.buffer_count == self.buffer_size: + self.write(bunch_num) + self.buffer_count = 0 + + def write(self, bunch_num): + """ + Write data from buffer to the HDF5 file. + + Parameters + ---------- + bunch_num : int + """ + self.file[self.group_name]["time"][self.write_count*self.buffer_size:( + self.write_count+1)*self.buffer_size] = self.time + + self.file[self.group_name]["mean"][:, bunch_num, + self.write_count*self.buffer_size:(self.write_count+1) * + self.buffer_size] = self.mean + + self.file[self.group_name]["std"][:, bunch_num, + self.write_count*self.buffer_size:(self.write_count+1) * + self.buffer_size] = self.std + + self.file[self.group_name]["emit"][:, bunch_num, + self.write_count*self.buffer_size:(self.write_count+1) * + self.buffer_size] = self.emit + + self.file[self.group_name]["current"][bunch_num, + self.write_count*self.buffer_size:(self.write_count+1) * + self.buffer_size] = self.current + + self.write_count += 1 + + \ No newline at end of file