diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bcc8c4331d2e8ee7b1816e4557feb5dad480a1bd..6595dad41d04d802015e8a686903008a4b85e2b4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,6 +9,8 @@ tests: - pip3 install 'scipy==1.4.1' -U - pip3 install 'matplotlib==3.1.2' -U - pip3 install --no-deps . + - pip3 freeze + - conda list - python3 tffpy/tests/ci_config.py - pytest-3 diff --git a/python/doc/conf.py b/python/doc/conf.py index a6066609518b00a765b81d49d3dd587e98b9508d..91b6cad902a92ac4edbb722ae09df4a42839b10b 100755 --- a/python/doc/conf.py +++ b/python/doc/conf.py @@ -272,6 +272,7 @@ texinfo_no_detailmenu = False intersphinx_mapping = { 'numpy': ('https://docs.scipy.org/doc/numpy/', None), 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), + 'pandas': ('https://pandas.pydata.org/docs/', None), 'skpomade': ('http://valentin.emiya.pages.lis-lab.fr/skpomade/', None), 'yafe': ('http://skmad-suite.pages.lis-lab.fr/yafe/', None), 'ltfatpy': ('http://dev.pages.lis-lab.fr/ltfatpy/', None), diff --git a/python/doc/references.rst b/python/doc/references.rst index da688d76c092d428efd354e0f19ce2b8f387cfad..b695502ae93c8e0e979c9baca7d650b63eb35379 100755 --- a/python/doc/references.rst +++ b/python/doc/references.rst @@ -57,5 +57,6 @@ tffpy\.experiments\.exp_solve_tff module .. automodule:: tffpy.experiments.exp_solve_tff :members: + :special-members: __call__ :undoc-members: :show-inheritance: diff --git a/python/tffpy/datasets.py b/python/tffpy/datasets.py index 8045e56f459e124e3daabeb087c461230fbacc90..2ddebc79ce7f26a2c633b9d5b38c971326989790 100644 --- a/python/tffpy/datasets.py +++ b/python/tffpy/datasets.py @@ -29,7 +29,7 @@ def get_dataset(): ------- dataset : dict dataset['wideband'] (resp. dataset['localized']) is a dictionary - containing the :py:class:`Path` object for all the wideband + containing the :py:class:`~pathlib.Path` object for all the wideband (resp. localized) sounds. """ dataset = dict() @@ -94,7 +94,7 @@ def get_mix(loc_source, wideband_src, crop=None, closing_first : bool If True, morphological closings are applied first, followed by openings. If False, the reverse way is used. - fig_dir : str or Path + fig_dir : None or str or Path If not None, folder where figures are stored. If None, figures are not plotted. prefix : str diff --git a/python/tffpy/experiments/exp_solve_tff.py b/python/tffpy/experiments/exp_solve_tff.py index 6877d06677be25bc56e8c03dd62598fba2629f87..2558156cd84a56984524db4132ac9930e906109c 100644 --- a/python/tffpy/experiments/exp_solve_tff.py +++ b/python/tffpy/experiments/exp_solve_tff.py @@ -1,5 +1,12 @@ # -*- coding: utf-8 -*- """ +Class `SolveTffExperiment` uses the :class:`yafe.base.Experiment` experiment +framework to handle the main time-frequency fading experiment: It includes +loading the data, generating the problems, applying solvers, and exploiting +results. + +See the `documentation <http://skmad-suite.pages.lis-lab.fr/yafe/>`_ of +package :py:mod:`yafe` for the technical details. .. moduleauthor:: Valentin Emiya """ @@ -8,6 +15,7 @@ import numpy as np import matplotlib.pyplot as plt import matplotlib import pandas as pd +from pathlib import Path from yafe import Experiment from madarrays import Waveform @@ -20,6 +28,21 @@ from tffpy.utils import \ class SolveTffExperiment(Experiment): + """ + The main experiment to solve time-frequency fading problems with a + number of sounds mixtures and solvers. + + Parameters + ---------- + force_reset : bool + If true, reset the experiment by erasing all previous results + in order to run it from scratch. If False, the existing results are + kept in order to proceed with the existing experiment. + suffix : str + Suffix that is appended to the name of the experiment, useful to + save results in a specific folder. + """ + def __init__(self, force_reset=False, suffix=''): Experiment.__init__(self, name='SolveTffExperiment' + suffix, @@ -36,10 +59,36 @@ class SolveTffExperiment(Experiment): @property def n_tasks(self): + """ + Number of tasks + + Returns + ------- + int + """ return len(list((self.xp_path / 'tasks').glob('*'))) @staticmethod def get_experiment(setting='full', force_reset=False): + """ + Get the experiment instance with default values in order to handle it. + + Parameters + ---------- + setting : {'full', 'light'} + If 'full', the default values are set to run the full + experiment. If 'light', the default values are set to have a + very light experiment with few tasks, running fast, for test + purposes. + force_reset : bool + If true, reset the experiment by erasing all previous results + in order to run it from scratch. If False, the existing results are + kept in order to proceed with the existing experiment. + + Returns + ------- + SolveTffExperiment + """ assert setting in ('full', 'light') dataset = get_dataset() @@ -76,7 +125,25 @@ class SolveTffExperiment(Experiment): exp.generate_tasks() return exp - def export_task_params(self): + def export_task_params(self, csv_path=None): + """ + Export task parameters to a csv file and to a + :class:`pandas.DataFrame` object. + + Parameters + ---------- + csv_path : str or Path + Name of the csv file to be written. If None, file is + located in the experiment folder with name 'task_params.csv'. + + Returns + ------- + pandas.DataFrame + """ + if csv_path is None: + csv_path = self.xp_path / 'task_params.csv' + else: + csv_path = Path(csv_path) task_list = [] for i_task in range(self.n_tasks): task = self.get_task_data_by_id(idt=i_task) @@ -84,17 +151,39 @@ class SolveTffExperiment(Experiment): for k in task['task_params'] for kk in task['task_params'][k]}) df = pd.DataFrame(task_list) - - csv_path = self.xp_path / 'task_params.csv' df.to_csv(csv_path) print('Task params exported to', csv_path) return df def generate_tasks(self): + """ + Generate tasks and export params to a csv file + + See :py:meth:`yafe.Experiment.generate_tasks` + """ Experiment.generate_tasks(self) self.export_task_params() def get_misc_file(self, task_params=None, idt=None): + """ + Get file with some additional task results. + + This has been set up in order to pass additional data in a way that + could not be handled by the :py:mod:`yafe` framework. + + Parameters + ---------- + task_params : dict + Task parameters. + idt : int + Task identifier. Either `task_params` or `idt` should be given + in order to specify the task. + + Returns + ------- + Path + File containing additional task results. + """ if task_params is not None: task = self.get_task_data_by_params( data_params=task_params['data_params'], @@ -107,6 +196,9 @@ class SolveTffExperiment(Experiment): return path_task / 'misc.npz' def plot_results(self): + """ + Plot and save results of the experiment + """ self.fig_dir.mkdir(parents=True, exist_ok=True) print('Figures saved in {}'.format(self.fig_dir)) results = self.load_results(array_type='xarray') @@ -285,6 +377,16 @@ class SolveTffExperiment(Experiment): return label def plot_task(self, idt, fontsize=16): + """ + Plot and save figures for a specific task + + Parameters + ---------- + idt : int + Task identifier + fontsize : int + Fontsize to be used in Figures. + """ matplotlib.rcParams.update({'font.size': fontsize}) fig_dir = self.xp_path / 'figures' / 'tasks' / '{:06}'.format(idt) fig_dir.mkdir(parents=True, exist_ok=True) @@ -468,14 +570,72 @@ class SolveTffExperiment(Experiment): def get_data(loc_source, wideband_src): + """ + Prepare the input data information for the :py:class:`SolveTffExperiment` + experiment. + + This function is only embedding its input in a dictionary + + Parameters + ---------- + loc_source : Path + File for the source localized in time-frequency (perturbation) + wideband_src : Path + File for the source of interest. + + Returns + ------- + dict + Dictionary to be given when calling the problemm ( + see :py:meth:`Problem.__call__`), with keys `'loc_source'` and + `wideband_src`. + """ return dict(loc_source=loc_source, wideband_src=wideband_src) class Problem: - def __init__(self, win_choice, or_mask, - n_iter_closing, n_iter_opening, closing_first, - delta_mix_db, delta_loc_db, wb_to_loc_ratio_db, crop, - fig_dir): + """ + Problem generation for the :py:class:`SolveTffExperiment` experiment. + + Parameters + ---------- + crop : int or None + If not None, a cropped, centered portion of the sound will be + extracted with the specified length, in samples. + win_choice : str + String of the form 'name len' where 'name' is a window name and + 'len' is a window length, e.g. 'hann 512', 'gauss 256. + delta_mix_db : float + Coefficient energy ratio, in dB, between the wideband source and the + localized source in the mixture in order to select coefficients in + the mask. + delta_loc_db : float + Dynamic range, in dB, for the localized source in order to select + coefficients in the mask. + wb_to_loc_ratio_db : float + Wideband source to localized source energy ratio to be adjusted in + the mix. + or_mask : bool + If True, the mask is build by taking the union of the two masks + obtained using thresholds `delta_mix_db` and `delta_loc_db`. If + False, the intersection is taken. + n_iter_closing : int + Number of successive morphological closings with radius 1 (a.k.a. + radius of one single closing) + n_iter_opening : int + Number of successive morphological openings with radius 1 (a.k.a. + radius of one single opening) + closing_first : bool + If True, morphological closings are applied first, followed by + openings. If False, the reverse way is used. + fig_dir : None or str or Path + If not None, folder where figures are stored. If None, figures are + not plotted. + """ + + def __init__(self, crop, win_choice, + delta_mix_db, delta_loc_db, wb_to_loc_ratio_db, or_mask, + n_iter_closing, n_iter_opening, closing_first, fig_dir): win_type, win_len_str = win_choice.split(sep=' ') win_dur = int(win_len_str) / 8000 self.win_dur = win_dur @@ -497,6 +657,26 @@ class Problem: self.fig_dir = fig_dir def __call__(self, loc_source, wideband_src): + """ + Generate the problem from input data. + + Parameters + ---------- + loc_source : Path + File for the source localized in time-frequency (perturbation) + wideband_src : Path + File for the source of interest. + + Returns + ------- + problem_data : dict + Dictionary to be given to a solver, with keys `'x_mix'` (mix + signal), `mask` (time-frequency mask), `dgt_params` (DGT + parameters) and `signal_params` (signal parameters). + solution_data : dict + Dictionary containing problem solutions, with keys `'x_loc'` ( + localized signal ) and `x_wb` (wideband signal). + """ x_mix, dgt_params, signal_params, mask, x_loc, x_wb = \ get_mix(loc_source=loc_source, wideband_src=wideband_src, @@ -521,12 +701,78 @@ class Problem: class Solver: + """ + Solver for the :py:class:`SolveTffExperiment` experiment. + + This solver is computing + + * the `TFF-1` of `TFF-P` solution (depending on parameter `tol_subregions`) + using a :py:class:`~tffpy.tf_fading.GabMulTff` instance + * the `Interp` solution using function + :py:func:`~tffpy.interpolation_solver.solve_by_interpolation` + + Parameters + ---------- + tol_subregions : None or float + Tolerance to split the mask into sub-regions in + :py:class:`~tffpy.tf_fading.GabMulTff`. + tolerance_arrf : float + Tolerance for the randomized EVD in + :py:class:`~tffpy.tf_fading.GabMulTff`, see method + :py:meth:`~tffpy.tf_fading.GabMulTff.compute_decomposition`. + proba_arrf : float + Probability of error for the randomized EVD in + :py:class:`~tffpy.tf_fading.GabMulTff`, see method + :py:meth:`~tffpy.tf_fading.GabMulTff.compute_decomposition`. + """ + def __init__(self, tol_subregions, tolerance_arrf, proba_arrf): self.tol_subregions = tol_subregions self.tolerance_arrf = tolerance_arrf self.proba_arrf = proba_arrf def __call__(self, x_mix, mask, dgt_params, signal_params): + """ + Apply the solver to estimate solutions from the problem data. + + The output dictionary is composed of data with keys: + + * `'x_tff'`: solution estimated by :py:class:`~tffpy.tf_fading.GabMulTff` + * `'x_zero'`: solution when applying the Gabor + multiplier (i.e., :math:`\lambda=1`) + * `'x_interp'`: solution from function + :py:func:`~tffpy.interpolation_solver.solve_by_interpolation` + * `'gmtff'`: `GabMulTff` instance + * `'t_lambda_tff'`: running times to estimate hyperparameter in method + :py:meth:`~tffpy.tf_fading.GabMulTff.compute_lambda` + * `'t_arrf'`: running times to compute range approximation in method + :py:meth:`~tffpy.tf_fading.GabMulTff.compute_decomposition` + * `'t_evdn'`: running times to compute EVD in method + :py:meth:`~tffpy.tf_fading.GabMulTff.compute_decomposition` + * `'t_uh_x'`: running times to compute additional matrix products in + method :py:meth:`~tffpy.tf_fading.GabMulTff.compute_decomposition` + * `'t_subreg'`: running times to split mask into sub-regions in class + :py:class:`~tffpy.tf_fading.GabMulTff` + * `'lambda_tff'`: estimated values for hyper-parameters + :math:`\lambda_i` estimated by + :py:meth:`~tffpy.tf_fading.GabMulTff`.compute_lambda` + + Parameters + ---------- + x_mix : nd-array + Mix signal + mask : nd-array + Time-frequency mask + dgt_params : dict + DGT parameters + signal_params : dict + Signal parameters + + Returns + ------- + dict + The estimated solution and additional information + """ gmtff = GabMulTff(x_mix=x_mix, mask=mask, dgt_params=dgt_params, signal_params=signal_params, tol_subregions=self.tol_subregions) @@ -550,6 +796,31 @@ class Solver: def perf_measures(task_params, source_data, problem_data, solution_data, solved_data, exp=None): + """ + Performance measure, including computation of oracle solutions + + Parameters + ---------- + task_params : dict + Task parameters + source_data : dict + Input data + problem_data : dict + Problem data + solution_data : dict + Solver output + solved_data : dict + True solution data + exp : SolveTffExperiment + The experiment + + Returns + ------- + dict + All data useful for result analysis including SDR and Itakura-Saito + performance, running times, hyperparameter values, mask size and + number of sub-regions. + """ x_tff = solved_data['x_tff'] x_zero = solved_data['x_zero'] gmtff = solved_data['gmtff'] @@ -607,6 +878,9 @@ def perf_measures(task_params, source_data, problem_data, def create_and_run_light_experiment(): + """ + Create a light experiment and run it + """ exp = SolveTffExperiment.get_experiment(setting='light', force_reset=True) print('*' * 80) print('Created experiment') diff --git a/python/tffpy/tf_fading.py b/python/tffpy/tf_fading.py index f2e15b8930963f661eca84b30ddb984a25b17acf..c0be148e6ddc7241794a2ccb44b82fe1290e887a 100644 --- a/python/tffpy/tf_fading.py +++ b/python/tffpy/tf_fading.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- """ +Class :class:`GabMulTff` is the main object to solve a time-frequency fading +problem. .. moduleauthor:: Valentin Emiya """ @@ -38,7 +40,7 @@ class GabMulTff: tol_subregions : None or float If None, the mask is considered as a single region. If float, tolerance to split the mask into sub-regions using - :py:func:`tffpy.create_subregions.create_subregions`. + :py:func:`~tffpy.create_subregions.create_subregions`. fig_dir : str or Path If not None, folder where figures are stored. If None, figures are not plotted. @@ -95,16 +97,19 @@ class GabMulTff: followed by :py:func:`skpomade.factorization_construction.evd_nystrom`. The rank of each decomposition is estimated using parameters `tolerance_arrf` and `proba_arrf`. - Running times are stored in attributes `t_arrf`, `t_evdn` and `t_uh_x`. + Running times to compute the range approximation, the + EVD itself and the additional matrix products for subsequent + computations are stored in attributes `t_arrf`, `t_evdn` and + `t_uh_x`, respectively. Parameters ---------- tolerance_arrf : float Tolerance for - :py:func:`skpomade.range_approximation.adaptive_randomized_range_finder` + :py:func:`~skpomade.range_approximation.adaptive_randomized_range_finder` proba_arrf : float Probability of error for - :py:func:`skpomade.range_approximation.adaptive_randomized_range_finder` + :py:func:`~skpomade.range_approximation.adaptive_randomized_range_finder` """ for i in range(self.n_areas):