diff --git a/HISTORY.rst b/HISTORY.rst index b2d7945..8a8ca66 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -296,4 +296,9 @@ History 0.12.6 (2024-07-11) ------------------- -* transform3D_viewer improved \ No newline at end of file +* transform3D_viewer improved + +0.12.7 (2024-07-11) +------------------- +* some of the names of the funcitons are mroe accurate now +* plot now takes arguments that plt.plot takes and a bug has been fixed there! \ No newline at end of file diff --git a/lognflow/__init__.py b/lognflow/__init__.py index 2f17c59..2a90968 100644 --- a/lognflow/__init__.py +++ b/lognflow/__init__.py @@ -2,7 +2,7 @@ __author__ = 'Alireza Sadri' __email__ = 'arsadri@gmail.com' -__version__ = '0.12.6' +__version__ = '0.12.7' from .lognflow import lognflow from .logviewer import logviewer diff --git a/lognflow/lognflow.py b/lognflow/lognflow.py index 80ec61c..e668822 100644 --- a/lognflow/lognflow.py +++ b/lognflow/lognflow.py @@ -188,6 +188,7 @@ def __init__(self, new_log_dir_found = True else: self._init_time = time.time() + self.logs_root = logs_root self.log_dir_provided = False else: self.log_dir_provided = True @@ -207,25 +208,46 @@ def __init__(self, self.enabled = True self.counted_vars = {} - self.log_text = self.text - self.log_text_flush = self.text_flush - self.log_var = self.record - self.log_var_flush = self.record_flush - self.log_plot = self.plot - self.log_hist = self.hist - self.log_scatter3 = self.scatter3 - self.log_surface = self.surface - self.log_hexbin = self.hexbin - self.log_imshow = self.imshow - self.imshow_by_subplots = self.imshow_subplots - self.log_imshow_series = self.imshow_series - self.log_images_in_pdf = self.images_in_pdf - self.log_plt = self.savefig - self.log_torch_dict = self.save_torch - self.log_single = self.save - self.get_single = self.load - self.get_var = self.get_record - self.get_torch_dict = self.load_torch + #all depricated + self.log_text = self.text + self.log_text_flush = self.text_flush + self.log_var = self.record + self.log_var_flush = self.record_flush + self.log_plot = self.plot + self.log_hist = self.hist + self.log_scatter3 = self.scatter3 + self.log_surface = self.surface + self.log_hexbin = self.hexbin + self.log_imshow = self.imshow + self.log_imshow_by_subplots = self.imshow_subplots + self.log_imshow_series = self.imshow_series + self.log_images_in_pdf = self.images_to_pdf + self.log_plt = self.savefig + self.log_torch_dict = self.save_torch + self.log_single = self.save + self.get_single = self.load + self.get_var = self.get_record + self.get_torch_dict = self.load_torch + + def assert_log_dir(self): + if not self.log_dir.is_dir(): + print('~'*60) + if self.log_dir_provided: + print(f'lognflow.logdir: No such directory: ') + print(self.log_dir) + elif self.logs_root.is_dir(): + self.log_dir = self.logs_root + print('lognflow Warning: You read from the provided logs_root:') + print(self.logs_root) + print('to read from a log, use log_dir as the input argument:') + print(f'logger = lognflow(log_dir = {self.log_dir}') + print('I will assume that this logs_root is log_dir from now on') + else: + print('You should provide log_dir when initializing lognflow ' + 'if you wish to read the stored data first as follows:') + print(f'logger = lognflow(log_dir = pathlib.Path(STORAGE_DIR)') + print('~'*60) + assert self.log_dir.is_dir() def disable(self): self.enabled = False @@ -254,7 +276,6 @@ def file_from_name(self, parameter_name): """ return self.log_dir / parameter_name - def copy(self, parameter_name = None, source = None, suffix = None, time_tag = False): """ copy into a new file @@ -374,11 +395,11 @@ def rename(self, new_name:str, append: bool = False): curr_textinlog.log_fpath = \ self.log_dir /curr_textinlog.log_fpath.name except: - self.log_text(None, 'Could not rename the log_dir from:') - self.log_text(None, f'{self.log_dir.name}') - self.log_text(None, 'into:') - self.log_text(None, f'{new_name}') - self.log_text(None, 'Most probably a file was open.') + self.text(None, 'Could not rename the log_dir from:') + self.text(None, f'{self.log_dir.name}') + self.text(None, 'into:') + self.text(None, f'{new_name}') + self.text(None, 'Most probably a file was open.') return self.log_dir def _param_dir_name_suffix(self, parameter_name: str, suffix: str = None): @@ -733,13 +754,13 @@ def record(self, parameter_name: str, parameter_value, try: time_array[curr_index] = self.time_stamp except: - self.log_text( + self.text( self.log_name, f'current index {curr_index} cannot be used in the logger') if(parameter_value.shape == data_array[curr_index].shape): data_array[curr_index] = parameter_value else: - self.log_text( + self.text( self.log_name, f'Shape of variable {log_dirnamesuffix} cannot change shape '\ f'from {data_array[curr_index].shape} '\ @@ -871,11 +892,15 @@ def save(self, parameter_name: str, elif(suffix == 'mat'): from scipy.io import savemat if(mat_field is None): - mat_field = param_name - savemat(fpath, {f'{mat_field}':parameter_value}) + if isinstance(parameter_value, dict): + savemat(fpath, parameter_value) + else: + mat_field = param_name + if(mat_field is not None): + savemat(fpath, {f'{mat_field}':parameter_value}) elif(suffix == 'torch'): from torch import save as torch_save - torch_save(parameter_value.state_dict(), fpath) + torch_save(parameter_value, fpath) else: with open(fpath,'a') as fdata: fdata.write(str(parameter_value)) @@ -916,17 +941,20 @@ def savefig(self, except: if(close_plt): plt.close() - self.log_text( + self.text( None, f'Cannot save the plt instance {parameter_name}.') return None def plot(self, parameter_name: str, - parameter_value_list, - x_values = None, - image_format='jpg', dpi=1200, title = None, - time_tag: bool = None, - return_figure = False, - **kwargs): + parameter_value_list, + *plt_plot_args, + x_values = None, + image_format='jpg', + dpi=1200, + title = None, + time_tag: bool = None, + return_figure = False, + **kwargs): """log a single plot If you have a numpy array or a list of arrays (or indexable by first dimension, an array of 1D arrays), use this to log a plot @@ -961,22 +989,24 @@ def plot(self, parameter_name: str, if( not( (len(x_values) == len(parameter_value_list)) | \ (len(x_values) == 1) )): - self.log_text( + self.text( self.log_name, f'x_values for {parameter_name} should have'\ + ' length of 1 or the same as parameters list.') raise ValueError + fig = plt.figure() + ax = fig.add_subplot(111) for list_cnt, parameter_value in enumerate(parameter_value_list): - fig = plt.figure() - ax = fig.add_subplot(111) if(x_values is None): - ax.plot(parameter_value, **kwargs) + ax.plot(parameter_value, *plt_plot_args, **kwargs) else: if(len(x_values) == len(parameter_value)): - ax.plot(x_values[list_cnt], parameter_value, **kwargs) + ax.plot(x_values[list_cnt], parameter_value, + *plt_plot_args, **kwargs) else: - ax.plot(x_values[0], parameter_value, **kwargs) + ax.plot(x_values[0], parameter_value, + *plt_plot_args, **kwargs) if title is not None: ax.set_title(title) @@ -1076,7 +1106,7 @@ def scatter3(self, parameter_name: str, if data_N_by_3.shape[0] == 3: if data_N_by_3.shape[1] != 3: data_N_by_3 = data_N_by_3.T - self.log_text( + self.text( None, 'lognflow.log_scatter3> input dataset is transposed.') fig_ax_opt_stack = plt_scatter3(data_N_by_3, title = title, elev_list = elev_list, azim_list = azim_list, @@ -1240,7 +1270,7 @@ def imshow(self, else: return fig, ax else: - self.log_text( + self.text( self.log_name, f'Cannot imshow variable {parameter_name} with shape' + \ f'{parameter_value.shape}') @@ -1392,7 +1422,7 @@ def imshow_series(self, else: return fig, ax - def images_in_pdf(self, + def images_to_pdf(self, parameter_name: str, parameter_value: list, time_tag: bool = None, @@ -1426,7 +1456,7 @@ def variables_to_pdf(self, dpi = 1200, **kwargs): images = self.logged.get_stack_from_names(parameter_value) - self.log_images_in_pdf( + self.images_to_pdf( parameter_name, images, time_tag, dpi, **kwargs) def log_confusion_matrix(self, @@ -1585,43 +1615,30 @@ def __call__(self, *args, **kwargs): logger('Hello lognflow') The text (str(...)) will be passed to the main log text file. """ - fpath = self.log_text(None, *args, **kwargs) + fpath = self.text(None, *args, **kwargs) self.flush_all() return fpath #towards supporting all that logging supports def debug(self, text_to_log): - self.log_text('debug', text_to_log, time_tag = False) + self.text('debug', text_to_log, time_tag = False) def info(self): - self.log_text('info', text_to_log, time_tag = False) + self.text('info', text_to_log, time_tag = False) def warning(self): - self.log_text('warning', text_to_log, time_tag = False) + self.text('warning', text_to_log, time_tag = False) def error(self): - self.log_text('error', text_to_log, time_tag = False) + self.text('error', text_to_log, time_tag = False) def critical(self): - self.log_text('critical', text_to_log, time_tag = False) + self.text('critical', text_to_log, time_tag = False) def exception(self): - self.log_text('exception', text_to_log, time_tag = False) - - def assert_log_dir(self): - if not self.log_dir.is_dir(): - print('~'*60) - if self.log_dir_provided: - print(f'lognflow.logdir: No such directory: ') - print(self.log_dir) - else: - print('You should provide log_dir when initializing lognflow ' - 'if you wish to read the stored data first as follows:') - print(f'logger = lognflow(log_dir = {self.log_dir.parent}') - print('~'*60) - assert self.log_dir.is_dir() + self.text('exception', text_to_log, time_tag = False) def save_torch(self, name, x): if isinstance(x, dict): for key in x.keys(): log_dict(name+'/'+key, x[key]) else: - self.log_single(name, x.detach().cpu().numpy()) + self.save(name, x.detach().cpu().numpy()) def load_torch(self, name): self.assert_log_dir() @@ -1790,7 +1807,7 @@ def get_text(self, log_name='main_log', flist = None, suffix = 'txt', txt = txt[0] return txt - def _get_single(self, var_name, file_index = None, + def _load(self, var_name, file_index = None, suffix = None, read_func = None, verbose = False): """ get a single variable return the value of a saved variable. @@ -1823,22 +1840,22 @@ def _get_single(self, var_name, file_index = None, else: if file_index is not None: if verbose: - self.log_text(None, + self.text(None, f'There are {len(flist)} files, logged with' + f' name {var_name}.' + f' The given index is {file_index}.') var_path = flist[file_index] else: - self.log_text(None, '-'*60) - self.log_text(None, + self.text(None, '-'*60) + self.text(None, f'There are {len(flist)} files, logged with' + f' name {var_name} but the index is not given.') - self.log_text(None, '-'*60) + self.text(None, '-'*60) return None if(var_path.is_file()): if verbose: - self.log_text(None, f'Loading {var_path}') + self.text(None, f'Loading {var_path}') if read_func is not None: return (read_func(var_path), var_path) if(var_path.suffix == '.npz'): @@ -1881,7 +1898,7 @@ def _get_single(self, var_name, file_index = None, var_path = None if (var_path is None) & verbose: - self.log_text(None, f'Looking for {var_name} failed. ' + \ + self.text(None, f'Looking for {var_name} failed. ' + \ f'{var_path} is not in: {self.log_dir}') return None, None @@ -1909,7 +1926,7 @@ def load(self, var_name, file_index = -1, Also when reading a npz except if it is made by log_var """ self.assert_log_dir() - get_single_data, fpath = self._get_single( + get_single_data, fpath = self._load( var_name = var_name, file_index = file_index, suffix = suffix, read_func = read_func, verbose = verbose) if return_fpath: @@ -1974,11 +1991,11 @@ def get_stack_from_files(self, read_func(flist[0]) except Exception as e: if flist[0].is_file(): - self.log_text(None, + self.text(None, f'lognflow: The data file {flist[0]} could not be read.' 'Please provide a read_function for this file.') else: - self.log_text( + self.text( None, f'File {flist[0]} does not exist.') raise e dataset = [read_func(fpath) for fpath in flist] @@ -2044,7 +2061,7 @@ def replace_time_with_index(self, var_name, verbose = False): var_dir = var_dir.parent flist = list(var_dir.glob(f'{var_fname}')) if (len(flist) == 0) & (not ('*' in var_fname)): - self.log_text(None, + self.text(None, 'lognflow, replace_time_with_index:' +\ 'the given pattern has no * and no files were found') if flist: @@ -2052,20 +2069,20 @@ def replace_time_with_index(self, var_name, verbose = False): fcnt_width = len(str(len(flist))) for fcnt, fpath in enumerate(flist): if verbose: - self.log_text(None, f'Changing {flist[fcnt].name}') + self.text(None, f'Changing {flist[fcnt].name}') fname_new = fpath.name.split(fpath.stem.split('_')[-1]) fname_new = \ fname_new[0] + f'{fcnt:0{fcnt_width}d}' + fname_new[1] fpath_new = flist[fcnt].parent / fname_new if verbose: - self.log_text(None, f'To {fpath_new.name}') + self.text(None, f'To {fpath_new.name}') flist[fcnt].rename(fpath_new) def __del__(self): try: self.flush_all() except: - print('lognflow: couldnt flush all logs') + pass def __repr__(self): return f'{self.log_dir}' diff --git a/lognflow/plt_utils.py b/lognflow/plt_utils.py index bb2b4b2..191f27f 100644 --- a/lognflow/plt_utils.py +++ b/lognflow/plt_utils.py @@ -18,10 +18,8 @@ def complex2hsv(data_complex, vmin=None, vmax=None): sx, sy = data_complex.shape data_abs = np.abs(data_complex) - if vmin is None: - vmin = data_abs.min() - if vmax is None: - vmax = data_abs.max() + if vmin is None: vmin = data_abs.min() + if vmax is None: vmax = data_abs.max() sat = (data_abs - vmin) / (vmax - vmin) data_angle = np.angle(data_complex) % (2 * np.pi) hue = data_angle / (2 * np.pi) @@ -92,7 +90,7 @@ def complex2hsv_colorbar( return fig, ax -def plt_colorbar(mappable, colorbar_aspect=2, colorbar_pad_fraction=0.05): +def plt_colorbar(mappable, colorbar_aspect=3, colorbar_pad_fraction=0.05): """ Add a colorbar to the current axis with consistent width. @@ -250,6 +248,8 @@ def plt_imshow(img, portrait = None, complex_type = 'abs_angle', **kwargs): + vmin = kwargs['vmin'] if 'vmin' in kwargs else None + vmax = kwargs['vmax'] if 'vmax' in kwargs else None if(not np.iscomplexobj(img)): fig, ax = plt.subplots() im = ax.imshow(img, cmap = cmap, **kwargs) @@ -259,12 +259,15 @@ def plt_imshow(img, plt.setp(ax, xticks=[], yticks=[]) else: if (cmap == 'complex') | (complex_type == 'complex'): - # Convert complex data to RGB - complex_image, data_abs, data_angle = complex2hsv(img) + # Convert complex data to RGB + + complex_image, data_abs, data_angle = complex2hsv( + img, vmin = vmin, vmax = vmax) # Calculate min and max angles - vmin = data_abs.min() - vmax = data_abs.max() + if vmin is None: vmin = data_abs.min() + if vmax is None: vmax = data_abs.max() + try: min_angle = data_angle[data_abs > 0].min() except: @@ -622,7 +625,7 @@ def imshow_series(list_of_stacks, list_of_titles_columns = None, list_of_titles_rows = None, fontsize = None, - transpose = False, + transpose = True, ): """ imshow a stack of images or sets of images in a shelf, input must be a list or array of images @@ -765,6 +768,12 @@ def imshow_by_subplots( figsize (tuple): Size of the figure. remove_axis_ticks (bool): Whether to remove axis ticks. Default is True. """ + try: + dims = stack.shape + if len(dims) == 2: + dims = [dims] + except: pass + if grid_locations is None: N = len(stack) if frame_shape is None: @@ -782,10 +791,12 @@ def imshow_by_subplots( figsize = 10 * frame_shape / frame_shape.max() fig = plt.figure(figsize=figsize) - + # stack_shape_0 = [image.shape[0] for image in stack] + # image_blank_shape[0] = ... for i, (image, location) in enumerate(zip(stack, grid_locations)): ax = fig.add_subplot(location[0], location[1], location[2]) - + if image is None: + continue if 'cmap' in kwargs: cax = ax.imshow(image, **kwargs) else: diff --git a/setup.cfg b/setup.cfg index b8a03f8..c8471e9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.12.5 +current_version = 0.12.6 commit = True tag = True diff --git a/setup.py b/setup.py index 22ed6a3..74aa074 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ __author__ = 'Alireza Sadri' __email__ = 'arsadri@gmail.com' -__version__ = '0.12.6' +__version__ = '0.12.7' with open('README.rst') as readme_file: readme = readme_file.read() diff --git a/tests/test_lognflow.py b/tests/test_lognflow.py index cd81c21..354f494 100644 --- a/tests/test_lognflow.py +++ b/tests/test_lognflow.py @@ -121,6 +121,14 @@ def test_log_animation(): logger('This is a test for log_animation') logger.log_animation('var1',var1) +def test_save_matlab(): + logger = lognflow(temp_dir) + logger('This is a test for saving MATLAB files using a dictionary') + data_to_save = dict(data1 = np.random.rand(3,3), + data2 = 'test', + data3 = [1, 2, 4]) + logger.save('MATLAB_test.mat', data_to_save) + def test_save(): var1 = np.random.rand(100) @@ -142,11 +150,9 @@ def test_plot(): logger = lognflow(temp_dir) logger('This is a test for plot') - logger.plot(parameter_name = 'var1', - parameter_value_list = var1) + logger.plot('var1', var1) - logger.plot(parameter_name = 'vars', - parameter_value_list = [var1, var2, var3]) + logger.plot('vars', [var1, var2, var3], '-*') def test_hist(): var1 = np.random.rand(10000) @@ -360,7 +366,7 @@ def test_log_images_to_pdf(): logger.imshow('im1', np.random.randn(20, 40)) images = logger.get_stack_from_names('im1*.*') - logger.images_in_pdf( + logger.images_to_pdf( 'im1_all', parameter_value = images, time_tag = False) def test_variables_to_pdf(): @@ -495,6 +501,8 @@ def test_depricated_logviewer(): temp_dir = select_directory() #---------------------------# #tests about reading back + test_plot(); exit() + test_save_matlab() test_save() test_scatter3() test_log_animation() @@ -528,7 +536,6 @@ def test_depricated_logviewer(): test_surface() test_lognflow_conflict_in_names() test_rename() - test_plot() test_logger() test_log_flush_period() test_record_without_time_stamp()