Source code for prsctrl.data.util

from .functions import eV_to_nm
from .prsdata import *
from .plot import plot_rta, plot_spectrum
from .calc import calc_absorption_data
from devctrl.utility.file_io import listdir_abs
from .check import sanity_check_data_gen_report

import os
import shutil
import re

[docs] def get_data_name(s, date=False, time=False, sample_type=True, sample_batch=True, info=True, mode=True, replace_underscore=True): r = r"(\d{4}-\d{2}-\d{2}_)?(\d{2}-\d{2}_)?([^_]+_)([^_]+_)?(.*?)(?:(-...))?$" match = re.match(s, r) if match: name = "" if date: name += match.groups()[0] if time: name += match.groups()[1] if sample_type: name += match.groups()[2] if sample_batch: name += match.groups()[3] if info: name += match.groups()[4] if mode: name += match.groups()[5] else: log.debug(f"Failed to match data name: '{s}'") name = s if replace_underscore: name = name.replace("_", " ") return name
[docs] def get_data_name_template(s, template="<sample_type> (<sample_batch>)", replace_underscore=True): r = r"(\d{4}-\d{2}-\d{2}_)?(\d{2}-\d{2}_)?([^_]+_)([^_]+_?)?(.*?)(?:-(...))?$" match = re.match(r, s) if match: repls = [ "<date>", "<time>", "<sample_type>", "<sample_batch>", "<info>", "<mode>"] groups = [(g or "").removesuffix("_") for g in match.groups()] name = template for i, repl in enumerate(repls): name = name.replace(repl, groups[i] or "") else: log.warning(f"Failed to match data name: '{s}'") name = s if replace_underscore: name = name.replace("_", " ") return name
# # LOAD/FIND DATA #
[docs] def load_rta(parent_data_dir, norm_data_dir=None, force_recalculate_absorption=False, **abs_prsdata_kw): """ Load reflection, transmission and absorption data from the path to the parent directory. The parent directoy must contain two directories with the same as itself, with suffixes -ref and -tra. The absorption data may exist (suffix -abs) or not, in the latter case it will be calculated from ref and tra. :param force_recalculate_absoprtion: Calculate absorption data from ref and tra even if it exists :param norm_data_dir: Data directory with reference data for normalizing the absorption spectrum :return: Reflection, Transmission, Absorptíon data """ dirname = os.path.basename(parent_data_dir.removesuffix("/")) ref_data = PrsData(os.path.join(parent_data_dir, dirname + "-" + REFLECTION)) tra_data = PrsData(os.path.join(parent_data_dir, dirname + "-" + TRANSMISSION)) abs_dir = os.path.join(parent_data_dir, dirname + "-" + ABSORPTION) if os.path.isdir(abs_dir) and not force_recalculate_absorption: abs_data = PrsData(abs_dir) else: if norm_data_dir: norm_data = PrsData(norm_data_dir) else: norm_data = None abs_data = calc_absorption_data(ref_data=ref_data, tra_data=tra_data, norm_data=norm_data, **abs_prsdata_kw) return ref_data, tra_data, abs_data
[docs] def find_all_data_dirs(base_dir) -> list[str]: """ Recursively find all PrsData directories in base_dir """ data_dirs = [] is_data_dir = False for file in listdir_abs(base_dir): if os.path.isfile(file) and (file.endswith(FULL_DATA_SUFFIX) or file.endswith(FULL_DATA_SUFFIX+".gz")): is_data_dir = True elif os.path.isfile(file) and file.startswith(PARTIAL_PREFIX): is_data_dir = True elif os.path.isdir(file): data_dirs += find_all_data_dirs(file) if is_data_dir: data_dirs.append(base_dir) return data_dirs
# # ABSOPRTION #
[docs] def find_all_rta_data_dirs(base_dir: str) -> list[tuple[str,str,str,str|None]]: """ Find data directories where reflection and transmission data exists. The directory paths must habe the same stem and only differ by the -ref -tra -abs suffixes. :return: list[(shared_part_of_dirname, ref_data_dir, tra_data_dir, abs_data_dir/None)] """ all_data_dirs: list[str] = find_all_data_dirs(base_dir) all_data_dirs.sort() rta_data_shared_names: list[tuple[str,str,str,str|None]] = [] ref_suffix = f"-{REFLECTION}" abs_suffix = f"-{ABSORPTION}" tra_suffix = f"-{TRANSMISSION}" for i, ddir in enumerate(all_data_dirs): if not ddir.endswith(ref_suffix): continue abs_dir = None tra_dir = None shared_name = ddir.removesuffix(ref_suffix) # rely on the fact that data is sorted, abs will come before ref and tra after ref if i > 0: if all_data_dirs[i-1].startswith(shared_name) and all_data_dirs[i-1].endswith(abs_suffix): abs_dir = all_data_dirs[i-1] if i+1 < len(all_data_dirs): if all_data_dirs[i+1].startswith(shared_name) and all_data_dirs[i+1].endswith(tra_suffix): tra_dir = all_data_dirs[i+1] if tra_dir: # have at least ref and tra rta_data_shared_names.append((shared_name, ddir, tra_dir, abs_dir)) return rta_data_shared_names
[docs] def add_all_absorption_datas(base_dir: str, norm_data_dir:str|None=None, **add_kw): """ - Search recursively through base_dir - For all directories that contains reflection and transmission data, do: - Add an absorption directory - Calculate the absorption data using reference data from norm_data_dir - If plot: plot the absorption, and R/T/A combined """ if norm_data_dir: norm_data = PrsData(norm_data_dir) else: norm_data = None rta_dirs = find_all_rta_data_dirs(base_dir) for shared_dirpath, ref_dir, tra_dir, abs_dir in rta_dirs: abs_dir = shared_dirpath + f"-{ABSORPTION}" add_absorption_data(ref_dir, tra_dir, abs_dir, norm_data, **add_kw)
[docs] def add_absorption_data(ref_data_or_dir: str|PrsData|None, tra_data_or_dir: str|PrsData, abs_dir: str, norm_data_or_dir: str|PrsData|None, replace_existing=False, replot_existing=True, plot=True, csv=True, subplots_kw=None, **plot_spectrum_kw) -> PrsData|None: ref_data: PrsData = PrsData(ref_data_or_dir) if type(ref_data_or_dir) == str else ref_data_or_dir tra_data: PrsData = PrsData(tra_data_or_dir) if type(tra_data_or_dir) == str else tra_data_or_dir if len(ref_data.wavelengths) == 0: log.warning(f"Not adding absorption data, reflection data has no wavelength data: '{ref_data.dirpath}'") return None if len(tra_data.wavelengths) == 0: log.warning(f"Not adding absorption data, transmission data has no wavelength data: '{ref_data.dirpath}'") return None norm_data: PrsData|None = PrsData(norm_data_or_dir) if type(norm_data_or_dir) == str else norm_data_or_dir if replace_existing or abs_dir is None: abs_data = calc_absorption_data(ref_data=ref_data, tra_data=tra_data, norm_data=norm_data, file_mode="w", data_path=abs_dir) abs_data.write_full_file() abs_data.save_csv(columns=["wl", "E", "dI-X_I", "dI-X", "I", "sdI-X_I", "sdI-X", "sI"]) elif plot and replot_existing: abs_data = PrsData(abs_dir) else: return None if csv: write_rta_csv(ref_data, tra_data, abs_data) if plot: # Plot of abs cols = ["wl", "E", "dI-X_I", "sdI-X_I"] sdata, cols = abs_data.get_spectrum_data(columns=cols) fig1, _ = plot_spectrum(sdata, columns=cols, title=abs_data.name, what=["dI-X_I"], mode=abs_data.mode, **plot_spectrum_kw) fig1_path = os.path.join(abs_data.dirpath, abs_data.dirname + ".pdf") fig1.savefig(fig1_path) # Plot of ref, tra and abs plot_rta(ref_data, tra_data, abs_data, fig=None, axs=None, title=True, single_ax=False, subplots_kw=subplots_kw or {}, label=None, save_fig=True, **plot_spectrum_kw) return abs_data
[docs] def write_rta_csv(ref_data: PrsData, tra_data: PrsData, abs_data: PrsData|None, save_file=True, columns=["dI-X_I"]) -> str: datas: list[PrsData] = [ref_data, tra_data] if abs_data: datas.append(abs_data) # get common wavelengths wls = np.array(datas[0].wavelengths) for i in range(1, len(datas)): wls = np.intersect1d(wls, datas[i].wavelengths) fixed_cols = ["wl", "E"] # get spectrum data get_columns = fixed_cols + columns sdatas = [data.get_spectrum_data(wavelengths=wls, columns=get_columns)[0] for data in datas] # header line csv = ",".join([PrsData.get_column_label_with_unit(col) for col in fixed_cols]) for i, data in enumerate(datas): for col in columns: csv += "," + data.get_column_label_with_unit(col) for j, wl in enumerate(wls): csv += "\n" # Energy, wavelength csv += str(sdatas[0][j,0]) + "," + str(sdatas[0][j,1]) for _, sdata in enumerate(sdatas): for k, col in enumerate(columns): csv += "," + str(sdata[j,k+len(fixed_cols)]) if save_file == True: save_file = os.path.join(os.path.dirname(ref_data.dirpath), ref_data.dirname.removesuffix("-" + REFLECTION) + ".csv") if save_file: log.info(f"Saving RTA csv as '{save_file}'") with open(save_file, "w") as f: f.write(csv) return csv
# # SAVING DATA #
[docs] def archive_data(data_dir_path: str, archive_dirs: str|list[str], n_wavelengths_threshold=0): """ Save a zip of the directory in the archive directory, if it contains enough partial files. This is restriction is to prevent the archive being flooded with small test measurements. :param data_dir_path: :param archive_dirs: archive directory or list of archive directories :param n_wavelengths_threshold: Minimum number of wavelengths the data must have to be archived :return: """ if n_wavelengths_threshold <= 0: n_part_files = 0 for f in os.listdir(data_dir_path): if f.startswith(PARTIAL_PREFIX): n_part_files += 1 if n_part_files < n_wavelengths_threshold: log.info(f"Not archiving data since it only contains {n_part_files} partial files, threshold is set to {n_wavelengths_threshold}") return if not type(archive_dirs) is list: archive_dirs = [archive_dirs] if len(archive_dirs) == 0: return archive_dir = os.path.expanduser(archive_dirs[0]) if not os.path.isdir(archive_dir): os.makedirs(archive_dir) data_dirname = os.path.basename(data_dir_path) zipname = os.path.join(archive_dir, data_dirname) log.info(f"Archiving data at '{zipname}.zip'. This might take a while...") # this is so slow # import zipfile # zipfile.main(['-c', zipname, data_dir_path + "/"]) # this is so slow too shutil.make_archive(zipname, "zip", data_dir_path) # if more than one archive dirs, copy to the others since zipping is so slow for i in range(1, len(archive_dirs)): archive_dir_2 = os.path.expanduser(archive_dirs[i]) log.info(f"Copying archive from '{zipname+'.zip'}' to '{archive_dir_2}'") shutil.copy(zipname + ".zip", archive_dir_2)
[docs] def save_data(data_dir_path, copy_data_dirs: str|list[str], dont_save_partial_files=True): """ Copy the data from data_dir_path to the data_dir from the configuration file. Partial files can be skipped. :param data_dir_path: :param dont_save_partial_files: :param copy_data_dirs: Directory(ies) to copy the data to :return: """ if not type(copy_data_dirs) is list: copy_data_dirs = [copy_data_dirs] for d in copy_data_dirs: d = os.path.expanduser(d) new_data_dir = os.path.join(d, os.path.basename(data_dir_path)) if os.path.isdir(new_data_dir): log.error(f"Data directory '{new_data_dir}' already exists, skipping.") return os.makedirs(new_data_dir) import shutil log.info(f"Copying data from '{d}' to '{new_data_dir}'") for f in os.listdir(data_dir_path): if dont_save_partial_files and f.startswith(PARTIAL_PREFIX): continue log.debug(f"Copying '{f}' to '{new_data_dir}'") shutil.copy(os.path.join(data_dir_path, f), new_data_dir)
[docs] def save_data_rta(ref_data: PrsData, tra_data: PrsData, copy_data_dirs: str|list[str], norm_data_dir: str|None, dont_save_partial_files=True, **plot_spectrum_kw): """ Copy the data ref and tra data into a subdir in the data_dir_final(s) from the configuration file. Partial files can be skipped. Reflection and transmission data will be placed in a subdirectory and the absorption data will be calculated. :param data_dir_path_no_suffix: Path to the reference and transmission data, without "-ref"/"-tra") :param copy_data_dirs: Directory(ies) to copy the data to :param dont_save_partial_files: :return*: """ # get shared name data_dir_path_no_suffix_ref = ref_data.dirpath.removesuffix(f"-{REFLECTION}") data_dir_path_no_suffix_tra = tra_data.dirpath.removesuffix(f"-{TRANSMISSION}") if data_dir_path_no_suffix_ref != data_dir_path_no_suffix_tra: raise RuntimeError(f"ref and tra data must share common name, but names are '{data_dir_path_no_suffix_ref}' and '{data_dir_path_no_suffix_tra}") data_dir_path_no_suffix: str = data_dir_path_no_suffix_ref dirname = os.path.basename(data_dir_path_no_suffix) # get target directories if not type(copy_data_dirs) is list: copy_data_dirs = [copy_data_dirs] copy_data_dirs = [os.path.expanduser(d) for d in copy_data_dirs] # copy and calc in the first dir new_data_dir_base = os.path.join(copy_data_dirs[0], dirname) if os.path.isdir(new_data_dir_base): log.error(f"Data directory '{new_data_dir_base}' already exists, skipping.") return os.makedirs(new_data_dir_base) for mode in [REFLECTION, TRANSMISSION]: data_dir = data_dir_path_no_suffix + f"-{mode}" new_data_dir = os.path.join(new_data_dir_base, dirname + f"-{mode}") log.info(f"Copying data from '{data_dir}' to '{new_data_dir}'") os.makedirs(new_data_dir) for f in os.listdir(data_dir): if dont_save_partial_files and f.startswith(PARTIAL_PREFIX): continue log.debug(f"Copying '{f}' to '{new_data_dir}'") shutil.copy(os.path.join(data_dir, f), new_data_dir) # calculate absorption if norm_data_dir: norm_data = PrsData(norm_data_dir) else: norm_data = None abs_dir = os.path.join(new_data_dir_base, dirname + f"-{ABSORPTION}") abs_data = calc_absorption_data(ref_data=ref_data, tra_data=tra_data, norm_data=norm_data, file_mode="w", data_path=abs_dir) # save absorption abs_data.write_full_file() abs_data.save_csv(columns=["wl", "E", "dI-X_I", "dI-X", "I", "sdI-X_I", "sdI-X", "sI"]) plot_file = os.path.join(new_data_dir_base, dirname + ".pdf") plot_rta(ref_data, tra_data, abs_data, fig=None, axs=None, title=True, single_ax=False, label=None, save_fig=plot_file, **plot_spectrum_kw) csv_file = os.path.join(new_data_dir_base, dirname + ".csv") # combined CSV try: write_rta_csv(ref_data, tra_data, abs_data, save_file=csv_file, columns=["dI-X_I"]) except Exception as e: log.error(f"Failed to write combined csv: {type(e)}: {e}") # do a combined error report try: report = sanity_check_data_gen_report(ref_data) report += "\n\n" + sanity_check_data_gen_report(tra_data) report += "\n\n" + sanity_check_data_gen_report(abs_data) report = report.strip("\n") if report: with open(os.path.join(new_data_dir_base, dirname + "-report.md"), "w") as file: file.write(report) except Exception as e: log.error(f"Failed to assemble or write report: {type(e)}: {e}") # copy all three directories to all other final data directories for i in range(1, len(copy_data_dirs)): new_data_dir_base_2 = os.path.join(copy_data_dirs[i], dirname) log.info(f"Copying from '{new_data_dir_base}' to '{new_data_dir_base_2}'") shutil.copytree(new_data_dir_base, new_data_dir_base_2)