From 9bb49673c62f62931b4aa1e90fca2f58f1dea017 Mon Sep 17 00:00:00 2001 From: Antoine Bernas Date: Mon, 18 Sep 2023 18:14:07 +0200 Subject: [PATCH 01/34] bug fix: in collect_nm if batch_size = 1 and extension = .txt --- pcntoolkit/normative_parallel.py | 47 +++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/pcntoolkit/normative_parallel.py b/pcntoolkit/normative_parallel.py index 268e9461..119e5055 100755 --- a/pcntoolkit/normative_parallel.py +++ b/pcntoolkit/normative_parallel.py @@ -458,7 +458,7 @@ def collect_nm(processing_dir, :param collect: If True data is checked for failed batches and collected; if False data is just checked :param binary: Results in pkl format - :outputs: Text files containing all results accross all batches the combined output (written to disk). + :outputs: Text or pkl files containing all results accross all batches the combined output (written to disk). :returns 0: if batches fail :returns 1: if bathches complete successfully @@ -492,7 +492,10 @@ def collect_nm(processing_dir, else: file_example = pd.read_pickle(file_example[0]) numsubjects = file_example.shape[0] - batch_size = file_example.shape[1] + try: + batch_size = file_example.shape[1] # doesn't exist if size=1, and txt file + except: + batch_size = 1 # artificially creates files for batches that were not executed batch_dirs = glob.glob(processing_dir + 'batch_*/') @@ -600,7 +603,10 @@ def collect_nm(processing_dir, pRho_filenames = fileio.sort_nicely(pRho_filenames) pRho_dfs = [] for pRho_filename in pRho_filenames: - pRho_dfs.append(pd.DataFrame(fileio.load(pRho_filename))) + if batch_size == 1 and binary is False: #if batch size = 1 and .txt file + pRho_dfs.append(pd.DataFrame(fileio.load(pRho_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array + else: + pRho_dfs.append(pd.DataFrame(fileio.load(pRho_filename))) pRho_dfs = pd.concat(pRho_dfs, ignore_index=True, axis=0) fileio.save(pRho_dfs, processing_dir + 'pRho' + outputsuffix + file_extentions) @@ -612,7 +618,10 @@ def collect_nm(processing_dir, Rho_filenames = fileio.sort_nicely(Rho_filenames) Rho_dfs = [] for Rho_filename in Rho_filenames: - Rho_dfs.append(pd.DataFrame(fileio.load(Rho_filename))) + if batch_size == 1 and binary is False: #if batch size = 1 and .txt file + Rho_dfs.append(pd.DataFrame(fileio.load(Rho_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array + else: + Rho_dfs.append(pd.DataFrame(fileio.load(Rho_filename))) Rho_dfs = pd.concat(Rho_dfs, ignore_index=True, axis=0) fileio.save(Rho_dfs, processing_dir + 'Rho' + outputsuffix + file_extentions) @@ -660,7 +669,10 @@ def collect_nm(processing_dir, rmse_filenames = fileio.sort_nicely(rmse_filenames) rmse_dfs = [] for rmse_filename in rmse_filenames: - rmse_dfs.append(pd.DataFrame(fileio.load(rmse_filename))) + if batch_size == 1 and binary is False: #if batch size = 1 and .txt file + rmse_dfs.append(pd.DataFrame(fileio.load(rmse_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array + else: + rmse_dfs.append(pd.DataFrame(fileio.load(rmse_filename))) rmse_dfs = pd.concat(rmse_dfs, ignore_index=True, axis=0) fileio.save(rmse_dfs, processing_dir + 'RMSE' + outputsuffix + file_extentions) @@ -672,7 +684,10 @@ def collect_nm(processing_dir, smse_filenames = fileio.sort_nicely(smse_filenames) smse_dfs = [] for smse_filename in smse_filenames: - smse_dfs.append(pd.DataFrame(fileio.load(smse_filename))) + if batch_size == 1 and binary is False: #if batch size = 1 and .txt file + smse_dfs.append(pd.DataFrame(fileio.load(smse_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array + else: + smse_dfs.append(pd.DataFrame(fileio.load(smse_filename))) smse_dfs = pd.concat(smse_dfs, ignore_index=True, axis=0) fileio.save(smse_dfs, processing_dir + 'SMSE' + outputsuffix + file_extentions) @@ -684,7 +699,10 @@ def collect_nm(processing_dir, expv_filenames = fileio.sort_nicely(expv_filenames) expv_dfs = [] for expv_filename in expv_filenames: - expv_dfs.append(pd.DataFrame(fileio.load(expv_filename))) + if batch_size == 1 and binary is False: #if batch size = 1 and .txt file + expv_dfs.append(pd.DataFrame(fileio.load(expv_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array + else: + expv_dfs.append(pd.DataFrame(fileio.load(expv_filename))) expv_dfs = pd.concat(expv_dfs, ignore_index=True, axis=0) fileio.save(expv_dfs, processing_dir + 'EXPV' + outputsuffix + file_extentions) @@ -696,7 +714,10 @@ def collect_nm(processing_dir, msll_filenames = fileio.sort_nicely(msll_filenames) msll_dfs = [] for msll_filename in msll_filenames: - msll_dfs.append(pd.DataFrame(fileio.load(msll_filename))) + if batch_size == 1 and binary is False: #if batch size = 1 and .txt file + msll_dfs.append(pd.DataFrame(fileio.load(msll_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array + else: + msll_dfs.append(pd.DataFrame(fileio.load(msll_filename))) msll_dfs = pd.concat(msll_dfs, ignore_index=True, axis=0) fileio.save(msll_dfs, processing_dir + 'MSLL' + outputsuffix + file_extentions) @@ -708,7 +729,10 @@ def collect_nm(processing_dir, nll_filenames = fileio.sort_nicely(nll_filenames) nll_dfs = [] for nll_filename in nll_filenames: - nll_dfs.append(pd.DataFrame(fileio.load(nll_filename))) + if batch_size == 1 and binary is False: #if batch size = 1 and .txt file + nll_dfs.append(pd.DataFrame(fileio.load(nll_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array + else: + nll_dfs.append(pd.DataFrame(fileio.load(nll_filename))) nll_dfs = pd.concat(nll_dfs, ignore_index=True, axis=0) fileio.save(nll_dfs, processing_dir + 'NLL' + outputsuffix + file_extentions) @@ -720,7 +744,10 @@ def collect_nm(processing_dir, bic_filenames = fileio.sort_nicely(bic_filenames) bic_dfs = [] for bic_filename in bic_filenames: - bic_dfs.append(pd.DataFrame(fileio.load(bic_filename))) + if batch_size == 1 and binary is False: #if batch size = 1 and .txt file + bic_dfs.append(pd.DataFrame(fileio.load(bic_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array + else: + bic_dfs.append(pd.DataFrame(fileio.load(bic_filename))) bic_dfs = pd.concat(bic_dfs, ignore_index=True, axis=0) fileio.save(bic_dfs, processing_dir + 'BIC' + outputsuffix + file_extentions) From ead4fbdf2b27d301f0b4d782ec7560fe21189b22 Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Fri, 13 Oct 2023 14:00:12 +0200 Subject: [PATCH 02/34] bump version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 811f7cb1..59bff139 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='pcntoolkit', - version='0.28', + version='0.29', description='Predictive Clinical Neuroscience toolkit', url='http://github.com/amarquand/PCNtoolkit', author='Andre Marquand', From 510c7481c4d5890a51c05a732e61a794fac6cde6 Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Mon, 30 Oct 2023 09:57:35 +0100 Subject: [PATCH 03/34] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c7eb533..fa37c7d4 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ conda install -y pandas scipy Install pip dependencies ``` -pip --no-cache-dir install nibabel sklearn torch glob3 +pip --no-cache-dir install nibabel scikit-learn torch glob3 ``` Clone the repo From cb775f8c0bb97d86aa8bc5db532f19aa7aa52e4e Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Wed, 15 Nov 2023 08:04:55 +0100 Subject: [PATCH 04/34] Index is added to the transform function in scaler class. --- pcntoolkit/util/utils.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pcntoolkit/util/utils.py b/pcntoolkit/util/utils.py index 3d22bb57..e96d9b2c 100644 --- a/pcntoolkit/util/utils.py +++ b/pcntoolkit/util/utils.py @@ -1141,15 +1141,19 @@ def fit(self, X): self.max[i] = np.median(np.sort(X[:,i])[-int(np.round(X.shape[0] * self.tail)):]) - def transform(self, X): + def transform(self, X, index=None): if self.scaler_type == 'standardize': - - X = (X - self.m) / self.s + if index is None: + X = (X - self.m) / self.s + else: + X = (X - self.m[index]) / self.s[index] elif self.scaler_type in ['minmax', 'robminmax']: - - X = (X - self.min) / (self.max - self.min) + if index is None: + X = (X - self.min) / (self.max - self.min) + else: + X = (X - self.min[index]) / (self.max[index] - self.min[index]) if self.adjust_outliers: From 828007c1aea5cdc01234d9f9d7eaf0021e8b5d0a Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Wed, 15 Nov 2023 08:06:06 +0100 Subject: [PATCH 05/34] A quick patch to fix z-scores for SHASH HBR in estimate function. --- pcntoolkit/normative.py | 19 ++++++++++++++----- pcntoolkit/normative_model/norm_hbr.py | 22 +++++++++++++--------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/pcntoolkit/normative.py b/pcntoolkit/normative.py index 275f8e99..97898c5e 100755 --- a/pcntoolkit/normative.py +++ b/pcntoolkit/normative.py @@ -447,7 +447,7 @@ def estimate(covfile, respfile, **kwargs): kwargs['trbefile'] = 'be_kfold_tr_tempfile.pkl' kwargs['tsbefile'] = 'be_kfold_ts_tempfile.pkl' - # estimate the models for all subjects + # estimate the models for all response variables for i in range(0, len(nz)): print("Estimating model ", i+1, "of", len(nz)) nm = norm_init(Xz_tr, Yz_tr[:, i], alg=alg, **kwargs) @@ -500,7 +500,14 @@ def estimate(covfile, respfile, **kwargs): else: Ytest = Y[ts, nz[i]] - Z[ts, nz[i]] = (Ytest - Yhat[ts, nz[i]]) / \ + if alg=='hbr': + if outscaler in ['standardize', 'minmax', 'robminmax']: + Ytestz = Y_scaler.transform(Ytest.reshape(-1,1), index=i) + else: + Ytestz = Ytest.reshape(-1,1) + Z[ts, nz[i]] = nm.get_mcmc_zscores(Xz_ts, Ytestz, **kwargs) + else: + Z[ts, nz[i]] = (Ytest - Yhat[ts, nz[i]]) / \ np.sqrt(S2[ts, nz[i]]) except Exception as e: @@ -749,7 +756,8 @@ def predict(covfile, respfile, maskfile=None, **kwargs): else: Xz = X - # estimate the models for all subjects + # estimate the models for all variabels + #TODO Z-scores adaptation for SHASH HBR for i, m in enumerate(models): print("Prediction by model ", i+1, "of", feature_num) nm = norm_init(Xz) @@ -806,7 +814,7 @@ def predict(covfile, respfile, maskfile=None, **kwargs): warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] Yw[:,i] = nm.blr.warp.f(Y[:,i], warp_param) - Y = Yw; + Y = Yw else: warp = False @@ -1062,7 +1070,8 @@ def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, Yte = Yw; else: warp = False - + + #TODO Z-scores adaptation for SHASH HBR Z = (Yte - Yhat) / np.sqrt(S2) print("Evaluating the model ...") diff --git a/pcntoolkit/normative_model/norm_hbr.py b/pcntoolkit/normative_model/norm_hbr.py index f8ff9da6..e4718071 100644 --- a/pcntoolkit/normative_model/norm_hbr.py +++ b/pcntoolkit/normative_model/norm_hbr.py @@ -488,7 +488,7 @@ def get_mcmc_quantiles(self, X, batch_effects=None, z_scores=None): return quantiles.mean(axis=-1) - def get_mcmc_zscores(self, X, y, batch_effects=None): + def get_mcmc_zscores(self, X, y, **kwargs): """ Computes zscores of data given an estimated model @@ -496,13 +496,17 @@ def get_mcmc_zscores(self, X, y, batch_effects=None): Args: X ([N*p]ndarray): covariates y ([N*1]ndarray): response variables - batch_effects (ndarray): the batch effects corresponding to X """ - # Set batch effects to zero if none are provided + print(self.configs['likelihood']) - if batch_effects is None: - batch_effects = batch_effects_test = np.zeros([X.shape[0], 1]) - + + tsbefile = kwargs.get("tsbefile", None) + if tsbefile is not None: + batch_effects_test = fileio.load(tsbefile) + else: # Set batch effects to zero if none are provided + print("Could not find batch-effects file! Initializing all as zeros ...") + batch_effects_test = np.zeros([X.shape[0], 1]) + # Determine the variables to predict if self.configs["likelihood"] == "Normal": var_names = ["mu_samples", "sigma_samples","sigma_plus_samples"] @@ -525,7 +529,7 @@ def get_mcmc_zscores(self, X, y, batch_effects=None): # Do a forward to get the posterior predictive in the idata self.hbr.predict( X=X, - batch_effects=batch_effects, + batch_effects=batch_effects_test, batch_effects_maps=self.batch_effects_maps, pred="single", var_names=var_names+["y_like"], @@ -536,7 +540,7 @@ def get_mcmc_zscores(self, X, y, batch_effects=None): self.hbr.idata, "posterior_predictive", var_names=var_names ) - # Remove superfluous var_nammes + # Remove superfluous var_names var_names.remove('sigma_samples') if 'delta_samples' in var_names: var_names.remove('delta_samples') @@ -553,7 +557,7 @@ def get_mcmc_zscores(self, X, y, batch_effects=None): *array_of_vars, kwargs={"y": y, "likelihood": self.configs['likelihood']}, ) - return z_scores.mean(axis=-1) + return z_scores.mean(axis=-1).values From 32215b41cf33eb51b09575647426126de9551284 Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Tue, 5 Dec 2023 13:21:13 +0100 Subject: [PATCH 06/34] Fixed this issue --- pcntoolkit/model/hbr.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pcntoolkit/model/hbr.py b/pcntoolkit/model/hbr.py index ca4dfc36..402c625e 100644 --- a/pcntoolkit/model/hbr.py +++ b/pcntoolkit/model/hbr.py @@ -431,6 +431,11 @@ def predict( var_names = self.vars_to_sample n_samples = X.shape[0] + + # Need to delete self.idata.posterior_predictive, otherwise, if it exists, it will not be overwritten + if hasattr(self.idata, 'posterior_predictive'): + del self.idata.posterior_predictive + with modeler(X, y, truncated_batch_effects_train, self.configs) as model: # For each batch effect dim for i in range(batch_effects.shape[1]): From 97712e20e28e2015ccff1684e790e45c9a10f46a Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Thu, 7 Dec 2023 16:52:24 +0100 Subject: [PATCH 07/34] Wrote docstring for most functions. --- pcntoolkit/dataio/fileio.py | 190 +++++++++++++- pcntoolkit/model/SHASH.py | 11 +- pcntoolkit/model/bayesreg.py | 10 +- pcntoolkit/model/hbr.py | 326 +++++++++++++++++++++++-- pcntoolkit/normative.py | 64 ++++- pcntoolkit/normative_NP.py | 42 +++- pcntoolkit/normative_model/norm_blr.py | 51 ++++ pcntoolkit/normative_model/norm_gpr.py | 40 +++ pcntoolkit/normative_model/norm_hbr.py | 87 ++++++- pcntoolkit/normative_model/norm_np.py | 92 +++++++ pcntoolkit/normative_model/norm_rfa.py | 39 ++- pcntoolkit/trendsurf.py | 64 ++++- 12 files changed, 969 insertions(+), 47 deletions(-) diff --git a/pcntoolkit/dataio/fileio.py b/pcntoolkit/dataio/fileio.py index 37ce1ef7..b32e6302 100644 --- a/pcntoolkit/dataio/fileio.py +++ b/pcntoolkit/dataio/fileio.py @@ -35,6 +35,9 @@ def predictive_interval(s2_forward, cov_forward, multiplicator): + """ + Calculates a predictive interval for the forward model + """ # calculates a predictive interval PI=np.zeros(len(cov_forward)) @@ -44,6 +47,18 @@ def predictive_interval(s2_forward, return PI def create_mask(data_array, mask, verbose=False): + """ + Create a mask from a data array or a nifti file + + Basic usage:: + + create_mask(data_array, mask, verbose) + + :param data_array: numpy array containing the data to write out + :param mask: nifti image containing a mask for the image + :param verbose: verbose output + """ + # create a (volumetric) mask either from an input nifti or the nifti itself if mask is not None: @@ -68,6 +83,17 @@ def create_mask(data_array, mask, verbose=False): def vol2vec(dat, mask, verbose=False): + """ + Vectorise a 3d image + + Basic usage:: + + vol2vec(dat, mask, verbose) + + :param dat: numpy array containing the data to write out + :param mask: nifti image containing a mask for the image + :param verbose: verbose output + """ # vectorise a 3d image if len(dat.shape) < 4: @@ -92,6 +118,15 @@ def vol2vec(dat, mask, verbose=False): def file_type(filename): + """ + Determine the file type of a file + + Basic usage:: + + file_type(filename) + + :param filename: name of the file to check + """ # routine to determine filetype if filename.endswith(('.dtseries.nii', '.dscalar.nii', '.dlabel.nii')): @@ -109,6 +144,16 @@ def file_type(filename): def file_extension(filename): + """ + Determine the file extension of a file (e.g. .nii.gz) + + Basic usage:: + + file_extension(filename) + + :param filename: name of the file to check + """ + # routine to get the full file extension (e.g. .nii.gz, not just .gz) parts = filename.split(os.extsep) @@ -131,7 +176,15 @@ def file_extension(filename): def file_stem(filename): + """ + Determine the file stem of a file (e.g. /path/to/file.nii.gz -> file) + + Basic usage:: + + file_stem(filename) + :param filename: name of the file to check + """ idx = filename.find(file_extension(filename)) stm = filename[0:idx] @@ -143,6 +196,18 @@ def file_stem(filename): def load_nifti(datafile, mask=None, vol=False, verbose=False): + """ + Load a nifti file into a numpy array + + Basic usage:: + + load_nifti(datafile, mask, vol, verbose) + + :param datafile: name of the file to load + :param mask: nifti image containing a mask for the image + :param vol: whether to load the image as a volume + :param verbose: verbose output + """ if verbose: print('Loading nifti: ' + datafile + ' ...') @@ -167,8 +232,6 @@ def save_nifti(data, filename, examplenii, mask, dtype=None): save_nifti(data, filename mask, dtype) - where the variables are defined below. - :param data: numpy array containing the data to write out :param filename: where to store it :param examplenii: nifti to copy the geometry and data type from @@ -210,7 +273,18 @@ def save_nifti(data, filename, examplenii, mask, dtype=None): def load_cifti(filename, vol=False, mask=None, rmtmp=True): + """ + Load a cifti file into a numpy array + Basic usage:: + + load_cifti(filename, vol, mask, rmtmp) + + :param filename: name of the file to load + :param vol: whether to load the image as a volume + :param mask: nifti image containing a mask for the image + :param rmtmp: whether to remove temporary files + """ # parse the name dnam, fnam = os.path.split(filename) fpref = file_stem(fnam) @@ -261,7 +335,20 @@ def load_cifti(filename, vol=False, mask=None, rmtmp=True): def save_cifti(data, filename, example, mask=None, vol=True, volatlas=None): - """ Write output to nifti """ + """ + Save a cifti file from a numpy array + + Basic usage:: + + save_cifti(data, filename, example, mask, vol, volatlas) + + :param data: numpy array containing the data to write out + :param filename: where to store it + :param example: example file to copy the geometry from + :param mask: nifti image containing a mask for the image + :param vol: whether to load the image as a volume + :param volatlas: atlas to use for the volume + """ # do some sanity checks if data.dtype == 'float32' or \ @@ -349,6 +436,16 @@ def save_cifti(data, filename, example, mask=None, vol=True, volatlas=None): def load_pd(filename): + """ + Load a csv file into a pandas dataframe + + Basic usage:: + + load_pd(filename) + + :param filename: name of the file to load + """ + # based on pandas x = pd.read_csv(filename, sep=' ', @@ -357,6 +454,16 @@ def load_pd(filename): def save_pd(data, filename): + """ + Save a pandas dataframe to a csv file + + Basic usage:: + + save_pd(data, filename) + + :param data: pandas dataframe containing the data to write out + :param filename: where to store it + """ # based on pandas data.to_csv(filename, index=None, @@ -366,12 +473,32 @@ def save_pd(data, filename): def load_ascii(filename): + """ + Load an ascii file into a numpy array + + Basic usage:: + + load_ascii(filename) + + :param filename: name of the file to load + """ + # based on pandas x = np.loadtxt(filename) return x def save_ascii(data, filename): + """ + Save a numpy array to an ascii file + + Basic usage:: + + save_ascii(data, filename) + + :param data: numpy array containing the data to write out + :param filename: where to store it + """ # based on pandas np.savetxt(filename, data) @@ -381,6 +508,21 @@ def save_ascii(data, filename): def save(data, filename, example=None, mask=None, text=False, dtype=None): + """ + Save a numpy array to a file + + Basic usage:: + + save(data, filename, example, mask, text, dtype) + + :param data: numpy array containing the data to write out + :param filename: where to store it + :param example: example file to copy the geometry from + :param mask: nifti image containing a mask for the image + :param text: whether to write out a text file + :param dtype: data type for the output image (if different from the image) + """ + if file_type(filename) == 'cifti': save_cifti(data.T, filename, example, vol=True) @@ -394,6 +536,18 @@ def save(data, filename, example=None, mask=None, text=False, dtype=None): def load(filename, mask=None, text=False, vol=True): + """ + Load a numpy array from a file + + Basic usage:: + + load(filename, mask, text, vol) + + :param filename: name of the file to load + :param mask: nifti image containing a mask for the image + :param text: whether to write out a text file + :param vol: whether to load the image as a volume + """ if file_type(filename) == 'cifti': x = load_cifti(filename, vol=vol) @@ -404,7 +558,6 @@ def load(filename, mask=None, text=False, vol=True): elif file_type(filename) == 'binary': x = pd.read_pickle(filename) x = x.to_numpy() - return x # ------------------- @@ -413,6 +566,16 @@ def load(filename, mask=None, text=False, vol=True): def tryint(s): + """ + Try to convert a string to an integer + + Basic usage:: + + tryint(s) + + :param s: string to convert + """ + try: return int(s) except ValueError: @@ -420,8 +583,27 @@ def tryint(s): def alphanum_key(s): + """ + Turn a string into a list of numbers + + Basic usage:: + + alphanum_key(s) + + :param s: string to convert + """ return [tryint(c) for c in re.split('([0-9]+)', s)] def sort_nicely(l): + """ + Sort a list of strings in a natural way + + Basic usage:: + + sort_nicely(l) + + :param l: list of strings to sort + """ + return sorted(l, key=alphanum_key) diff --git a/pcntoolkit/model/SHASH.py b/pcntoolkit/model/SHASH.py index 68bc8f69..3936255d 100644 --- a/pcntoolkit/model/SHASH.py +++ b/pcntoolkit/model/SHASH.py @@ -169,8 +169,7 @@ def rng_fn(cls, rng, epsilon, delta, size=None) -> np.ndarray: class SHASH(Continuous): rv_op = shash """ - SHASH described by Jones et al., based on a standard normal - All SHASH subclasses inherit from this + SHASH described by Jones et al., based on a standard normal distribution. """ @classmethod @@ -210,8 +209,7 @@ def rng_fn(cls, rng, mu, sigma, epsilon, delta, size=None) -> np.ndarray: class SHASHo(Continuous): rv_op = shasho """ - This is the shash where the location and scale parameters have simply been applied as an linear transformation - directly on the original shash. + This is the transformation where the location and scale parameters have simply been applied as an linear transformation directly on the original distribution. """ @classmethod @@ -257,7 +255,7 @@ def rng_fn(cls, rng, mu, sigma, epsilon, delta, size=None) -> np.ndarray: class SHASHo2(Continuous): rv_op = shasho2 """ - This is the shash where we apply the reparameterization provided in section 4.3 in Jones et al. + This is the reparameterization where we apply the transformation provided in section 4.3 in Jones et al. """ @classmethod @@ -316,8 +314,7 @@ def rng_fn( class SHASHb(Continuous): rv_op = shashb """ - This is the shash where the location and scale parameters been applied as an linear transformation on the shash - distribution which was corrected for mean and variance. + This is the reparameterization where the location and scale parameters been applied as an linear transformation on the shash distribution which was corrected for mean and variance. """ @classmethod diff --git a/pcntoolkit/model/bayesreg.py b/pcntoolkit/model/bayesreg.py index c353081d..91a62ebf 100755 --- a/pcntoolkit/model/bayesreg.py +++ b/pcntoolkit/model/bayesreg.py @@ -88,6 +88,14 @@ def __init__(self, **kwargs): self.gamma = None def _parse_hyps(self, hyp, X, Xv=None): + """ + Parse hyperparameters into noise precision, lengthscale precision and + lengthscale parameters. + + :param hyp: hyperparameter vector + :param X: covariates + :param Xv: covariates for heteroskedastic noise + """ N = X.shape[0] @@ -108,7 +116,7 @@ def _parse_hyps(self, hyp, X, Xv=None): beta = np.asarray([np.exp(hyp[0])]) n_lik_param = len(beta) - # parameters for warping the likelhood function + # parameters for warping the likelihood function if self.warp is not None: gamma = hyp[n_lik_param:(n_lik_param + self.n_warp_param)] n_lik_param += self.n_warp_param diff --git a/pcntoolkit/model/hbr.py b/pcntoolkit/model/hbr.py index 402c625e..ba1bea0e 100644 --- a/pcntoolkit/model/hbr.py +++ b/pcntoolkit/model/hbr.py @@ -35,6 +35,13 @@ def bspline_fit(X, order, nknots): + """ + Fit a B-spline to the data + :param X: [N×P] array of clinical covariates + :param order: order of the spline + :param nknots: number of knots + :return: a list of B-spline basis functions + """ feature_num = X.shape[1] bsp_basis = [] @@ -53,6 +60,13 @@ def bspline_fit(X, order, nknots): def bspline_transform(X, bsp_basis): + """ + Transform the data using the B-spline basis functions + :param X: [N×P] array of clinical covariates + :param bsp_basis: a list of B-spline basis functions + :return: a [N×(P×nknots)] array of transformed data + """ + if type(bsp_basis) != list: temp = [] temp.append(bsp_basis) @@ -68,8 +82,12 @@ def bspline_transform(X, bsp_basis): def create_poly_basis(X, order): - """compute a polynomial basis expansion of the specified order""" - + """ + Create a polynomial basis expansion of the specified order + :param X: [N×P] array of clinical covariates + :param order: order of the polynomial + :return: a [N×(P×order)] array of transformed data + """ if len(X.shape) == 1: X = X[:, np.newaxis] D = X.shape[1] @@ -82,6 +100,17 @@ def create_poly_basis(X, order): def from_posterior(param, samples, shape, distribution=None, half=False, freedom=1): + """ + Create a PyMC distribution from posterior samples + + :param param: name of the parameter + :param samples: samples from the posterior + :param shape: shape of the parameter + :param distribution: distribution to use for the parameter + :param half: if true, the distribution is assumed to be defined on the positive real line + :param freedom: freedom parameter for the distribution + :return: a PyMC distribution + """ if distribution is None: smin, smax = np.min(samples), np.max(samples) width = smax - smin @@ -169,17 +198,18 @@ def from_posterior(param, samples, shape, distribution=None, half=False, freedo def hbr(X, y, batch_effects, configs, idata=None): """ + Create a Hierarchical Bayesian Regression model + :param X: [N×P] array of clinical covariates :param y: [N×1] array of neuroimaging measures :param batch_effects: [N×M] array of batch effects - :param batch_effects_size: [b1, b2,...,bM] List of counts of unique values of batch effects :param configs: :param idata: :param return_shared_variables: If true, returns references to the shared variables. The values of the shared variables can be set manually, allowing running the same model on different data without re-compiling it. :return: """ - # Make a param builder that will make the correct calls + # Make a param builder that contains all the data and configs pb = ParamBuilder(X, y, batch_effects, idata, configs) def get_sample_dims(var): @@ -212,7 +242,6 @@ def get_sample_dims(var): ) if configs["likelihood"] == "Normal": - mu = pm.Deterministic( "mu_samples", pb.make_param( @@ -344,9 +373,20 @@ def __init__(self, configs): self.configs = configs def get_modeler(self): + """ + This used to return hbr or nnhbr, but now it returns hbr. + Can be removed in a future release + //TODO remove this in a future release + """ return hbr def transform_X(self, X): + """ + Transform the covariates according to the model type + + :param X: N-by-P input matrix of P features for N subjects + :return: transformed covariates + """ if self.model_type == "polynomial": Phi = create_poly_basis(X, self.configs["order"]) elif self.model_type == "bspline": @@ -359,7 +399,19 @@ def transform_X(self, X): return Phi def find_map(self, X, y, batch_effects, method="L-BFGS-B"): - """Function to estimate the model""" + """ + Find the maximum a posteriori (MAP) estimate of the model parameters. + + This function transforms the data according to the model type, + and then uses the modeler to find the MAP estimate of the model parameters. + The results are stored in the instance variable `MAP`. + + :param X: N-by-P input matrix of P features for N subjects. This is the input data for the model. + :param y: N-by-1 vector of outputs. This is the target data for the model. + :param batch_effects: N-by-B matrix of B batch ids for N subjects. This represents the batch effects to be considered in the model. + :param method: String representing the optimization method to use. Default is "L-BFGS-B". + :return: A dictionary of MAP estimates. + """ X, y, batch_effects = expand_all(X, y, batch_effects) X = self.transform_X(X) modeler = self.get_modeler() @@ -368,7 +420,19 @@ def find_map(self, X, y, batch_effects, method="L-BFGS-B"): return self.MAP def estimate(self, X, y, batch_effects, **kwargs): - """Function to estimate the model""" + """ + Estimate the model parameters using the provided data. + + This function transforms the data according to the model type, + and then samples from the posterior using pymc. The results are stored + in the instance variable `idata`. + + :param X: N-by-P input matrix of P features for N subjects. This is the input data for the model. + :param y: N-by-1 vector of outputs. This is the target data for the model. + :param batch_effects: N-by-B matrix of B batch ids for N subjects. This represents the batch effects to be considered in the model. + :param kwargs: Additional keyword arguments to be passed to the modeler. + :return: idata. The results are also stored in the instance variable `self.idata`. + """ X, y, batch_effects = expand_all(X, y, batch_effects) X = self.transform_X(X) modeler = self.get_modeler() @@ -404,11 +468,19 @@ def estimate(self, X, y, batch_effects, **kwargs): def predict( self, X, batch_effects, batch_effects_maps, pred="single", var_names=None, **kwargs ): - """Function to make predictions from the model - Args: - X: Covariates - batch_effects: batch effects corresponding to X - all_batch_effects: combinations of all batch effects that were present the training data + """ + Make predictions from the model. + + This function expands the input data, transforms it according to the model type, + and then uses the modeler to make predictions. The results are stored in the instance variable `idata`. + + :param X: Covariates. This is the input data for the model. + :param batch_effects: Batch effects corresponding to X. This represents the batch effects to be considered in the model. + :param batch_effects_maps: A map from batch_effect values to indices. This is used to map the batch effects to the indices used by the model. + :param pred: String representing the prediction method to use. Default is "single". + :param var_names: List of variable names to consider in the prediction. If None or ['y_like'], self.vars_to_sample is used. + :param kwargs: Additional keyword arguments to be passed to the modeler. + :return: A 2-tuple of xarray datasets with the mean and variance of the posterior predictive distribution. The results are also stored in the instance variable `self.idata`. """ X, batch_effects = expand_all(X, batch_effects) @@ -458,7 +530,17 @@ def predict( return pred_mean, pred_var def estimate_on_new_site(self, X, y, batch_effects): - """Function to adapt the model""" + """ + Estimate the model parameters using the provided data for a new site. + + This function transforms the input data, then uses the modeler to estimate the model parameters. + The results are stored in the instance variable `idata`. + + :param X: Covariates. This is the input data for the model. + :param y: Outputs. This is the target data for the model. + :param batch_effects: Batch effects corresponding to X. This represents the batch effects to be considered in the model. + :return: An inferencedata object containing samples from the posterior distribution. + """ X, y, batch_effects = expand_all(X, y, batch_effects) X = self.transform_X(X) modeler = self.get_modeler() @@ -475,7 +557,16 @@ def estimate_on_new_site(self, X, y, batch_effects): return self.idata def predict_on_new_site(self, X, batch_effects): - """Function to make predictions from the model""" + """ + Make predictions from the model for a new site. + + This function transforms the input data, then uses the modeler to make predictions. + The results are stored in the instance variable `idata`. + + :param X: Covariates. This is the input data for the model. + :param batch_effects: Batch effects corresponding to X. This represents the batch effects to be considered in the model. + :return: A tuple containing the mean and variance of the predictions. The results are also stored in the instance variable `self.idata`. + """ X, batch_effects = expand_all(X, batch_effects) samples = self.configs["n_samples"] y = np.zeros([X.shape[0], 1]) @@ -491,7 +582,16 @@ def predict_on_new_site(self, X, batch_effects): return pred_mean, pred_var def generate(self, X, batch_effects, samples): - """Function to generate samples from posterior predictive distribution""" + """ + Generate samples from the posterior predictive distribution. + + This function expands and transforms the input data, then uses the modeler to generate samples from the posterior predictive distribution. + + :param X: Covariates. This is the input data for the model. + :param batch_effects: Batch effects corresponding to X. This represents the batch effects to be considered in the model. + :param samples: Number of samples to generate. This number of samples is generated for each input sample. + :return: A tuple containing the expanded and repeated X, batch_effects, and the generated samples. + """ X, batch_effects = expand_all(X, batch_effects) y = np.zeros([X.shape[0], 1]) @@ -512,7 +612,18 @@ def generate(self, X, batch_effects, samples): return X, batch_effects, generated_samples def sample_prior_predictive(self, X, batch_effects, samples, y = None, idata=None): - """Function to sample from prior predictive distribution""" + """ + Sample from the prior predictive distribution. + + This function transforms the input data, then uses the modeler to sample from the prior predictive distribution. + + :param X: Covariates. This is the input data for the model. + :param batch_effects: Batch effects corresponding to X. This represents the batch effects to be considered in the model. + :param samples: Number of samples to generate. This number of samples is generated for each input sample. + :param y: Outputs. If None, a zero array of appropriate shape is created. + :param idata: An xarray dataset with the posterior distribution. If None, self.idata is used if it exists. + :return: An xarray dataset with the prior predictive distribution. The results are also stored in the instance variable `self.idata`. + """ if y is None: y = np.zeros([X.shape[0], 1]) X, y, batch_effects = expand_all(X, y, batch_effects) @@ -524,6 +635,16 @@ def sample_prior_predictive(self, X, batch_effects, samples, y = None, idata=Non return self.idata def get_model(self, X, y, batch_effects): + """ + Get the model for the given data. + + This function expands and transforms the input data, then creates a pymc model using the hbr method + + :param X: Covariates. This is the input data for the model. + :param y: Outputs. This is the target data for the model. + :param batch_effects: Batch effects corresponding to X. This represents the batch effects to be considered in the model. + :return: The model for the given data. + """ X, y, batch_effects = expand_all(X, y, batch_effects) modeler = self.get_modeler() X = self.transform_X(X) @@ -531,6 +652,16 @@ def get_model(self, X, y, batch_effects): return modeler(X, y, batch_effects, self.configs, idata=idata) def create_dummy_inputs(self, covariate_ranges=[[0.1, 0.9, 0.01]]): + + """ + Create dummy inputs for the model. + + This function generates a Cartesian product of the provided covariate ranges and repeats it for each batch effect. + It also generates a Cartesian product of the batch effect indices and repeats it for each input sample. + + :param covariate_ranges: List of lists, where each inner list represents the range and step size of a covariate. Default is [[0.1, 0.9, 0.01]]. + :return: A tuple containing the dummy input data and the dummy batch effects. + """ arrays = [] for i in range(len(covariate_ranges)): arrays.append( @@ -550,7 +681,16 @@ def create_dummy_inputs(self, covariate_ranges=[[0.1, 0.9, 0.01]]): return X_dummy, batch_effects_dummy def Rhats(self, var_names=None, thin = 1, resolution = 100): - """Get Rhat of posterior samples as function of sampling iteration""" + """ + Get Rhat of posterior samples as function of sampling iteration. + + This function extracts the posterior samples from the instance variable `idata`, computes the Rhat statistic for each variable and sampling iteration, and returns a dictionary of Rhat values. + + :param var_names: List of variable names to consider. If None, all variables in `idata` are used. + :param thin: Integer representing the thinning factor for the samples. Default is 1. + :param resolution: Integer representing the number of points at which to compute the Rhat statistic. Default is 100. + :return: A dictionary where the keys are variable names and the values are arrays of Rhat values. + """ idata = self.idata testvars = az.extract(idata, group='posterior', var_names=var_names, combined=False) testvar_names = [var for var in list(testvars.data_vars.keys()) if not '_samples' in var] @@ -575,6 +715,18 @@ class Prior: """ def __init__(self, name, dist, params, pb, has_random_effect=False) -> None: + """ + Initialize the Prior object. + + This function initializes the Prior object with the given name, distribution, parameters, and model. + It also sets a flag indicating whether the prior has a random effect. + + :param name: String representing the name of the prior. + :param dist: String representing the type of the distribution. + :param params: Dictionary of parameters for the distribution. + :param pb: The model object. + :param has_random_effect: Boolean indicating whether the prior has a random effect. Default is False. + """ self.dist = None self.name = name self.has_random_effect = has_random_effect @@ -591,7 +743,16 @@ def __init__(self, name, dist, params, pb, has_random_effect=False) -> None: self.make_dist(dist, params, pb) def make_dist(self, dist, params, pb): - """This creates a pymc distribution. If there is a idata, the distribution is fitted to the idata. If there isn't a idata, the prior is parameterized by the values in (params)""" + """ + Create a PyMC distribution. + + This function creates a PyMC distribution. If there is an `idata` present, the distribution is fitted to the `idata`. + If there isn't an `idata`, the prior is parameterized by the values in `params`. + + :param dist: String representing the type of the distribution. + :param params: List of parameters for the distribution. + :param pb: The model object. + """ with pb.model as m: if pb.idata is not None: # Get samples @@ -629,7 +790,15 @@ def get_new_dim_size(tup): self.dist = self.distmap[dist](self.name, *params, dims=dims) def __getitem__(self, idx): - """The idx here is the index of the batch-effect. If the prior does not model batch effects, this should return the same value for each index""" + """ + Retrieve the distribution for a specific batch effect. + + This function retrieves the distribution for a specific batch effect. + If the prior does not model batch effects, this should return the same value for each index. + + :param idx: Index of the batch effect. + :return: The distribution for the specified batch effect. + """ assert self.dist is not None, "Distribution not initialized" if self.has_random_effect: return self.dist[idx] @@ -646,7 +815,6 @@ class ParamBuilder: def __init__(self, X, y, batch_effects, idata, configs): """ - :param model: model to attach all the distributions to :param X: Covariates :param y: IDPs @@ -681,6 +849,18 @@ def __init__(self, X, y, batch_effects, idata, configs): self.batch_effect_indices.append(this_be_indices) def make_param(self, name, **kwargs): + """ + Create a parameterization based on the configuration. + + This function creates a parameterization based on the configuration. + If the configuration specifies a linear parameterization, it creates a slope and intercept and uses those to make a linear parameterization. + If the configuration specifies a random parameterization, it creates a random parameterization, either centered or non-centered. + Otherwise, it creates a fixed parameterization. + + :param name: String representing the name of the parameter. + :param kwargs: Additional keyword arguments to be passed to the parameterization. + :return: The created parameterization. + """ if self.configs.get(f"linear_{name}", False): # First make a slope and intercept, and use those to make a linear parameterization slope_parameterization = self.make_param(f"slope_{name}", **kwargs) @@ -709,36 +889,79 @@ class Parameterization: """ def __init__(self, name): + """ + Initialize the Parameterization object. + + This function initializes the Parameterization object with the given name. + + :param name: String representing the name of the parameterization. + """ self.name = name # print(name, type(self)) def get_samples(self, pb): + """ + Get samples from the parameterization. + + This function should be overridden by subclasses to provide specific sampling methods. + + :param pb: The ParamBuilder object. + :return: None. This method should be overridden by subclasses. + """ pass class FixedParameterization(Parameterization): """ - A parameterization that takes a single value for all input. It does not depend on anything except its hyperparameters - """ + A parameterization that takes a single value for all input. + It does not depend on anything except its hyperparameters. This class inherits from the Parameterization class. + """ def __init__(self, name, pb: ParamBuilder, **kwargs): + """ + Initialize the FixedParameterization object. + + This function initializes the FixedParameterization object with the given name, ParamBuilder object, and additional arguments. + + :param name: String representing the name of the parameterization. + :param pb: The ParamBuilder object. + :param kwargs: Additional keyword arguments to be passed to the parameterization. + """ super().__init__(name) dist = kwargs.get(f"{name}_dist", "normal") params = kwargs.get(f"{name}_params", (0.0, 1.0)) self.dist = Prior(name, dist, params, pb) def get_samples(self, pb): + """ + Get samples from the parameterization. + + This function gets samples from the parameterization using the ParamBuilder object. + + :param pb: The ParamBuilder object. + :return: The samples from the parameterization. + """ with pb.model: return self.dist[0] class CentralRandomFixedParameterization(Parameterization): """ - A parameterization that is fixed for each batch effect. This is sampled in a central fashion; - the values are sampled from normal distribution with a group mean and group variance + A parameterization that is fixed for each batch effect. + + This is sampled in a central fashion; the values are sampled from normal distribution with a group mean and group variance """ def __init__(self, name, pb: ParamBuilder, **kwargs): + """ + Initialize the CentralRandomFixedParameterization object. + + This function initializes the CentralRandomFixedParameterization object with the given name, ParamBuilder object, and additional arguments. + + :param name: String representing the name of the parameterization. + :param pb: The ParamBuilder object. + :param kwargs: Additional keyword arguments to be passed to the parameterization. + """ super().__init__(name) # Normal distribution is default for mean @@ -764,6 +987,14 @@ def __init__(self, name, pb: ParamBuilder, **kwargs): ) def get_samples(self, pb: ParamBuilder): + """ + Get samples from the parameterization. + + This function gets samples from the parameterization using the ParamBuilder object. + + :param pb: The ParamBuilder object. + :return: The samples from the parameterization. + """ with pb.model: samples = self.dist[pb.batch_effect_indices] return samples @@ -776,6 +1007,15 @@ class NonCentralRandomFixedParameterization(Parameterization): """ def __init__(self, name, pb: ParamBuilder, **kwargs): + """ + Initialize the NonCentralRandomFixedParameterization object. + + This function initializes the NonCentralRandomFixedParameterization object with the given name, ParamBuilder object, and additional arguments. + + :param name: String representing the name of the parameterization. + :param pb: The ParamBuilder object. + :param kwargs: Additional keyword arguments to be passed to the parameterization. + """ super().__init__(name) # Normal distribution is default for mean @@ -806,6 +1046,14 @@ def __init__(self, name, pb: ParamBuilder, **kwargs): ) def get_samples(self, pb: ParamBuilder): + """ + Get samples from the parameterization. + + This function gets samples from the parameterization using the ParamBuilder object. + + :param pb: The ParamBuilder object. + :return: The samples from the parameterization. + """ with pb.model: samples = self.dist[pb.batch_effect_indices] return samples @@ -813,17 +1061,36 @@ def get_samples(self, pb: ParamBuilder): class LinearParameterization(Parameterization): """ - A parameterization that can model a linear dependence on X. + This class inherits from the Parameterization class and represents a parameterization that can model a linear dependence on X. + """ def __init__( self, name, slope_parameterization, intercept_parameterization, **kwargs ): + """ + Initialize the LinearParameterization object. + + This function initializes the LinearParameterization object with the given name, slope parameterization, intercept parameterization, and additional arguments. + + :param name: String representing the name of the parameterization. + :param slope_parameterization: An instance of a Parameterization subclass representing the slope. + :param intercept_parameterization: An instance of a Parameterization subclass representing the intercept. + :param kwargs: Additional keyword arguments to be passed to the parameterization. + """ super().__init__(name) self.slope_parameterization = slope_parameterization self.intercept_parameterization = intercept_parameterization def get_samples(self, pb): + """ + Get samples from the parameterization. + + This function gets samples from the parameterization using the ParamBuilder object. It computes the samples as the sum of the intercept and the product of X and the slope. + + :param pb: The ParamBuilder object. + :return: The samples from the parameterization. + """ with pb.model: intc = self.intercept_parameterization.get_samples(pb) slope_samples = self.slope_parameterization.get_samples(pb) @@ -838,6 +1105,15 @@ def get_samples(self, pb): def get_design_matrix(X, nm, basis="linear"): + """ + Get the design matrix for the given data. + + This function gets the design matrix for the given data. + + :param X: Covariates. This is the input data for the model. + :param nm: A normative model. + :param basis: String representing the basis to use. Default is "linear". + """ if basis == "bspline": Phi = bspline_transform(X, nm.hbr.bsp) elif basis == "polynomial": diff --git a/pcntoolkit/normative.py b/pcntoolkit/normative.py index 97898c5e..95688018 100755 --- a/pcntoolkit/normative.py +++ b/pcntoolkit/normative.py @@ -49,7 +49,18 @@ PICKLE_PROTOCOL = configs.PICKLE_PROTOCOL def load_response_vars(datafile, maskfile=None, vol=True): - """ load response variables (of any data type)""" + """ + Load response variables from file. This will load the data and mask it if + necessary. If the data is in ascii format it will be converted into a numpy + array. If the data is in neuroimaging format it will be reshaped into a + 2D array (subjects x variables) and a mask will be created if necessary. + + :param datafile: File containing the response variables + :param maskfile: Mask file (nifti only) + :param vol: If True, load the data as a 4D volume (nifti only) + :returns Y: Response variables + :returns volmask: Mask file (nifti only) + """ if fileio.file_type(datafile) == 'nifti': dat = fileio.load_nifti(datafile, vol=vol) @@ -65,7 +76,22 @@ def load_response_vars(datafile, maskfile=None, vol=True): def get_args(*args): - """ Parse command line arguments""" + """ + Parse command line arguments for normative modeling + + :param args: command line arguments + :returns respfile: response variables for the normative model + :returns maskfile: mask used to apply to the data (nifti only) + :returns covfile: covariates used to predict the response variable + :returns cvfolds: Number of cross-validation folds + :returns testcov: Test covariates + :returns testresp: Test responses + :returns func: Function to call + :returns alg: Algorithm for normative model + :returns configparam: Parameters controlling the estimation algorithm + :returns kw_args: Additional keyword arguments + """ + # parse arguments parser = argparse.ArgumentParser(description="Normative Modeling") @@ -219,6 +245,23 @@ def evaluate(Y, Yhat, S2=None, mY=None, sY=None, nlZ=None, nm=None, Xz_tr=None, def save_results(respfile, Yhat, S2, maskvol, Z=None, Y=None, outputsuffix=None, results=None, save_path=''): + """ + Writes the results of the normative model to disk. + + Parameters: + respfile (str): The response variables file. + Yhat (np.array): The predicted response variables. + S2 (np.array): The predictive variance. + maskvol (np.array): The mask volume. + Z (np.array, optional): The latent variable. Defaults to None. + Y (np.array, optional): The observed response variables. Defaults to None. + outputsuffix (str, optional): The suffix to append to the output files. Defaults to None. + results (dict, optional): The results of the normative model. Defaults to None. + save_path (str, optional): The directory to save the results to. Defaults to ''. + + Returns: + None + """ print("Writing outputs ...") if respfile is None: @@ -255,6 +298,7 @@ def save_results(respfile, Yhat, S2, maskvol, Z=None, Y=None, outputsuffix=None, else: fileio.save(results[metric], os.path.join(save_path, metric + ext), example=exfile, mask=maskvol) + def estimate(covfile, respfile, **kwargs): """ Estimate a normative model @@ -578,6 +622,22 @@ def estimate(covfile, respfile, **kwargs): def fit(covfile, respfile, **kwargs): + """ + Fits a normative model to the data. + + Parameters: + covfile (str): The path to the covariates file. + respfile (str): The path to the response variables file. + maskfile (str, optional): The path to the mask file. Defaults to None. + alg (str, optional): The algorithm to use. Defaults to 'gpr'. + savemodel (bool, optional): Whether to save the model. Defaults to True. + outputsuffix (str, optional): The suffix to append to the output files. Defaults to 'fit'. + inscaler (str, optional): The scaler to use for the input data. Defaults to 'None'. + outscaler (str, optional): The scaler to use for the output data. Defaults to 'None'. + + Returns: + None + """ # parse keyword arguments maskfile = kwargs.pop('maskfile',None) diff --git a/pcntoolkit/normative_NP.py b/pcntoolkit/normative_NP.py index 3694e146..9392f3c6 100644 --- a/pcntoolkit/normative_NP.py +++ b/pcntoolkit/normative_NP.py @@ -48,7 +48,26 @@ import configs def get_args(*args): - """ Parse command line arguments""" + """ + Parses command-line arguments for the Neural Processes (NP) for Deep Normative Modeling script. + + Parameters: + *args: Variable length argument list. + + Returns: + argparse.Namespace: An object that holds the command-line arguments as attributes. The arguments include: + - respfile: Training response nifti file address. + - covfile: Training covariates pickle file address. + - testcovfile: Test covariates pickle file address. + - testrespfile: Test response nifti file address. + - mask: Mask nifti file address. + - outdir: Output directory address. + - m: Number of fixed-effect estimations. + - batchnum: Input batch size for training. + - epochs: Number of epochs to train. + - device: Either cpu or cuda. + - estimator: Fixed-effect estimator type. + """ ############################ Parsing inputs ############################### @@ -91,6 +110,27 @@ def get_args(*args): return args def estimate(args): + """ + Estimates the fixed-effects for the Neural Processes (NP) for Deep Normative Modeling script. + + Parameters: + args (argparse.Namespace): An object that holds the command-line arguments as attributes. The arguments include: + - respfile: Training response nifti file address. + - covfile: Training covariates pickle file address. + - testcovfile: Test covariates pickle file address. + - mask: Mask nifti file address. + - outdir: Output directory address. + - m: Number of fixed-effect estimations. + - device: Either cpu or cuda. + - estimator: Fixed-effect estimator type. + + Returns: + None + + This function loads the input data, normalizes it, and estimates the fixed-effects using either single-task (ST) + or multi-task (MT) regression. The results are stored in the `y_context` and `y_context_test` variables. + """ + torch.set_default_dtype(torch.float32) args.type = 'MT' print('Loading the input Data ...') diff --git a/pcntoolkit/normative_model/norm_blr.py b/pcntoolkit/normative_model/norm_blr.py index 814e0b08..b3083797 100644 --- a/pcntoolkit/normative_model/norm_blr.py +++ b/pcntoolkit/normative_model/norm_blr.py @@ -32,6 +32,21 @@ class NormBLR(NormBase): """ def __init__(self, **kwargs): + """ + Initialize the NormBLR object. + + This function initializes the NormBLR object with the given arguments. It requires a data matrix 'X' and optionally takes a target 'y' and parameters 'theta'. + It also configures the model order and heteroskedastic noise if specified in the arguments. + + :param kwargs: Keyword arguments which should include: + - 'X': Data matrix. Must be specified. + - 'y': Target values. Optional. + - 'theta': Parameters for the model. Optional. + - 'optimizer': The optimization algorithm to use. Default is 'powell'. + - 'configparam' or 'model_order': The order of the model. Default is 1. + - 'varcovfile': File containing the variance-covariance matrix for heteroskedastic noise. Optional. + :raises ValueError: If 'X' is not specified in kwargs. + """ X = kwargs.pop('X', None) y = kwargs.pop('y', None) theta = kwargs.pop('theta', None) @@ -141,6 +156,20 @@ def neg_log_lik(self): return self.blr.nlZ def estimate(self, X, y, **kwargs): + """ + Estimate the parameters of the model. + + This function estimates the parameters of the model given the data matrix 'X' and target 'y'. + If 'theta' is provided in kwargs, it is used as the initial guess for the parameters. + Otherwise, the initial guess is set to the current value of 'self.theta'. + + :param X: Data matrix. + :param y: Target values. + :param kwargs: Keyword arguments which may include: + - 'theta': Initial guess for the parameters. Optional. + - 'warp': String representing the warp function. It is removed from kwargs before passing to the BLR object. + :return: The instance of the NormBLR object. + """ theta = kwargs.pop('theta', None) if isinstance(theta, str): theta = np.array(literal_eval(theta)) @@ -166,6 +195,28 @@ def estimate(self, X, y, **kwargs): return self def predict(self, Xs, X=None, y=None, **kwargs): + """ + Predict the target values for the given test data. + + This function predicts the target values for the given test data 'Xs' using the estimated parameters of the model. + If 'X' and 'y' are provided, they are used to update the model before prediction. + + :param Xs: Test data matrix. + :param X: Training data matrix. Optional. + :param y: Training target values. Optional. + :param kwargs: Keyword arguments which may include: + - 'testvargroup': Variance groups for the test data. Optional. + - 'testvargroupfile': File containing the variance groups for the test data. Optional. + - 'testvarcov': Variance covariates for the test data. Optional. + - 'testvarcovfile': File containing the variance covariates for the test data. Optional. + - 'adaptresp': Responses to adapt to. Optional. + - 'adaptrespfile': File containing the responses to adapt to. Optional. + - 'adaptcov': Covariates to adapt to. Optional. + - 'adaptcovfile': File containing the covariates to adapt to. Optional. + - 'adaptvargroup': Variance groups to adapt to. Optional. + - 'adaptvargroupfile': File containing the variance groups to adapt to. Optional. + :return: The predicted target values for the test data. + """ theta = self.theta # always use the estimated coefficients # remove from kwargs to avoid downstream problems diff --git a/pcntoolkit/normative_model/norm_gpr.py b/pcntoolkit/normative_model/norm_gpr.py index a74cc95b..c7926f8c 100644 --- a/pcntoolkit/normative_model/norm_gpr.py +++ b/pcntoolkit/normative_model/norm_gpr.py @@ -24,6 +24,17 @@ class NormGPR(NormBase): """ def __init__(self, **kwargs): #X=None, y=None, theta=None, + """ + Initialize the NormGPR object. + + This function initializes the NormGPR object with the given arguments. It requires a data matrix 'X' and optionally takes a target 'y' and parameters 'theta'. + It also initializes the covariance function and the Gaussian Process Regression (GPR) model. + + :param kwargs: Keyword arguments which should include: + - 'X': Data matrix. Must be specified. + - 'y': Target values. Optional. + - 'theta': Parameters for the model. Optional. + """ X = kwargs.pop('X', None) y = kwargs.pop('y', None) theta = kwargs.pop('theta', None) @@ -50,6 +61,20 @@ def neg_log_lik(self): return self.gpr.nlZ def estimate(self, X, y, **kwargs): + """ + Estimate the parameters of the Gaussian Process Regression model. + + This function estimates the parameters of the Gaussian Process Regression (GPR) model given the data matrix 'X' and target 'y'. + If 'theta' is provided in kwargs, it is used as the initial guess for the parameters. + Otherwise, the initial guess is set to the current value of 'self.theta0'. + + :param X: Data matrix. + :param y: Target values. + :param kwargs: Keyword arguments which may include: + - 'theta': Initial guess for the parameters. Optional. + :return: The instance of the NormGPR object. + """ + theta = kwargs.pop('theta', None) if theta is None: theta = self.theta0 @@ -59,6 +84,21 @@ def estimate(self, X, y, **kwargs): return self def predict(self, Xs, X, y, **kwargs): + """ + Predict the target values for the given test data. + + This function predicts the target values for the given test data 'Xs' using the estimated parameters of the Gaussian Process Regression (GPR) model. + If 'X' and 'y' are provided, they are used to update the model before prediction. + If 'theta' is provided in kwargs, it is used as the parameters for prediction. + Otherwise, the current value of 'self.theta' is used. + + :param Xs: Test data matrix. + :param X: Training data matrix. Optional. + :param y: Training target values. Optional. + :param kwargs: Keyword arguments which may include: + - 'theta': Parameters for prediction. Optional. + :return: A tuple containing the predicted target values and the marginal variances for the test data. + """ theta = kwargs.pop('theta', None) if theta is None: theta = self.theta diff --git a/pcntoolkit/normative_model/norm_hbr.py b/pcntoolkit/normative_model/norm_hbr.py index e4718071..e71fe78b 100644 --- a/pcntoolkit/normative_model/norm_hbr.py +++ b/pcntoolkit/normative_model/norm_hbr.py @@ -103,7 +103,7 @@ class NormHBR(NormBase): is important for some convergence checks. :param cores: String that specifies the number of chains to run in parallel. (defauls is '1'). - :param Initialization method to use for auto-assigned NUTS samplers. The + :param init: Initialization method to use for auto-assigned NUTS samplers. The defauls is 'jitter+adapt_diag' that starts with a identity mass matrix and then adapt a diagonal based on the variance of the tuning samples while adding a uniform jitter in [-1, 1] to the starting point in each chain. @@ -251,6 +251,19 @@ def neg_log_lik(self): return -1 def estimate(self, X, y, **kwargs): + """ + Sample from the posterior of the Hierarchical Bayesian Regression model. + + This function samples from the posterior distribution of the Hierarchical Bayesian Regression (HBR) model given the data matrix 'X' and target 'y'. + If 'trbefile' is provided in kwargs, it is used as batch effects for the training data. + Otherwise, the batch effects are initialized as zeros. + + :param X: Data matrix. + :param y: Target values. + :param kwargs: Keyword arguments which may include: + - 'trbefile': File containing the batch effects for the training data. Optional. + :return: The instance of the NormHBR object. + """ trbefile = kwargs.get("trbefile", None) if trbefile is not None: batch_effects_train = fileio.load(trbefile) @@ -268,6 +281,22 @@ def estimate(self, X, y, **kwargs): return self def predict(self, Xs, X=None, Y=None, **kwargs): + """ + Predict the target values for the given test data. + + This function predicts the target values for the given test data 'Xs' using the Hierarchical Bayesian Regression (HBR) model. + If 'X' and 'Y' are provided, they are used to update the model before prediction. + If 'tsbefile' is provided in kwargs, it is used to as batch effects for the test data. + Otherwise, the batch effects are initialized as zeros. + + :param Xs: Test data matrix. + :param X: Training data matrix. Optional. + :param Y: Training target values. Optional. + :param kwargs: Keyword arguments which may include: + - 'tsbefile': File containing the batch effects for the test data. Optional. + :return: A tuple containing the predicted target values and the marginal variances for the test data. + :raises ValueError: If the model is a transferred model. In this case, use the predict_on_new_sites function. + """ tsbefile = kwargs.get("tsbefile", None) if tsbefile is not None: batch_effects_test = fileio.load(tsbefile) @@ -295,11 +324,34 @@ def predict(self, Xs, X=None, Y=None, **kwargs): def estimate_on_new_sites(self, X, y, batch_effects): + """ + Samples from the posterior of the Hierarchical Bayesian Regression model. + + This function samples from the posterior of the Hierarchical Bayesian Regression (HBR) model given the data matrix 'X' and target 'y'. The posterior samples from the previous iteration are used to construct the priors for this one. + If 'trbefile' is provided in kwargs, it is used as batch effects for the training data. + Otherwise, the batch effects are initialized as zeros. + + :param X: Data matrix. + :param y: Target values. + :param kwargs: Keyword arguments which may include: + - 'trbefile': File containing the batch effects for the training data. Optional. + :return: The instance of the NormHBR object. + """ self.hbr.estimate_on_new_site(X, y, batch_effects) self.configs["transferred"] = True return self def predict_on_new_sites(self, X, batch_effects): + """ + Predict the target values for the given test data on new sites. + + This function predicts the target values for the given test data 'X' on new sites using the Hierarchical Bayesian Regression (HBR) model. + The batch effects for the new sites must be provided. + + :param X: Test data matrix for the new sites. + :param batch_effects: Batch effects for the new sites. + :return: A tuple containing the predicted target values and the marginal variances for the test data on the new sites. + """ yhat, s2 = self.hbr.predict_on_new_site(X, batch_effects) return yhat, s2 @@ -313,6 +365,23 @@ def extend( samples=10, informative_prior=False, ): + + """ + Extend the Hierarchical Bayesian Regression model using data sampled from the posterior predictive distribution. + + This function extends the Hierarchical Bayesian Regression (HBR) model, given the data matrix 'X' and target 'y'. + It also generates data from the posterior predictive distribution and merges it with the new data before estimation. + If 'informative_prior' is True, it uses the adapt method for estimation. Otherwise, it uses the estimate method. + + :param X: Data matrix for the new sites. + :param y: Target values for the new sites. + :param batch_effects: Batch effects for the new sites. + :param X_dummy_ranges: Ranges for generating the dummy data. Default is [[0.1, 0.9, 0.01]]. + :param merge_batch_dim: Dimension for merging the batch effects. Default is 0. + :param samples: Number of samples to generate for the dummy data. Default is 10. + :param informative_prior: Whether to use the adapt method for estimation. Default is False. + :return: The instance of the NormHBR object. + """ X_dummy, batch_effects_dummy = self.hbr.create_dummy_inputs(X_dummy_ranges) X_dummy, batch_effects_dummy, Y_dummy = self.hbr.generate( @@ -350,8 +419,11 @@ def tune( samples=10, informative_prior=False, ): + + #TODO need to check if this is correct + tune_ids = list(np.unique(batch_effects[:, merge_batch_dim])) - + X_dummy, batch_effects_dummy = self.hbr.create_dummy_inputs(X_dummy_ranges) for idx in tune_ids: @@ -382,6 +454,17 @@ def tune( def merge( self, nm, X_dummy_ranges=[[0.1, 0.9, 0.01]], merge_batch_dim=0, samples=10 ): + """ + Samples from the posterior predictive distribitions of two models, merges them, and estimates a model on the merged data. + + This function samples from the posterior predictive distribitions of two models, merges them, and estimates a model on the merged data. + + :param nm: The other NormHBR object. + :param X_dummy_ranges: Ranges for generating the dummy data. Default is [[0.1, 0.9, 0.01]]. + :param merge_batch_dim: Dimension for merging the batch effects. Default is 0. + :param samples: Number of samples to generate for the dummy data. Default is 10. + """ + X_dummy1, batch_effects_dummy1 = self.hbr.create_dummy_inputs(X_dummy_ranges) X_dummy2, batch_effects_dummy2 = nm.hbr.create_dummy_inputs(X_dummy_ranges) diff --git a/pcntoolkit/normative_model/norm_np.py b/pcntoolkit/normative_model/norm_np.py index 14bdd291..7986cc46 100755 --- a/pcntoolkit/normative_model/norm_np.py +++ b/pcntoolkit/normative_model/norm_np.py @@ -37,7 +37,30 @@ class struct(object): pass class Encoder(nn.Module): + """ + Encoder module for the Neural Process Regression model. + + This module is responsible for encoding the input data into a latent representation. + It is a part of the Neural Process Regression (NPR) model and is implemented as a PyTorch module. + + :param x: Input data matrix. + :param y: Target values. + :param args: A dictionary-like object containing the following attributes: + - r_dim: Dimension of the latent representation. + - z_dim: Dimension of the latent variable. + - hidden_neuron_num: Number of neurons in the hidden layers. + """ def __init__(self, x, y, args): + """ + Initialize the Encoder module. + + :param x: Input data matrix. + :param y: Target values. + :param args: A dictionary-like object containing the following attributes: + - r_dim: Dimension of the latent representation. + - z_dim: Dimension of the latent variable. + - hidden_neuron_num: Number of neurons in the hidden layers. + """ super(Encoder, self).__init__() self.r_dim = args.r_dim self.z_dim = args.z_dim @@ -47,6 +70,14 @@ def __init__(self, x, y, args): self.h_3 = nn.Linear(self.hidden_neuron_num, self.r_dim) def forward(self, x, y): + """ + Forward pass of the Encoder module. + + :param x: Input data matrix. + :param y: Target values. + :return: The latent representation of the input data. + """ + x_y = torch.cat([x, y], dim=2) x_y = F.relu(self.h_1(x_y)) x_y = F.relu(self.h_2(x_y)) @@ -56,7 +87,30 @@ def forward(self, x, y): class Decoder(nn.Module): + """ + Decoder module for the Neural Process Regression model. + + This module is responsible for decoding the latent representation into the target values. + It is a part of the Neural Process Regression (NPR) model and is implemented as a PyTorch module. + + :param x: Input data matrix. + :param y: Target values. + :param args: A dictionary-like object containing the following attributes: + - r_dim: Dimension of the latent representation. + - z_dim: Dimension of the latent variable. + - hidden_neuron_num: Number of neurons in the hidden layers. + """ def __init__(self, x, y, args): + """ + Initialize the Decoder module. + + :param x: Input data matrix. + :param y: Target values. + :param args: A dictionary-like object containing the following attributes: + - r_dim: Dimension of the latent representation. + - z_dim: Dimension of the latent variable. + - hidden_neuron_num: Number of neurons in the hidden layers. + """ super(Decoder, self).__init__() self.r_dim = args.r_dim self.z_dim = args.z_dim @@ -71,6 +125,12 @@ def __init__(self, x, y, args): self.g_3_84 = nn.Linear(self.hidden_neuron_num, y.shape[1]) def forward(self, z_sample): + """ + Forward pass of the Decoder module. + + :param z_sample: Sampled latent variable. + :return: The predicted target values. + """ z_hat = F.relu(self.g_1(z_sample)) z_hat = F.relu(self.g_2(z_hat)) y_hat = torch.sigmoid(self.g_3(z_hat)) @@ -89,6 +149,17 @@ class NormNP(NormBase): """ def __init__(self, X, y, configparam=None): + """ + Initialize the NormNP object. + + This function initializes the NormNP object with the given arguments. It requires a data matrix 'X' and target 'y'. + It also takes an optional 'configparam' which is a path to a pickle file containing configuration parameters. + If 'configparam' is not provided, default values are used for the configuration parameters. + + :param X: Data matrix. + :param y: Target values. + :param configparam: Path to a pickle file containing configuration parameters. Optional. + """ self.configparam = configparam if configparam is not None: with open(configparam, 'rb') as handle: @@ -155,6 +226,16 @@ def neg_log_lik(self): return -1 def estimate(self, X, y): + """ + Estimate the parameters of the Neural Process Regression model. + + This function estimates the parameters of the Neural Process Regression (NPR) model given the data matrix 'X' and target 'y'. + It uses mini-batch gradient descent for optimization and updates the model parameters in place. + + :param X: Data matrix. + :param y: Target values. If y is one-dimensional, it is reshaped to (-1, 1). + :return: The instance of the norm_np object with updated parameters. + """ if y.ndim == 1: y = y.reshape(-1,1) sample_num = X.shape[0] @@ -207,6 +288,17 @@ def estimate(self, X, y): return self def predict(self, Xs, X=None, Y=None, theta=None): + """ + Predict the target values for the given test data. + + This function predicts the target values for the given test data 'Xs' using the Neural Process Regression (NPR) model. + + :param Xs: Test data matrix. + :param X: Not used in this function. + :param Y: Not used in this function. + :param theta: Not used in this function. + :return: A tuple containing the predicted target values and the marginal variances for the test data. + """ sample_num = Xs.shape[0] factor_num = self.args.m x_context_test = np.zeros([sample_num, factor_num, Xs.shape[1]]) diff --git a/pcntoolkit/normative_model/norm_rfa.py b/pcntoolkit/normative_model/norm_rfa.py index f60e731a..39b7cf5d 100644 --- a/pcntoolkit/normative_model/norm_rfa.py +++ b/pcntoolkit/normative_model/norm_rfa.py @@ -24,7 +24,18 @@ class NormRFA(NormBase): """ def __init__(self, X, y=None, theta=None, n_feat=None): - + """ + Initialize the NormRFA object. + + This function initializes the NormRFA object with the given arguments. It requires a data matrix 'X' and optionally takes a target 'y', parameters 'theta', and the number of random features 'n_feat'. + It initializes the Gaussian Process Regression with Random Feature Approximation (GPRRFA) model and sets the initial parameters. + + :param X: Data matrix. Must be specified. + :param y: Not used. + :param theta: Parameters for the model. Optional. + :param n_feat: Number of random features for the GPRRFA model. Optional. + :raises ValueError: If 'X' is not specified. + """ if (X is not None): if n_feat is None: print("initialising RFA") @@ -56,6 +67,18 @@ def neg_log_lik(self): return self.gprrfa.nlZ def estimate(self, X, y, theta=None): + """ + Estimate the parameters of the Random Feature Approximation model. + + This function estimates the parameters of the Random Feature Approximation (RFA) model given the data matrix 'X' and target 'y'. + If 'theta' is provided, it is used as the initial parameters for estimation. + Otherwise, the current value of 'self.theta0' is used. + + :param X: Data matrix. + :param y: Target values. + :param theta: Initial parameters for estimation. Optional. + :return: The instance of the NormRFA object with updated parameters. + """ if theta is None: theta = self.theta0 self.gprrfa = GPRRFA(theta, X, y) @@ -64,6 +87,20 @@ def estimate(self, X, y, theta=None): return self def predict(self, Xs, X, y, theta=None): + """ + Predict the target values for the given test data. + + This function predicts the target values for the given test data 'Xs' using the Random Feature Approximation (RFA) model. + If 'X' and 'y' are provided, they are used to update the model before prediction. + If 'theta' is provided, it is used as the parameters for prediction. + Otherwise, the current value of 'self.theta' is used. + + :param Xs: Test data matrix. + :param X: Training data matrix. + :param y: Training target values. + :param theta: Parameters for prediction. Optional. + :return: A tuple containing the predicted target values and the marginal variances for the test data. + """ if theta is None: theta = self.theta yhat, s2 = self.gprrfa.predict(theta, X, y, Xs) diff --git a/pcntoolkit/trendsurf.py b/pcntoolkit/trendsurf.py index f009ad29..3695379d 100644 --- a/pcntoolkit/trendsurf.py +++ b/pcntoolkit/trendsurf.py @@ -32,7 +32,20 @@ def load_data(datafile, maskfile=None): - """ load 4d nifti data """ + """ + Load data from disk + + This will load data from disk, either in nifti or ascii format. If the + data are in ascii format, they should be in tab or space delimited format + with the number of voxels in rows and the number of subjects in columns. + Neuroimaging data will be reshaped into the appropriate format + + :param datafile: 4-d nifti file containing the images to be estimated + :param maskfile: nifti mask used to apply to the data + :returns: * dat - data in vectorised form + * world - voxel coordinates + * mask - mask used to apply to the data + """ if datafile.endswith("nii.gz") or datafile.endswith("nii"): # we load the data this way rather than fileio.load() because we need # access to the volumetric representation (to know the # coordinates) @@ -63,7 +76,22 @@ def load_data(datafile, maskfile=None): def create_basis(X, basis, mask): - """ Create a (polynomial) basis set """ + """ + Create a basis set + + This will create a basis set for the trend surface model. This is + currently fit using a polynomial model of a specified degree. The models + are estimated on the basis of data stored on disk in ascii or + neuroimaging data formats (currently nifti only). Ascii data should be in + tab or space delimited format with the number of voxels in rows and the + number of subjects in columns. Neuroimaging data will be reshaped + into the appropriate format + + :param X: covariates + :param basis: model order for the interpolating polynomial + :param mask: mask used to apply to the data + :returns: * Phi - basis set + """ # check whether we are using a polynomial basis set if type(basis) is int or (type(basis) is str and len(basis) == 1): @@ -92,8 +120,18 @@ def create_basis(X, basis, mask): def write_nii(data, filename, examplenii, mask): - """ Write output to nifti """ + """ + Write data to nifti file + + This will write data to a nifti file, using the header information from + an example nifti file. + :param data: data to be written + :param filename: name of file to be written + :param examplenii: example nifti file + :param mask: mask used to apply to the data + :returns: * Phi - basis set + """ # load example image ex_img = nib.load(examplenii) dim = ex_img.shape[0:3] @@ -110,7 +148,25 @@ def write_nii(data, filename, examplenii, mask): def get_args(*args): - # parse arguments + """ + Parse command line arguments + + This will parse the command line arguments for the trend surface model. + The arguments are: + + :param filename: 4-d nifti file containing the images to be estimated + :param maskfile: nifti mask used to apply to the data + :param basis: model order for the interpolating polynomial + :param covfile: file containing covariates + :param ard: use ARD + :param outputall: output all measures + :returns: * filename - 4-d nifti file containing the images to be estimated + * maskfile - nifti mask used to apply to the data + * basis - model order for the interpolating polynomial + * covfile - file containing covariates + * ard - use ARD + * outputall - output all measures + """ parser = argparse.ArgumentParser(description="Trend surface model") parser.add_argument("filename") parser.add_argument("-b", help="basis set", dest="basis", default=3) From 8036dc6c9579edbd1522c14deba19c25a7bf54ec Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Fri, 8 Dec 2023 11:36:53 +0100 Subject: [PATCH 08/34] Format whole project with autopep8 Create a setup.cfg file with the autopep8 settings, and CONTRIBUTING.MD --- CHANGES | 2 +- CONTRIBUTING.md | 71 ++ doc/source/conf.py | 24 +- pcntoolkit/dataio/fileio.py | 63 +- pcntoolkit/model/NP.py | 36 +- pcntoolkit/model/NPR.py | 35 +- pcntoolkit/model/SHASH.py | 9 +- pcntoolkit/model/architecture.py | 236 ++++--- pcntoolkit/model/bayesreg.py | 229 ++++--- pcntoolkit/model/gp.py | 59 +- pcntoolkit/model/hbr.py | 105 +-- pcntoolkit/model/rfa.py | 110 +-- pcntoolkit/normative.py | 834 ++++++++++++----------- pcntoolkit/normative_NP.py | 234 ++++--- pcntoolkit/normative_model/__init__.py | 2 +- pcntoolkit/normative_model/norm_base.py | 4 +- pcntoolkit/normative_model/norm_blr.py | 94 +-- pcntoolkit/normative_model/norm_gpr.py | 22 +- pcntoolkit/normative_model/norm_hbr.py | 109 +-- pcntoolkit/normative_model/norm_np.py | 114 ++-- pcntoolkit/normative_model/norm_rfa.py | 22 +- pcntoolkit/normative_model/norm_utils.py | 9 +- pcntoolkit/normative_parallel.py | 604 ++++++++-------- pcntoolkit/trendsurf.py | 9 +- pcntoolkit/util/__init__.py | 2 +- pcntoolkit/util/hbr_utils.py | 98 +-- pcntoolkit/util/utils.py | 786 +++++++++++---------- setup.cfg | 3 + setup.py | 4 +- tests/profile_trendsurf.py | 10 +- tests/testHBR.py | 56 +- tests/testHBR_transfer.py | 76 ++- tests/test_NP.py | 3 + tests/test_ST_NP.py | 27 +- tests/test_blr.py | 157 +++-- tests/test_gpr.py | 292 ++++---- tests/test_hbr_pymc.py | 95 +-- tests/test_normative.py | 53 +- tests/test_normative_parallel.py | 11 +- tests/test_rand_feat.py | 163 ++--- tests/unit_tests.py | 82 +-- 41 files changed, 2612 insertions(+), 2342 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 setup.cfg diff --git a/CHANGES b/CHANGES index 93e0fa36..061d0ca6 100644 --- a/CHANGES +++ b/CHANGES @@ -75,6 +75,6 @@ version 0.27 - Added configuration files for containerisation with Docker version 0.28 -- Updated to PyMC5 (including migrating back-end to PyTensor +- Updated to PyMC5 (including migrating back-end to PyTensor) - Added support for longitudinal normative modelling with BLR (see Buckova-Rehak et al 2023) - Changed default optimiser for trend surface models (for scalability) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..cf078cde --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,71 @@ +# Contributing to PCNtoolkit + +First off, thanks for taking the time to contribute! 🎉👍 + +The following is a set of guidelines for contributing to PCNtoolkit. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. + +## How Can I Contribute? + +### Reporting Bugs + +This section guides you through submitting a bug report. Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports. + +**Before Submitting A Bug Report** + +- Check the debugging guide. +- Check the FAQs on the forum for a list of common questions and problems. +- Ensure the bug is not already reported by searching on GitHub under [Issues](https://github.com/amarquand/PCNtoolkit/issues). + +**How Do I Submit A Good Bug Report?** + +Bugs are tracked as [GitHub issues](https://github.com/amarquand/PCNtoolkit/issues). Create an issue and provide the following information: + +- **Use a clear and descriptive title** for the issue to identify the problem. +- **Describe the exact steps which reproduce the problem** in as much detail as possible. +- **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. + +### Suggesting Enhancements + +This section guides you through submitting an enhancement suggestion, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion and find related suggestions. + +**How Do I Submit A Good Enhancement Suggestion?** + +Enhancement suggestions are tracked as [GitHub issues](https://github.com/amarquand/PCNtoolkit/issues). Create an issue and provide the following information: + +- **Use a clear and descriptive title** for the issue to identify the suggestion. +- **Provide a step-by-step description of the suggested enhancement** in as much detail as possible. +- **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as Markdown code blocks. + + + +## Styleguides + +### Git Commit Messages + +- Use the present tense ("Add feature" not "Added feature") +- Use the imperative mood ("Move cursor to..." not "Moves cursor to...") +- Limit the first line to 72 characters or less +- Reference issues and pull requests liberally after the first line + +### Python Styleguide + +All Python code must adhere to [PEP 8](https://www.python.org/dev/peps/pep-0008/), and we use `autopep8` for automatic code formatting. Please see the [autopep8 documentation](https://github.com/hhatto/autopep8) for more details. The autopep8 settings can be found in setup.cfg. + + +## Additional Notes + +Feel free to propose changes to this document in a pull request. diff --git a/doc/source/conf.py b/doc/source/conf.py index 793a70d7..384b2223 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -26,7 +26,6 @@ sys.path.insert(0, os.path.abspath('../../pcntoolkit/utils')) - # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. @@ -36,19 +35,19 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx_tabs.tabs', +extensions = ['sphinx_tabs.tabs', 'sphinx.ext.autodoc', 'sphinx.ext.imgmath', 'sphinx.ext.githubpages', - 'sphinx.ext.autosectionlabel', + 'sphinx.ext.autosectionlabel', 'sphinx.ext.autosummary', 'sphinx_automodapi.automodapi', - #'sphinx.ext.doctest', - #'sphinx.ext.intersphinx', - #'sphinx.ext.mathjax', + # 'sphinx.ext.doctest', + # 'sphinx.ext.intersphinx', + # 'sphinx.ext.mathjax', 'sphinx.ext.napoleon', 'sphinx.ext.viewcode', - #'sphinxarg.ext', + # 'sphinxarg.ext', ] autosummary_generate = True @@ -61,7 +60,7 @@ # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [] -highlight_language ='none' +highlight_language = 'none' # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: @@ -105,7 +104,7 @@ # a list of builtin themes. # html_theme = 'sphinx_rtd_theme' -html_theme_options = { 'style_nav_header_background': '#1E90FF'} +html_theme_options = {'style_nav_header_background': '#1E90FF'} pygments_style = 'sphinx' @@ -120,12 +119,15 @@ # These paths are either relative to html_static_path # or fully qualified paths (eg. https://...) -html_css_files = ['pages/css/pcntoolkit.css', +html_css_files = ['pages/css/pcntoolkit.css', 'pages/css/pcntoolkit_nomaxwidth.css'] # add custom files that are stored in _static + + def setup(app): - app.add_css_file('pages/css/pcntoolkit_tabs.css') + app.add_css_file('pages/css/pcntoolkit_tabs.css') + # add logo html_logo = "pcn-logo.png" diff --git a/pcntoolkit/dataio/fileio.py b/pcntoolkit/dataio/fileio.py index b32e6302..ca962aad 100644 --- a/pcntoolkit/dataio/fileio.py +++ b/pcntoolkit/dataio/fileio.py @@ -14,7 +14,7 @@ pass path = os.path.abspath(os.path.dirname(__file__)) - path = os.path.dirname(path) # parent directory + path = os.path.dirname(path) # parent directory if path not in sys.path: sys.path.append(path) del path @@ -32,6 +32,7 @@ # general utility routines # ------------------------ + def predictive_interval(s2_forward, cov_forward, multiplicator): @@ -39,19 +40,20 @@ def predictive_interval(s2_forward, Calculates a predictive interval for the forward model """ # calculates a predictive interval - - PI=np.zeros(len(cov_forward)) - for i,xdot in enumerate(cov_forward): - s=np.sqrt(s2_forward[i]) - PI[i]=multiplicator*s + + PI = np.zeros(len(cov_forward)) + for i, xdot in enumerate(cov_forward): + s = np.sqrt(s2_forward[i]) + PI[i] = multiplicator*s return PI + def create_mask(data_array, mask, verbose=False): """ Create a mask from a data array or a nifti file Basic usage:: - + create_mask(data_array, mask, verbose) :param data_array: numpy array containing the data to write out @@ -87,7 +89,7 @@ def vol2vec(dat, mask, verbose=False): Vectorise a 3d image Basic usage:: - + vol2vec(dat, mask, verbose) :param dat: numpy array containing the data to write out @@ -100,8 +102,8 @@ def vol2vec(dat, mask, verbose=False): dim = dat.shape[0:3] + (1,) else: dim = dat.shape[0:3] + (dat.shape[3],) - - #mask = create_mask(dat, mask=mask, verbose=verbose) + + # mask = create_mask(dat, mask=mask, verbose=verbose) if mask is None: mask = create_mask(dat, mask=mask, verbose=verbose) @@ -122,7 +124,7 @@ def file_type(filename): Determine the file type of a file Basic usage:: - + file_type(filename) :param filename: name of the file to check @@ -148,7 +150,7 @@ def file_extension(filename): Determine the file extension of a file (e.g. .nii.gz) Basic usage:: - + file_extension(filename) :param filename: name of the file to check @@ -180,7 +182,7 @@ def file_stem(filename): Determine the file stem of a file (e.g. /path/to/file.nii.gz -> file) Basic usage:: - + file_stem(filename) :param filename: name of the file to check @@ -200,7 +202,7 @@ def load_nifti(datafile, mask=None, vol=False, verbose=False): Load a nifti file into a numpy array Basic usage:: - + load_nifti(datafile, mask, vol, verbose) :param datafile: name of the file to load @@ -215,7 +217,7 @@ def load_nifti(datafile, mask=None, vol=False, verbose=False): dat = img.get_data() if mask is not None: - mask=load_nifti(mask, vol=True) + mask = load_nifti(mask, vol=True) if not vol: dat = vol2vec(dat, mask) @@ -224,10 +226,9 @@ def load_nifti(datafile, mask=None, vol=False, verbose=False): def save_nifti(data, filename, examplenii, mask, dtype=None): - ''' Write output to nifti - + Basic usage:: save_nifti(data, filename mask, dtype) @@ -238,7 +239,6 @@ def save_nifti(data, filename, examplenii, mask, dtype=None): :mask: nifti image containing a mask for the image :param dtype: data type for the output image (if different from the image) ''' - # load mask if isinstance(mask, str): @@ -264,7 +264,7 @@ def save_nifti(data, filename, examplenii, mask, dtype=None): hdr.set_data_dtype(dtype) array_data = array_data.astype(dtype) array_img = nib.Nifti1Image(array_data, ex_img.affine, hdr) - + nib.save(array_img, filename) # -------------- @@ -277,7 +277,7 @@ def load_cifti(filename, vol=False, mask=None, rmtmp=True): Load a cifti file into a numpy array Basic usage:: - + load_cifti(filename, vol, mask, rmtmp) :param filename: name of the file to load @@ -339,7 +339,7 @@ def save_cifti(data, filename, example, mask=None, vol=True, volatlas=None): Save a cifti file from a numpy array Basic usage:: - + save_cifti(data, filename, example, mask, vol, volatlas) :param data: numpy array containing the data to write out @@ -357,7 +357,7 @@ def save_cifti(data, filename, example, mask=None, vol=True, volatlas=None): data = data.astype('float32') # force 32 bit output dtype = 'NIFTI_TYPE_FLOAT32' else: - raise(ValueError, 'Only float data types currently handled') + raise (ValueError, 'Only float data types currently handled') if len(data.shape) == 1: Nimg = 1 @@ -385,7 +385,7 @@ def save_cifti(data, filename, example, mask=None, vol=True, volatlas=None): for i in range(0, Nimg): garraysl.append( nib.gifti.gifti.GiftiDataArray(data=data[0:Nvertl, i], - datatype=dtype)) + datatype=dtype)) giil = nib.gifti.gifti.GiftiImage(darrays=garraysl) fnamel = fstem + '-left.func.gii' nib.save(giil, fnamel) @@ -397,7 +397,7 @@ def save_cifti(data, filename, example, mask=None, vol=True, volatlas=None): for i in range(0, Nimg): garraysr.append( nib.gifti.gifti.GiftiDataArray(data=data[Nvertl:Nvertl+Nvertr, i], - datatype=dtype)) + datatype=dtype)) giir = nib.gifti.gifti.GiftiImage(darrays=garraysr) fnamer = fstem + '-right.func.gii' nib.save(giir, fnamer) @@ -440,7 +440,7 @@ def load_pd(filename): Load a csv file into a pandas dataframe Basic usage:: - + load_pd(filename) :param filename: name of the file to load @@ -477,7 +477,7 @@ def load_ascii(filename): Load an ascii file into a numpy array Basic usage:: - + load_ascii(filename) :param filename: name of the file to load @@ -512,7 +512,7 @@ def save(data, filename, example=None, mask=None, text=False, dtype=None): Save a numpy array to a file Basic usage:: - + save(data, filename, example, mask, text, dtype) :param data: numpy array containing the data to write out @@ -523,7 +523,6 @@ def save(data, filename, example=None, mask=None, text=False, dtype=None): :param dtype: data type for the output image (if different from the image) """ - if file_type(filename) == 'cifti': save_cifti(data.T, filename, example, vol=True) elif file_type(filename) == 'nifti': @@ -540,7 +539,7 @@ def load(filename, mask=None, text=False, vol=True): Load a numpy array from a file Basic usage:: - + load(filename, mask, text, vol) :param filename: name of the file to load @@ -570,7 +569,7 @@ def tryint(s): Try to convert a string to an integer Basic usage:: - + tryint(s) :param s: string to convert @@ -585,9 +584,9 @@ def tryint(s): def alphanum_key(s): """ Turn a string into a list of numbers - + Basic usage:: - + alphanum_key(s) :param s: string to convert diff --git a/pcntoolkit/model/NP.py b/pcntoolkit/model/NP.py index 13370286..714942bf 100644 --- a/pcntoolkit/model/NP.py +++ b/pcntoolkit/model/NP.py @@ -12,6 +12,7 @@ ##################################### NP Model ################################ + class NP(nn.Module): def __init__(self, encoder, decoder, args): super(NP, self).__init__() @@ -20,19 +21,19 @@ def __init__(self, encoder, decoder, args): self.dp_level = encoder.dp_level self.encoder = encoder self.decoder = decoder - self.r_to_z_mean_dp = nn.Dropout(p = self.dp_level) + self.r_to_z_mean_dp = nn.Dropout(p=self.dp_level) self.r_to_z_mean = nn.Linear(self.r_dim, self.z_dim) - self.r_to_z_logvar_dp = nn.Dropout(p = self.dp_level) + self.r_to_z_logvar_dp = nn.Dropout(p=self.dp_level) self.r_to_z_logvar = nn.Linear(self.r_dim, self.z_dim) self.device = args.device self.type = args.type - + def xy_to_z_params(self, x, y): r = self.encoder.forward(x, y) mu = self.r_to_z_mean(self.r_to_z_mean_dp(r)) logvar = self.r_to_z_logvar(self.r_to_z_logvar_dp(r)) return mu, logvar - + def reparameterise(self, z): mu, logvar = z std = torch.exp(0.5 * logvar) @@ -40,43 +41,48 @@ def reparameterise(self, z): z_sample = eps.mul(std).add_(mu) return z_sample - def forward(self, x_context, y_context, x_all=None, y_all=None, n = 10): + def forward(self, x_context, y_context, x_all=None, y_all=None, n=10): y_sigma = None z_context = self.xy_to_z_params(x_context, y_context) if self.training: z_all = self.xy_to_z_params(x_all, y_all) z_sample = self.reparameterise(z_all) y_hat = self.decoder.forward(z_sample, x_all) - else: + else: z_all = z_context if self.type == 'ST': - temp = torch.zeros([n,y_context.shape[0], y_context.shape[2]], device = 'cpu') + temp = torch.zeros( + [n, y_context.shape[0], y_context.shape[2]], device='cpu') elif self.type == 'MT': - temp = torch.zeros([n,y_context.shape[0],1,y_context.shape[2],y_context.shape[3], - y_context.shape[4]], device = 'cpu') + temp = torch.zeros([n, y_context.shape[0], 1, y_context.shape[2], y_context.shape[3], + y_context.shape[4]], device='cpu') for i in range(n): z_sample = self.reparameterise(z_all) - temp[i,:] = self.decoder.forward(z_sample, x_context) + temp[i, :] = self.decoder.forward(z_sample, x_context) y_hat = torch.mean(temp, dim=0).to(self.device) if n > 1: y_sigma = torch.std(temp, dim=0).to(self.device) return y_hat, z_all, z_context, y_sigma - + ############################################################################### - + + def apply_dropout_test(m): if type(m) == nn.Dropout: m.train() + def kl_div_gaussians(mu_q, logvar_q, mu_p, logvar_p): var_p = torch.exp(logvar_p) kl_div = (torch.exp(logvar_q) + (mu_q - mu_p) ** 2) / (var_p) \ - - 1.0 \ - + logvar_p - logvar_q + - 1.0 \ + + logvar_p - logvar_q kl_div = 0.5 * kl_div.sum() return kl_div + def np_loss(y_hat, y, z_all, z_context): - BCE = F.binary_cross_entropy(torch.squeeze(y_hat), torch.mean(y,dim=1), reduction="sum") + BCE = F.binary_cross_entropy(torch.squeeze( + y_hat), torch.mean(y, dim=1), reduction="sum") KLD = kl_div_gaussians(z_all[0], z_all[1], z_context[0], z_context[1]) return BCE + KLD diff --git a/pcntoolkit/model/NPR.py b/pcntoolkit/model/NPR.py index 6e8c1f53..f475c619 100755 --- a/pcntoolkit/model/NPR.py +++ b/pcntoolkit/model/NPR.py @@ -12,6 +12,7 @@ ##################################### NP Model ################################ + class NPR(nn.Module): def __init__(self, encoder, decoder, args): super(NPR, self).__init__() @@ -22,13 +23,13 @@ def __init__(self, encoder, decoder, args): self.r_to_z_mean = nn.Linear(self.r_dim, self.z_dim) self.r_to_z_logvar = nn.Linear(self.r_dim, self.z_dim) self.device = args.device - + def xy_to_z_params(self, x, y): r = self.encoder.forward(x, y) mu = self.r_to_z_mean(r) logvar = self.r_to_z_logvar(r) return mu, logvar - + def reparameterise(self, z): mu, logvar = z std = torch.exp(0.5 * logvar) @@ -36,7 +37,7 @@ def reparameterise(self, z): z_sample = eps.mul(std).add_(mu) return z_sample - def forward(self, x_context, y_context, x_all=None, y_all=None, n = 10): + def forward(self, x_context, y_context, x_all=None, y_all=None, n=10): y_sigma = None y_sigma_84 = None z_context = self.xy_to_z_params(x_context, y_context) @@ -44,36 +45,42 @@ def forward(self, x_context, y_context, x_all=None, y_all=None, n = 10): z_all = self.xy_to_z_params(x_all, y_all) z_sample = self.reparameterise(z_all) y_hat, y_hat_84 = self.decoder.forward(z_sample) - else: + else: z_all = z_context - temp = torch.zeros([n,y_context.shape[0], y_context.shape[2]], device = self.device) - temp_84 = torch.zeros([n,y_context.shape[0], y_context.shape[2]], device = self.device) + temp = torch.zeros( + [n, y_context.shape[0], y_context.shape[2]], device=self.device) + temp_84 = torch.zeros( + [n, y_context.shape[0], y_context.shape[2]], device=self.device) for i in range(n): z_sample = self.reparameterise(z_all) - temp[i,:], temp_84[i,:] = self.decoder.forward(z_sample) + temp[i, :], temp_84[i, :] = self.decoder.forward(z_sample) y_hat = torch.mean(temp, dim=0).to(self.device) y_hat_84 = torch.mean(temp_84, dim=0).to(self.device) if n > 1: y_sigma = torch.std(temp, dim=0).to(self.device) y_sigma_84 = torch.std(temp_84, dim=0).to(self.device) return y_hat, y_hat_84, z_all, z_context, y_sigma, y_sigma_84 - + ############################################################################### + def kl_div_gaussians(mu_q, logvar_q, mu_p, logvar_p): var_p = torch.exp(logvar_p) kl_div = (torch.exp(logvar_q) + (mu_q - mu_p) ** 2) / (var_p) \ - - 1.0 \ - + logvar_p - logvar_q + - 1.0 \ + + logvar_p - logvar_q kl_div = 0.5 * kl_div.sum() return kl_div + def np_loss(y_hat, y_hat_84, y, z_all, z_context): - #PBL = pinball_loss(y, y_hat, 0.05) - BCE = F.binary_cross_entropy(torch.squeeze(y_hat), torch.mean(y,dim=1), reduction="sum") + # PBL = pinball_loss(y, y_hat, 0.05) + BCE = F.binary_cross_entropy(torch.squeeze( + y_hat), torch.mean(y, dim=1), reduction="sum") idx1 = (y >= y_hat_84).squeeze() idx2 = (y < y_hat_84).squeeze() - BCE84 = 0.84 * F.binary_cross_entropy(torch.squeeze(y_hat_84[idx1,:]), torch.mean(y[idx1,:],dim=1), reduction="sum") + \ - 0.16 * F.binary_cross_entropy(torch.squeeze(y_hat_84[idx2,:]), torch.mean(y[idx2,:],dim=1), reduction="sum") + BCE84 = 0.84 * F.binary_cross_entropy(torch.squeeze(y_hat_84[idx1, :]), torch.mean(y[idx1, :], dim=1), reduction="sum") + \ + 0.16 * F.binary_cross_entropy(torch.squeeze( + y_hat_84[idx2, :]), torch.mean(y[idx2, :], dim=1), reduction="sum") KLD = kl_div_gaussians(z_all[0], z_all[1], z_context[0], z_context[1]) return BCE + KLD + BCE84 diff --git a/pcntoolkit/model/SHASH.py b/pcntoolkit/model/SHASH.py index 3936255d..39bf0646 100644 --- a/pcntoolkit/model/SHASH.py +++ b/pcntoolkit/model/SHASH.py @@ -64,7 +64,8 @@ def perform(self, node, inputs_storage, output_storage): inputs_storage[0], return_inverse=True ) unique_outputs = spp.kv(unique_inputs, inputs_storage[1]) - outputs = unique_outputs[inverse_indices].reshape(inputs_storage[0].shape) + outputs = unique_outputs[inverse_indices].reshape( + inputs_storage[0].shape) output_storage[0][0] = outputs def grad(self, inputs, output_grads): @@ -184,7 +185,8 @@ def logp(value, epsilon, delta): this_C_sqr = 1 + this_S_sqr frac1 = -ptt.log(ptt.constant(2 * np.pi)) / 2 frac2 = ( - ptt.log(delta) + ptt.log(this_C_sqr) / 2 - ptt.log(1 + ptt.sqr(value)) / 2 + ptt.log(delta) + ptt.log(this_C_sqr) / + 2 - ptt.log(1 + ptt.sqr(value)) / 2 ) exp = -this_S_sqr / 2 return frac1 + frac2 + exp @@ -301,7 +303,8 @@ def rng_fn( ) -> np.ndarray: s = rng.normal(size=size) mean = np.sinh(epsilon / delta) * numpy_P(1 / delta) - var = ((np.cosh(2 * epsilon / delta) * numpy_P(2 / delta) - 1) / 2) - mean**2 + var = ((np.cosh(2 * epsilon / delta) * + numpy_P(2 / delta) - 1) / 2) - mean**2 out = ( (np.sinh((np.arcsinh(s) + epsilon) / delta) - mean) / np.sqrt(var) ) * sigma + mu diff --git a/pcntoolkit/model/architecture.py b/pcntoolkit/model/architecture.py index 569d4336..0dfb09c9 100644 --- a/pcntoolkit/model/architecture.py +++ b/pcntoolkit/model/architecture.py @@ -11,19 +11,27 @@ from torch.nn import functional as F import numpy as np + def compute_conv_out_size(d_in, h_in, w_in, padding, dilation, kernel_size, stride, UPorDW): if UPorDW == 'down': - d_out = np.floor((d_in + 2 * padding[0] - dilation * (kernel_size - 1) - 1) / stride + 1) - h_out = np.floor((h_in + 2 * padding[1] - dilation * (kernel_size - 1) - 1) / stride + 1) - w_out = np.floor((w_in + 2 * padding[2] - dilation * (kernel_size - 1) - 1) / stride + 1) + d_out = np.floor( + (d_in + 2 * padding[0] - dilation * (kernel_size - 1) - 1) / stride + 1) + h_out = np.floor( + (h_in + 2 * padding[1] - dilation * (kernel_size - 1) - 1) / stride + 1) + w_out = np.floor( + (w_in + 2 * padding[2] - dilation * (kernel_size - 1) - 1) / stride + 1) elif UPorDW == 'up': - d_out = (d_in-1) * stride - 2 * padding[0] + dilation * (kernel_size - 1) + 1 - h_out = (h_in-1) * stride - 2 * padding[1] + dilation * (kernel_size - 1) + 1 - w_out = (w_in-1) * stride - 2 * padding[2] + dilation * (kernel_size - 1) + 1 + d_out = (d_in-1) * stride - 2 * \ + padding[0] + dilation * (kernel_size - 1) + 1 + h_out = (h_in-1) * stride - 2 * \ + padding[1] + dilation * (kernel_size - 1) + 1 + w_out = (w_in-1) * stride - 2 * \ + padding[2] + dilation * (kernel_size - 1) + 1 return d_out, h_out, w_out ################################ ARCHITECTURES ################################ + class Encoder(nn.Module): def __init__(self, x, y, args): super(Encoder, self).__init__() @@ -31,82 +39,88 @@ def __init__(self, x, y, args): self.r_conv_dim = 100 self.lrlu_neg_slope = 0.01 self.dp_level = 0.1 - - self.factor=args.m + + self.factor = args.m self.x_dim = x.shape[2] - + # Conv 1 - self.encoder_y_layer_1_conv = nn.Conv3d(in_channels = self.factor, out_channels=self.factor, - kernel_size=5, stride=2, padding=0, - dilation=1, groups=self.factor, bias=True) # in:(90,108,90) out:(43,52,43) + self.encoder_y_layer_1_conv = nn.Conv3d(in_channels=self.factor, out_channels=self.factor, + kernel_size=5, stride=2, padding=0, + dilation=1, groups=self.factor, bias=True) # in:(90,108,90) out:(43,52,43) self.encoder_y_layer_1_bn = nn.BatchNorm3d(self.factor) - d_out_1, h_out_1, w_out_1 = compute_conv_out_size(y.shape[2], y.shape[3], - y.shape[4], padding=[0,0,0], - dilation=1, kernel_size=5, + d_out_1, h_out_1, w_out_1 = compute_conv_out_size(y.shape[2], y.shape[3], + y.shape[4], padding=[ + 0, 0, 0], + dilation=1, kernel_size=5, stride=2, UPorDW='down') - + # Conv 2 - self.encoder_y_layer_2_conv = nn.Conv3d(in_channels=self.factor, out_channels=self.factor, - kernel_size=3, stride=2, padding=0, - dilation=1, groups=self.factor, bias=True) # out: (21,25,21) + self.encoder_y_layer_2_conv = nn.Conv3d(in_channels=self.factor, out_channels=self.factor, + kernel_size=3, stride=2, padding=0, + dilation=1, groups=self.factor, bias=True) # out: (21,25,21) self.encoder_y_layer_2_bn = nn.BatchNorm3d(self.factor) - d_out_2, h_out_2, w_out_2 = compute_conv_out_size(d_out_1, h_out_1, - w_out_1, padding=[0,0,0], - dilation=1, kernel_size=3, + d_out_2, h_out_2, w_out_2 = compute_conv_out_size(d_out_1, h_out_1, + w_out_1, padding=[ + 0, 0, 0], + dilation=1, kernel_size=3, stride=2, UPorDW='down') - + # Conv 3 - self.encoder_y_layer_3_conv = nn.Conv3d(in_channels=self.factor, out_channels=self.factor, - kernel_size=3, stride=2, padding=0, - dilation=1, groups=self.factor, bias=True) # out: (10,12,10) + self.encoder_y_layer_3_conv = nn.Conv3d(in_channels=self.factor, out_channels=self.factor, + kernel_size=3, stride=2, padding=0, + dilation=1, groups=self.factor, bias=True) # out: (10,12,10) self.encoder_y_layer_3_bn = nn.BatchNorm3d(self.factor) - d_out_3, h_out_3, w_out_3 = compute_conv_out_size(d_out_2, h_out_2, - w_out_2, padding=[0,0,0], - dilation=1, kernel_size=3, + d_out_3, h_out_3, w_out_3 = compute_conv_out_size(d_out_2, h_out_2, + w_out_2, padding=[ + 0, 0, 0], + dilation=1, kernel_size=3, stride=2, UPorDW='down') - + # Conv 4 - self.encoder_y_layer_4_conv = nn.Conv3d(in_channels=self.factor, out_channels=1, - kernel_size=3, stride=2, padding=0, - dilation=1, groups=1, bias=True) # out: (4,5,4) + self.encoder_y_layer_4_conv = nn.Conv3d(in_channels=self.factor, out_channels=1, + kernel_size=3, stride=2, padding=0, + dilation=1, groups=1, bias=True) # out: (4,5,4) self.encoder_y_layer_4_bn = nn.BatchNorm3d(1) - d_out_4, h_out_4, w_out_4 = compute_conv_out_size(d_out_3, h_out_3, - w_out_3, padding=[0,0,0], - dilation=1, kernel_size=3, + d_out_4, h_out_4, w_out_4 = compute_conv_out_size(d_out_3, h_out_3, + w_out_3, padding=[ + 0, 0, 0], + dilation=1, kernel_size=3, stride=2, UPorDW='down') self.cnn_feature_num = [1, int(d_out_4), int(h_out_4), int(w_out_4)] - + # FC 5 - self.encoder_y_layer_5_dp = nn.Dropout(p = self.dp_level) - self.encoder_y_layer_5_linear = nn.Linear(int(np.prod(self.cnn_feature_num)), self.r_conv_dim) - + self.encoder_y_layer_5_dp = nn.Dropout(p=self.dp_level) + self.encoder_y_layer_5_linear = nn.Linear( + int(np.prod(self.cnn_feature_num)), self.r_conv_dim) + # FC 6 - self.encoder_xy_layer_6_dp = nn.Dropout(p = self.dp_level) - self.encoder_xy_layer_6_linear = nn.Linear(self.r_conv_dim + self.x_dim, 50) - - # FC 7 - self.encoder_xy_layer_7_dp = nn.Dropout(p = self.dp_level) + self.encoder_xy_layer_6_dp = nn.Dropout(p=self.dp_level) + self.encoder_xy_layer_6_linear = nn.Linear( + self.r_conv_dim + self.x_dim, 50) + + # FC 7 + self.encoder_xy_layer_7_dp = nn.Dropout(p=self.dp_level) self.encoder_xy_layer_7_linear = nn.Linear(50, self.r_dim) def forward(self, x, y): y = F.leaky_relu(self.encoder_y_layer_1_bn( - self.encoder_y_layer_1_conv(y)), self.lrlu_neg_slope) + self.encoder_y_layer_1_conv(y)), self.lrlu_neg_slope) y = F.leaky_relu(self.encoder_y_layer_2_bn( - self.encoder_y_layer_2_conv(y)),self.lrlu_neg_slope) + self.encoder_y_layer_2_conv(y)), self.lrlu_neg_slope) y = F.leaky_relu(self.encoder_y_layer_3_bn( - self.encoder_y_layer_3_conv(y)),self.lrlu_neg_slope) + self.encoder_y_layer_3_conv(y)), self.lrlu_neg_slope) y = F.leaky_relu(self.encoder_y_layer_4_bn( - self.encoder_y_layer_4_conv(y)),self.lrlu_neg_slope) + self.encoder_y_layer_4_conv(y)), self.lrlu_neg_slope) y = F.leaky_relu(self.encoder_y_layer_5_linear(self.encoder_y_layer_5_dp( - y.view(y.shape[0], np.prod(self.cnn_feature_num)))), self.lrlu_neg_slope) + y.view(y.shape[0], np.prod(self.cnn_feature_num)))), self.lrlu_neg_slope) x_y = torch.cat((y, torch.mean(x, dim=1)), 1) x_y = F.leaky_relu(self.encoder_xy_layer_6_linear( - self.encoder_xy_layer_6_dp(x_y)),self.lrlu_neg_slope) + self.encoder_xy_layer_6_dp(x_y)), self.lrlu_neg_slope) x_y = F.leaky_relu(self.encoder_xy_layer_7_linear( - self.encoder_xy_layer_7_dp(x_y)),self.lrlu_neg_slope) + self.encoder_xy_layer_7_dp(x_y)), self.lrlu_neg_slope) return x_y - - + + class Decoder(nn.Module): def __init__(self, x, y, args): super(Decoder, self).__init__() @@ -117,85 +131,85 @@ def __init__(self, x, y, args): self.z_dim = 10 self.x_dim = x.shape[2] self.cnn_feature_num = args.cnn_feature_num - self.factor=args.m - + self.factor = args.m + # FC 1 - self.decoder_zx_layer_1_dp = nn.Dropout(p = self.dp_level) + self.decoder_zx_layer_1_dp = nn.Dropout(p=self.dp_level) self.decoder_zx_layer_1_linear = nn.Linear(self.z_dim + self.x_dim, 50) - + # FC 2 - self.decoder_zx_layer_2_dp = nn.Dropout(p = self.dp_level) - self.decoder_zx_layer_2_linear = nn.Linear(50, int(np.prod(self.cnn_feature_num))) - + self.decoder_zx_layer_2_dp = nn.Dropout(p=self.dp_level) + self.decoder_zx_layer_2_linear = nn.Linear( + 50, int(np.prod(self.cnn_feature_num))) + # Iconv 1 - self.decoder_zx_layer_1_iconv = nn.ConvTranspose3d(in_channels=1, out_channels=self.factor, - kernel_size=3, stride=1, - padding=0, output_padding=(0,0,0), - groups=1, bias=True, dilation=1) + self.decoder_zx_layer_1_iconv = nn.ConvTranspose3d(in_channels=1, out_channels=self.factor, + kernel_size=3, stride=1, + padding=0, output_padding=(0, 0, 0), + groups=1, bias=True, dilation=1) self.decoder_zx_layer_1_bn = nn.BatchNorm3d(self.factor) - d_out_4, h_out_4, w_out_4 = compute_conv_out_size(args.cnn_feature_num[1]*2, - args.cnn_feature_num[2]*2, - args.cnn_feature_num[3]*2, - padding=[0,0,0], - dilation=1, kernel_size=3, + d_out_4, h_out_4, w_out_4 = compute_conv_out_size(args.cnn_feature_num[1]*2, + args.cnn_feature_num[2]*2, + args.cnn_feature_num[3]*2, + padding=[0, 0, 0], + dilation=1, kernel_size=3, stride=1, UPorDW='up') - + # Iconv 2 - self.decoder_zx_layer_2_iconv = nn.ConvTranspose3d(in_channels=self.factor, out_channels=self.factor, - kernel_size=3, stride=1, padding=0, - output_padding=(0,0,0), groups=self.factor, - bias=True, dilation=1) + self.decoder_zx_layer_2_iconv = nn.ConvTranspose3d(in_channels=self.factor, out_channels=self.factor, + kernel_size=3, stride=1, padding=0, + output_padding=(0, 0, 0), groups=self.factor, + bias=True, dilation=1) self.decoder_zx_layer_2_bn = nn.BatchNorm3d(self.factor) - d_out_3, h_out_3, w_out_3 = compute_conv_out_size(d_out_4*2, - h_out_4*2, - w_out_4*2, - padding=[0,0,0], - dilation=1, kernel_size=3, + d_out_3, h_out_3, w_out_3 = compute_conv_out_size(d_out_4*2, + h_out_4*2, + w_out_4*2, + padding=[0, 0, 0], + dilation=1, kernel_size=3, stride=1, UPorDW='up') # Iconv 3 - self.decoder_zx_layer_3_iconv = nn.ConvTranspose3d(in_channels=self.factor, out_channels=self.factor, - kernel_size=3, stride=1, padding=0, - output_padding=(0,0,0), groups=self.factor, - bias=True, dilation=1) + self.decoder_zx_layer_3_iconv = nn.ConvTranspose3d(in_channels=self.factor, out_channels=self.factor, + kernel_size=3, stride=1, padding=0, + output_padding=(0, 0, 0), groups=self.factor, + bias=True, dilation=1) self.decoder_zx_layer_3_bn = nn.BatchNorm3d(self.factor) - d_out_2, h_out_2, w_out_2 = compute_conv_out_size(d_out_3*2, - h_out_3*2, - w_out_3*2, - padding=[0,0,0], - dilation=1, kernel_size=3, + d_out_2, h_out_2, w_out_2 = compute_conv_out_size(d_out_3*2, + h_out_3*2, + w_out_3*2, + padding=[0, 0, 0], + dilation=1, kernel_size=3, stride=1, UPorDW='up') - - # Iconv 4 - self.decoder_zx_layer_4_iconv = nn.ConvTranspose3d(in_channels=self.factor, out_channels=1, - kernel_size=3, stride=1, padding=(0,0,0), - output_padding= (0,0,0), groups=1, - bias=True, dilation=1) - d_out_1, h_out_1, w_out_1 = compute_conv_out_size(d_out_2*2, - h_out_2*2, - w_out_2*2, - padding=[0,0,0], - dilation=1, kernel_size=3, + + # Iconv 4 + self.decoder_zx_layer_4_iconv = nn.ConvTranspose3d(in_channels=self.factor, out_channels=1, + kernel_size=3, stride=1, padding=(0, 0, 0), + output_padding=(0, 0, 0), groups=1, + bias=True, dilation=1) + d_out_1, h_out_1, w_out_1 = compute_conv_out_size(d_out_2*2, + h_out_2*2, + w_out_2*2, + padding=[0, 0, 0], + dilation=1, kernel_size=3, stride=1, UPorDW='up') - - self.scaling = [y.shape[2]/d_out_1, y.shape[3]/h_out_1, + + self.scaling = [y.shape[2]/d_out_1, y.shape[3]/h_out_1, y.shape[4]/w_out_1] - + def forward(self, z_sample, x_target): - z_x = torch.cat([z_sample, torch.mean(x_target,dim=1)], dim=1) - z_x = F.leaky_relu(self.decoder_zx_layer_1_linear(self.decoder_zx_layer_1_dp(z_x)), + z_x = torch.cat([z_sample, torch.mean(x_target, dim=1)], dim=1) + z_x = F.leaky_relu(self.decoder_zx_layer_1_linear(self.decoder_zx_layer_1_dp(z_x)), self.lrlu_neg_slope) - z_x = F.leaky_relu(self.decoder_zx_layer_2_linear(self.decoder_zx_layer_2_dp(z_x)), + z_x = F.leaky_relu(self.decoder_zx_layer_2_linear(self.decoder_zx_layer_2_dp(z_x)), self.lrlu_neg_slope) z_x = z_x.view(x_target.shape[0], self.cnn_feature_num[0], self.cnn_feature_num[1], self.cnn_feature_num[2], self.cnn_feature_num[3]) z_x = F.leaky_relu(self.decoder_zx_layer_1_bn(self.decoder_zx_layer_1_iconv( - F.interpolate(z_x, scale_factor=2))), self.lrlu_neg_slope) + F.interpolate(z_x, scale_factor=2))), self.lrlu_neg_slope) z_x = F.leaky_relu(self.decoder_zx_layer_2_bn(self.decoder_zx_layer_2_iconv( - F.interpolate(z_x, scale_factor=2))), self.lrlu_neg_slope) + F.interpolate(z_x, scale_factor=2))), self.lrlu_neg_slope) z_x = F.leaky_relu(self.decoder_zx_layer_3_bn(self.decoder_zx_layer_3_iconv( - F.interpolate(z_x, scale_factor=2))), self.lrlu_neg_slope) + F.interpolate(z_x, scale_factor=2))), self.lrlu_neg_slope) z_x = self.decoder_zx_layer_4_iconv(F.interpolate(z_x, scale_factor=2)) y_hat = torch.sigmoid(F.interpolate(z_x, scale_factor=(self.scaling[0], - self.scaling[1],self.scaling[2]))) + self.scaling[1], self.scaling[2]))) return y_hat - \ No newline at end of file diff --git a/pcntoolkit/model/bayesreg.py b/pcntoolkit/model/bayesreg.py index 91a62ebf..0c0403a8 100755 --- a/pcntoolkit/model/bayesreg.py +++ b/pcntoolkit/model/bayesreg.py @@ -2,7 +2,7 @@ from __future__ import division import numpy as np -from scipy import optimize , linalg +from scipy import optimize, linalg from scipy.linalg import LinAlgError @@ -55,10 +55,11 @@ def __init__(self, **kwargs): var_covariates = kwargs.get('var_covariates', None) warp = kwargs.get('warp', None) warp_reparam = kwargs.get('warp_reparam', False) - + if var_groups is not None and var_covariates is not None: - raise ValueError("var_covariates and var_groups cannot both be used") - + raise ValueError( + "var_covariates and var_groups cannot both be used") + # basic parameters self.hyp = np.nan self.nlZ = np.nan @@ -84,9 +85,9 @@ def __init__(self, **kwargs): self.warp = warp self.n_warp_param = warp.get_n_params() self.warp_reparam = warp_reparam - + self.gamma = None - + def _parse_hyps(self, hyp, X, Xv=None): """ Parse hyperparameters into noise precision, lengthscale precision and @@ -98,24 +99,24 @@ def _parse_hyps(self, hyp, X, Xv=None): """ N = X.shape[0] - + # noise precision if Xv is not None: if len(Xv.shape) == 1: Dv = 1 Xv = Xv[:, np.newaxis] else: - Dv = Xv.shape[1] - w_d = np.asarray(hyp[0:Dv]) + Dv = Xv.shape[1] + w_d = np.asarray(hyp[0:Dv]) beta = np.exp(Xv.dot(w_d)) n_lik_param = len(w_d) elif self.var_groups is not None: beta = np.exp(hyp[0:len(self.var_ids)]) n_lik_param = len(beta) else: - beta = np.asarray([np.exp(hyp[0])]) + beta = np.asarray([np.exp(hyp[0])]) n_lik_param = len(beta) - + # parameters for warping the likelihood function if self.warp is not None: gamma = hyp[n_lik_param:(n_lik_param + self.n_warp_param)] @@ -133,7 +134,7 @@ def _parse_hyps(self, hyp, X, Xv=None): if self.warp is not None and self.warp_reparam: delta = np.exp(gamma[1]) beta = beta/(delta**2) - + # Create precision matrix from noise precision if Xv is not None: self.lambda_n_vec = beta @@ -142,18 +143,18 @@ def _parse_hyps(self, hyp, X, Xv=None): for v in range(len(self.var_ids)): beta_all[self.var_groups == self.var_ids[v]] = beta[v] self.lambda_n_vec = beta_all - else: + else: self.lambda_n_vec = np.ones(N)*beta - + return beta, alpha, gamma - + def post(self, hyp, X, y, Xv=None): """ Generic function to compute posterior distribution. This function will save the posterior mean and precision matrix as self.m and self.A and will also update internal parameters (e.g. N, D and the prior covariance (Sigma_a) and precision (Lambda_a). - + :param hyp: hyperparameter vector :param X: covariates :param y: responses @@ -169,7 +170,7 @@ def post(self, hyp, X, y, Xv=None): if (hyp == self.hyp).all() and hasattr(self, 'N'): print("hyperparameters have not changed, exiting") return - + beta, alpha, gamma = self._parse_hyps(hyp, X, Xv) if self.verbose: @@ -183,13 +184,13 @@ def post(self, hyp, X, y, Xv=None): raise ValueError("hyperparameter vector has invalid length") # compute posterior precision and mean - # this is equivalent to the following operation but makes much more + # this is equivalent to the following operation but makes much more # efficient use of memory by avoiding the need to store Lambda_n # # self.A = X.T.dot(self.Lambda_n).dot(X) + self.Lambda_a - # self.m = linalg.solve(self.A, X.T, + # self.m = linalg.solve(self.A, X.T, # check_finite=False).dot(self.Lambda_n).dot(y) - + XtLambda_n = X.T*self.lambda_n_vec self.A = XtLambda_n.dot(X) + self.Lambda_a invAXt = linalg.solve(self.A, X.T, check_finite=False) @@ -204,7 +205,7 @@ def loglik(self, hyp, X, y, Xv=None): """ Function to compute compute log (marginal) likelihood """ # hyperparameters (alpha not needed) - beta, alpha, gamma = self._parse_hyps(hyp, X, Xv) + beta, alpha, gamma = self._parse_hyps(hyp, X, Xv) # warp the likelihood? if self.warp is not None: @@ -212,16 +213,16 @@ def loglik(self, hyp, X, y, Xv=None): print('warping input...') y_unwarped = y y = self.warp.f(y, gamma) - + # load posterior and prior covariance - if (hyp != self.hyp).any() or not(hasattr(self, 'A')): + if (hyp != self.hyp).any() or not (hasattr(self, 'A')): try: self.post(hyp, X, y, Xv) except ValueError: print("Warning: Estimation of posterior distribution failed") nlZ = 1/np.finfo(float).eps return nlZ - + try: # compute the log determinants in a numerically stable way logdetA = 2*sum(np.log(np.diag(np.linalg.cholesky(self.A)))) @@ -230,9 +231,9 @@ def loglik(self, hyp, X, y, Xv=None): nlZ = 1/np.finfo(float).eps return nlZ - logdetSigma_a = sum(np.log(np.diag(self.Sigma_a))) # diagonal + logdetSigma_a = sum(np.log(np.diag(self.Sigma_a))) # diagonal logdetSigma_n = sum(np.log(1/self.lambda_n_vec)) - + # compute negative marginal log likelihood X_y_t_sLambda_n = (y-X.dot(self.m))*np.sqrt(self.lambda_n_vec) nlZ = -0.5 * (-self.N*np.log(2*np.pi) - @@ -242,10 +243,9 @@ def loglik(self, hyp, X, y, Xv=None): self.m.T.dot(self.Lambda_a).dot(self.m) - logdetA ) - - + if self.warp is not None: - # add in the Jacobian + # add in the Jacobian nlZ = nlZ - sum(np.log(self.warp.df(y_unwarped, gamma))) # make sure the output is finite to stop the minimizer getting upset @@ -257,10 +257,10 @@ def loglik(self, hyp, X, y, Xv=None): self.nlZ = nlZ return nlZ - + def penalized_loglik(self, hyp, X, y, Xv=None, l=0.1, norm='L1'): """ Function to compute the penalized log (marginal) likelihood - + :param hyp: hyperparameter vector :param X: covariates :param y: responses @@ -282,13 +282,13 @@ def dloglik(self, hyp, X, y, Xv=None): # hyperparameters beta, alpha, gamma = self._parse_hyps(hyp, X, Xv) - + if self.warp is not None: - raise ValueError('optimization with derivatives is not yet ' + \ + raise ValueError('optimization with derivatives is not yet ' + 'supported for warped liklihood') # load posterior and prior covariance - if (hyp != self.hyp).any() or not(hasattr(self, 'A')): + if (hyp != self.hyp).any() or not (hasattr(self, 'A')): try: self.post(hyp, X, y, Xv) except ValueError: @@ -296,49 +296,49 @@ def dloglik(self, hyp, X, y, Xv=None): dnlZ = np.sign(self.dnlZ) / np.finfo(float).eps return dnlZ - # precompute re-used quantities to maximise speed - # todo: revise implementation to use Cholesky throughout + # precompute re-used quantities to maximise speed + # todo: revise implementation to use Cholesky throughout # that would remove the need to explicitly compute the inverse S = np.linalg.inv(self.A) # posterior covariance SX = S.dot(X.T) - XLn = X.T*self.lambda_n_vec # = X.T.dot(self.Lambda_n) + XLn = X.T*self.lambda_n_vec # = X.T.dot(self.Lambda_n) XLny = XLn.dot(y) - SXLny = S.dot(XLny) + SXLny = S.dot(XLny) XLnXm = XLn.dot(X).dot(self.m) - + # initialise derivatives dnlZ = np.zeros(hyp.shape) dnl2 = np.zeros(hyp.shape) - + # noise precision parameter(s) for i in range(0, len(beta)): # first compute derivative of Lambda_n with respect to beta dL_n_vec = np.zeros(self.N) if self.var_groups is None: dL_n_vec = np.ones(self.N) - else: + else: dL_n_vec[np.where(self.var_groups == self.var_ids[i])[0]] = 1 dLambda_n = np.diag(dL_n_vec) - + # compute quantities used multiple times XdLnX = X.T.dot(dLambda_n).dot(X) dA = XdLnX - + # derivative of posterior parameters with respect to beta b = -S.dot(dA).dot(SXLny) + SX.dot(dLambda_n).dot(y) - + # compute np.trace(self.Sigma_n.dot(dLambda_n)) efficiently trSigma_ndLambda_n = sum((1/self.lambda_n_vec)*np.diag(dLambda_n)) # compute y.T.dot(Lambda_n) efficiently ytLn = (y*self.lambda_n_vec).T - + # compute derivatives - dnlZ[i] = - (0.5 * trSigma_ndLambda_n - + dnlZ[i] = - (0.5 * trSigma_ndLambda_n - 0.5 * y.dot(dLambda_n).dot(y) + y.dot(dLambda_n).dot(X).dot(self.m) + ytLn.dot(X).dot(b) - - 0.5 * self.m.T.dot(XdLnX).dot(self.m) - + 0.5 * self.m.T.dot(XdLnX).dot(self.m) - b.T.dot(XLnXm) - b.T.dot(self.Lambda_a).dot(self.m) - 0.5 * np.trace(S.dot(dA)) @@ -355,11 +355,11 @@ def dloglik(self, hyp, X, y, Xv=None): F = dLambda_a c = -S.dot(F).dot(SXLny) - + # compute np.trace(self.Sigma_a.dot(dLambda_a)) efficiently trSigma_adLambda_a = sum(np.diag(self.Sigma_a)*np.diag(dLambda_a)) - - dnlZ[i+len(beta)] = -(0.5* trSigma_adLambda_a + + + dnlZ[i+len(beta)] = -(0.5 * trSigma_adLambda_a + XLny.T.dot(c) - c.T.dot(XLnXm) - c.T.dot(self.Lambda_a).dot(self.m) - @@ -382,18 +382,18 @@ def dloglik(self, hyp, X, y, Xv=None): # model estimation (optimization) def estimate(self, hyp0, X, y, **kwargs): """ Function to estimate the model - + :param hyp: hyperparameter vector :param X: covariates :param y: responses :param optimizer: optimisation algorithm ('cg','powell','nelder-mead','l0bfgs-b') """ - - optimizer = kwargs.get('optimizer','cg') - + + optimizer = kwargs.get('optimizer', 'cg') + # covariates for heteroskedastic noise Xv = kwargs.get('var_covariates', None) - + # options for l-bfgs-b l = float(kwargs.get('l', 0.1)) epsilon = float(kwargs.get('epsilon', 0.1)) @@ -408,22 +408,26 @@ def estimate(self, hyp0, X, y, **kwargs): full_output=1) elif optimizer.lower() == 'nelder-mead': out = optimize.fmin(self.loglik, hyp0, (X, y, Xv), - full_output=1) + full_output=1) elif optimizer.lower() == 'l-bfgs-b': all_hyp_i = [hyp0] + def store(X): hyp = X all_hyp_i.append(hyp) try: out = optimize.fmin_l_bfgs_b(self.penalized_loglik, x0=hyp0, - args=(X, y, Xv, l, norm), approx_grad=True, - epsilon=epsilon, callback=store) + args=(X, y, Xv, l, + norm), approx_grad=True, + epsilon=epsilon, callback=store) # If the matrix becomes singular restart at last found hyp except np.linalg.LinAlgError: - print(f'Restarting estimation at hyp = {all_hyp_i[-1]}, due to *** numpy.linalg.LinAlgError: Matrix is singular.') + print( + f'Restarting estimation at hyp = {all_hyp_i[-1]}, due to *** numpy.linalg.LinAlgError: Matrix is singular.') out = optimize.fmin_l_bfgs_b(self.penalized_loglik, x0=all_hyp_i[-1], - args=(X, y, Xv, l, norm), approx_grad=True, - epsilon=epsilon) + args=(X, y, Xv, l, + norm), approx_grad=True, + epsilon=epsilon) else: raise ValueError("unknown optimizer") @@ -433,66 +437,67 @@ def store(X): return self.hyp - def predict(self, hyp, X, y, Xs, - var_groups_test=None, + def predict(self, hyp, X, y, Xs, + var_groups_test=None, var_covariates_test=None, **kwargs): """ Function to make predictions from the model - + :param hyp: hyperparameter vector :param X: covariates for training data :param y: responses for training data :param Xs: covariates for test data :param var_covariates_test: test covariates for heteroskedastic noise - + This always returns Gaussian predictions, i.e. - + :returns: * ys - predictive mean * s2 - predictive variance """ - + Xvs = var_covariates_test if Xvs is not None and len(Xvs.shape) == 1: Xvs = Xvs[:, np.newaxis] - + if X is None or y is None: # set dummy hyperparameters - beta, alpha, gamma = self._parse_hyps(hyp, np.zeros((self.N, self.D)), Xvs) + beta, alpha, gamma = self._parse_hyps( + hyp, np.zeros((self.N, self.D)), Xvs) else: - + # set hyperparameters beta, alpha, gamma = self._parse_hyps(hyp, X, Xvs) - + # do we need to re-estimate the posterior? - if (hyp != self.hyp).any() or not(hasattr(self, 'A')): - raise(ValueError, 'posterior not properly estimated') + if (hyp != self.hyp).any() or not (hasattr(self, 'A')): + raise (ValueError, 'posterior not properly estimated') N_test = Xs.shape[0] ys = Xs.dot(self.m) - + if self.var_groups is not None: if len(var_groups_test) != N_test: - raise(ValueError, 'Invalid variance groups for test') + raise (ValueError, 'Invalid variance groups for test') # separate variance groups s2n = np.ones(N_test) for v in range(len(self.var_ids)): s2n[var_groups_test == self.var_ids[v]] = 1/beta[v] else: s2n = 1/beta - + # compute xs.dot(S).dot(xs.T) avoiding computing off-diagonal entries s2 = s2n + np.sum(Xs*linalg.solve(self.A, Xs.T).T, axis=1) - - return ys, s2 - - def predict_and_adjust(self, hyp, X, y, Xs=None, - ys=None, - var_groups_test=None, - var_groups_adapt=None, **kwargs): + + return ys, s2 + + def predict_and_adjust(self, hyp, X, y, Xs=None, + ys=None, + var_groups_test=None, + var_groups_adapt=None, **kwargs): """ Function to transfer the model to a new site. This is done by first making predictions on the adaptation data given by X, adjusting by the residuals with respect to y. - + :param hyp: hyperparameter vector :param X: covariates for adaptation (i.e. calibration) data :param y: responses for adaptation data @@ -500,23 +505,23 @@ def predict_and_adjust(self, hyp, X, y, Xs=None, :param ys: true response variables (to be adjusted) :param var_groups_test: variance groups (e.g. sites) for test data :param var_groups_adapt: variance groups for adaptation data - + There are two possible ways of using this function, depending on whether ys or Xs is specified - + If ys is specified, this is applied directly to the data, which is assumed to be in the input space (i.e. not warped). In this case the adjusted true data points are returned in the same space - + Alternatively, Xs is specified, then the predictions are made and adjusted. In this case the predictive variance are returned in the warped (i.e. Gaussian) space. - + This function needs to know which sites are associated with which data points, which provided by var_groups_xxx, which is a list or array of scalar ids . """ - + if ys is None: if Xs is None: raise ValueError('Either ys or Xs must be specified') @@ -526,51 +531,55 @@ def predict_and_adjust(self, hyp, X, y, Xs=None, if len(ys.shape) < 1: raise ValueError('ys is specified but has insufficent length') N = ys.shape[0] - - if var_groups_test is None: + + if var_groups_test is None: var_groups_test = np.ones(N) var_groups_adapt = np.ones(X.shape[0]) - + ys_out = np.zeros(N) s2_out = np.zeros(N) for g in np.unique(var_groups_test): idx_s = var_groups_test == g idx_a = var_groups_adapt == g - + if sum(idx_a) < 2: - raise ValueError('Insufficient adaptation data to estimate variance') - + raise ValueError( + 'Insufficient adaptation data to estimate variance') + # Get predictions from old model on new data X - ys_ref, s2_ref = self.predict(hyp, None, None, X[idx_a,:]) - + ys_ref, s2_ref = self.predict(hyp, None, None, X[idx_a, :]) + # Subtract the predictions from true data to get the residuals if self.warp is None: residuals = ys_ref-y[idx_a] else: - # Calculate the residuals in warped space - y_ref_ws = self.warp.f(y[idx_a], hyp[1:self.warp.get_n_params()+1]) - residuals = ys_ref - y_ref_ws - + # Calculate the residuals in warped space + y_ref_ws = self.warp.f( + y[idx_a], hyp[1:self.warp.get_n_params()+1]) + residuals = ys_ref - y_ref_ws + residuals_mu = np.mean(residuals) residuals_sd = np.std(residuals) # Adjust the mean with the mean of the residuals if ys is None: # make and adjust predictions - ys_out[idx_s], s2_out[idx_s] = self.predict(hyp, None, None, Xs[idx_s,:]) - ys_out[idx_s] = ys_out[idx_s] - residuals_mu - + ys_out[idx_s], s2_out[idx_s] = self.predict( + hyp, None, None, Xs[idx_s, :]) + ys_out[idx_s] = ys_out[idx_s] - residuals_mu + # Set the deviation to the devations of the residuals s2_out[idx_s] = np.ones(len(s2_out[idx_s]))*residuals_sd**2 else: - # adjust the data + # adjust the data if self.warp is not None: - y_ws = self.warp.f(ys[idx_s], hyp[1:self.warp.get_n_params()+1]) - ys_out[idx_s] = y_ws + residuals_mu - ys_out[idx_s] = self.warp.invf(ys_out[idx_s], hyp[1:self.warp.get_n_params()+1]) + y_ws = self.warp.f( + ys[idx_s], hyp[1:self.warp.get_n_params()+1]) + ys_out[idx_s] = y_ws + residuals_mu + ys_out[idx_s] = self.warp.invf( + ys_out[idx_s], hyp[1:self.warp.get_n_params()+1]) else: - ys = ys - residuals_mu + ys = ys - residuals_mu s2_out = None return ys_out, s2_out - diff --git a/pcntoolkit/model/gp.py b/pcntoolkit/model/gp.py index b2fb1b75..2f8c5380 100644 --- a/pcntoolkit/model/gp.py +++ b/pcntoolkit/model/gp.py @@ -11,17 +11,17 @@ from abc import ABCMeta, abstractmethod -try: # Run as a package if installed +try: # Run as a package if installed from pcntoolkit.util.utils import squared_dist except ImportError: pass path = os.path.abspath(os.path.dirname(__file__)) - path = os.path.dirname(path) # parent directory + path = os.path.dirname(path) # parent directory if path not in sys.path: sys.path.append(path) del path - + from util.utils import squared_dist # -------------------- @@ -201,11 +201,11 @@ def __init__(self, x=None, covfuncnames=None): def cov(self, theta, x, z=None): theta_offset = 0 for ci, covfunc in enumerate(self.covfuncs): - try: + try: n_params_c = covfunc.get_n_params() theta_c = [theta[c] for c in range(theta_offset, theta_offset + n_params_c)] - theta_offset += n_params_c + theta_offset += n_params_c except Exception as e: print(e) @@ -283,14 +283,14 @@ def __init__(self, hyp=None, covfunc=None, X=None, y=None, n_iter=100, self.n_iter = n_iter self.verbose = verbose - # set up warped likelihood + # set up warped likelihood if warp is None: self.warp = None self.n_warp_param = 0 else: self.warp = warp self.n_warp_param = warp.get_n_params() - + self.gamma = None def _updatepost(self, hyp, covfunc): @@ -305,8 +305,8 @@ def _updatepost(self, hyp, covfunc): def post(self, hyp, covfunc, X, y): """ Generic function to compute posterior distribution. """ - - if len(hyp.shape) > 1: # force 1d hyperparameter array + + if len(hyp.shape) > 1: # force 1d hyperparameter array hyp = hyp.flatten() if len(X.shape) == 1: @@ -315,7 +315,7 @@ def post(self, hyp, covfunc, X, y): # hyperparameters sn2 = np.exp(2*hyp[0]) # noise variance - if self.warp is not None: # parameters for warping the likelhood + if self.warp is not None: # parameters for warping the likelhood n_lik_param = self.n_warp_param+1 else: n_lik_param = 1 @@ -337,14 +337,14 @@ def loglik(self, hyp, covfunc, X, y): # load or recompute posterior if self.verbose: print("computing likelihood ... | hyp=", hyp) - + # parameters for warping the likelhood function if self.warp is not None: gamma = hyp[1:(self.n_warp_param+1)] y = self.warp.f(y, gamma) y_unwarped = y - - if len(hyp.shape) > 1: # force 1d hyperparameter array + + if len(hyp.shape) > 1: # force 1d hyperparameter array hyp = hyp.flatten() if self._updatepost(hyp, covfunc): try: @@ -353,14 +353,14 @@ def loglik(self, hyp, covfunc, X, y): print("Warning: Estimation of posterior distribution failed") self.nlZ = 1/np.finfo(float).eps return self.nlZ - + self.nlZ = 0.5*y.T.dot(self.alpha) + sum(np.log(np.diag(self.L))) + \ - 0.5*self.N*np.log(2*np.pi) - + 0.5*self.N*np.log(2*np.pi) + if self.warp is not None: - # add in the Jacobian + # add in the Jacobian self.nlZ = self.nlZ - sum(np.log(self.warp.df(y_unwarped, gamma))) - + # make sure the output is finite to stop the minimizer getting upset if not np.isfinite(self.nlZ): self.nlZ = 1/np.finfo(float).eps @@ -374,13 +374,13 @@ def dloglik(self, hyp, covfunc, X, y): """ Function to compute derivatives """ - if len(hyp.shape) > 1: # force 1d hyperparameter array + if len(hyp.shape) > 1: # force 1d hyperparameter array hyp = hyp.flatten() - + if self.warp is not None: - raise ValueError('optimization with derivatives is not yet ' + \ + raise ValueError('optimization with derivatives is not yet ' + 'supported for warped liklihood') - + # hyperparameters sn2 = np.exp(2*hyp[0]) # noise variance theta = hyp[1:] # (generic) covariance hyperparameters @@ -429,7 +429,7 @@ def estimate(self, hyp0, covfunc, X, y, optimizer='cg'): X = X[:, np.newaxis] self.hyp0 = hyp0 - + if optimizer.lower() == 'cg': # conjugate gradients out = optimize.fmin_cg(self.loglik, hyp0, self.dloglik, (covfunc, X, y), disp=True, gtol=self.tol, @@ -454,26 +454,27 @@ def estimate(self, hyp0, covfunc, X, y, optimizer='cg'): def predict(self, hyp, X, y, Xs): """ Function to make predictions from the model """ - if len(hyp.shape) > 1: # force 1d hyperparameter array + if len(hyp.shape) > 1: # force 1d hyperparameter array hyp = hyp.flatten() - + # ensure X and Xs are multi-dimensional arrays if len(Xs.shape) == 1: Xs = Xs[:, np.newaxis] if len(X.shape) == 1: X = X[:, np.newaxis] - + # parameters for warping the likelhood function if self.warp is not None: gamma = hyp[1:(self.n_warp_param+1)] y = self.warp.f(y, gamma) - + # reestimate posterior (avoids numerical problems with optimizer) self.post(hyp, self.covfunc, X, y) - + # hyperparameters sn2 = np.exp(2*hyp[0]) # noise variance - theta = hyp[(self.n_warp_param + 1):] # (generic) covariance hyperparameters + # (generic) covariance hyperparameters + theta = hyp[(self.n_warp_param + 1):] Ks = self.covfunc.cov(theta, Xs, X) kss = self.covfunc.cov(theta, Xs) diff --git a/pcntoolkit/model/hbr.py b/pcntoolkit/model/hbr.py index ba1bea0e..ea7252c0 100644 --- a/pcntoolkit/model/hbr.py +++ b/pcntoolkit/model/hbr.py @@ -120,7 +120,8 @@ def from_posterior(param, samples, shape, distribution=None, half=False, freedo x = np.concatenate([x, [x[-1] + 0.1 * width]]) y = np.concatenate([y, [0]]) else: - x = np.concatenate([[x[0] - 0.1 * width], x, [x[-1] + 0.1 * width]]) + x = np.concatenate( + [[x[0] - 0.1 * width], x, [x[-1] + 0.1 * width]]) y = np.concatenate([[0], y, [0]]) if shape is None: return pm.distributions.Interpolated(param, x, y) @@ -277,7 +278,8 @@ def get_sample_dims(var): Any mapping that is applied here after sampling should also be applied in util.hbr_utils.forward in order for the functions there to properly work. For example, the softplus applied to sigma here is also applied in util.hbr_utils.forward """ - SHASH_map = {"SHASHb": SHASHb, "SHASHo": SHASHo, "SHASHo2": SHASHo2} + SHASH_map = {"SHASHb": SHASHb, + "SHASHo": SHASHo, "SHASHo2": SHASHo2} mu = pm.Deterministic( "mu_samples", @@ -342,7 +344,6 @@ def get_sample_dims(var): ) return model - class HBR: @@ -391,7 +392,8 @@ def transform_X(self, X): Phi = create_poly_basis(X, self.configs["order"]) elif self.model_type == "bspline": if self.bsp is None: - self.bsp = bspline_fit(X, self.configs["order"], self.configs["nknots"]) + self.bsp = bspline_fit( + X, self.configs["order"], self.configs["nknots"]) bspline = bspline_transform(X, self.bsp) Phi = np.concatenate((X, bspline), axis=1) else: @@ -453,7 +455,8 @@ def estimate(self, X, y, batch_effects, **kwargs): draw = self.idata.posterior.coords['draw'].data for j in self.idata.posterior.variables.mapping.keys(): if j.endswith('_samples'): - dummy_array = xarray.DataArray(data = np.zeros((len(chain), len(draw), 1)), coords = {'chain':chain, 'draw':draw,'empty':np.array([0])}, name=j) + dummy_array = xarray.DataArray(data=np.zeros((len(chain), len(draw), 1)), coords={ + 'chain': chain, 'draw': draw, 'empty': np.array([0])}, name=j) self.idata.posterior[j] = dummy_array self.vars_to_sample.append(j) @@ -492,7 +495,8 @@ def predict( # Make an array with occurences of all the values in be_train, but with the same size as be_test truncated_batch_effects_train = np.stack( [ - np.resize(np.array(list(batch_effects_maps[i].keys())), X.shape[0]) + np.resize( + np.array(list(batch_effects_maps[i].keys())), X.shape[0]) for i in range(batch_effects.shape[1]) ], axis=1, @@ -507,7 +511,7 @@ def predict( # Need to delete self.idata.posterior_predictive, otherwise, if it exists, it will not be overwritten if hasattr(self.idata, 'posterior_predictive'): del self.idata.posterior_predictive - + with modeler(X, y, truncated_batch_effects_train, self.configs) as model: # For each batch effect dim for i in range(batch_effects.shape[1]): @@ -524,8 +528,10 @@ def predict( progressbar=True, var_names=var_names, ) - pred_mean = self.idata.posterior_predictive["y_like"].to_numpy().mean(axis=(0, 1)) - pred_var = self.idata.posterior_predictive["y_like"].to_numpy().var(axis=(0, 1)) + pred_mean = self.idata.posterior_predictive["y_like"].to_numpy().mean( + axis=(0, 1)) + pred_var = self.idata.posterior_predictive["y_like"].to_numpy().var( + axis=(0, 1)) return pred_mean, pred_var @@ -601,7 +607,8 @@ def generate(self, X, batch_effects, samples): with modeler(X, y, batch_effects, self.configs): ppc = pm.sample_posterior_predictive(self.idata, progressbar=True) generated_samples = np.reshape( - ppc.posterior_predictive["y_like"].squeeze().T, [X.shape[0] * samples, 1] + ppc.posterior_predictive["y_like"].squeeze().T, [ + X.shape[0] * samples, 1] ) X = np.repeat(X, samples) if len(X.shape) == 1: @@ -611,7 +618,7 @@ def generate(self, X, batch_effects, samples): batch_effects = np.expand_dims(batch_effects, axis=1) return X, batch_effects, generated_samples - def sample_prior_predictive(self, X, batch_effects, samples, y = None, idata=None): + def sample_prior_predictive(self, X, batch_effects, samples, y=None, idata=None): """ Sample from the prior predictive distribution. @@ -623,7 +630,7 @@ def sample_prior_predictive(self, X, batch_effects, samples, y = None, idata=Non :param y: Outputs. If None, a zero array of appropriate shape is created. :param idata: An xarray dataset with the posterior distribution. If None, self.idata is used if it exists. :return: An xarray dataset with the prior predictive distribution. The results are also stored in the instance variable `self.idata`. - """ + """ if y is None: y = np.zeros([X.shape[0], 1]) X, y, batch_effects = expand_all(X, y, batch_effects) @@ -652,7 +659,6 @@ def get_model(self, X, y, batch_effects): return modeler(X, y, batch_effects, self.configs, idata=idata) def create_dummy_inputs(self, covariate_ranges=[[0.1, 0.9, 0.01]]): - """ Create dummy inputs for the model. @@ -672,7 +678,8 @@ def create_dummy_inputs(self, covariate_ranges=[[0.1, 0.9, 0.01]]): ) ) X = cartesian_product(arrays) - X_dummy = np.concatenate([X for i in range(np.prod(self.batch_effects_size))]) + X_dummy = np.concatenate( + [X for i in range(np.prod(self.batch_effects_size))]) arrays = [] for i in range(self.batch_effects_num): arrays.append(np.arange(0, self.batch_effects_size[i])) @@ -680,7 +687,7 @@ def create_dummy_inputs(self, covariate_ranges=[[0.1, 0.9, 0.01]]): batch_effects_dummy = np.repeat(batch_effects, X.shape[0], axis=0) return X_dummy, batch_effects_dummy - def Rhats(self, var_names=None, thin = 1, resolution = 100): + def Rhats(self, var_names=None, thin=1, resolution=100): """ Get Rhat of posterior samples as function of sampling iteration. @@ -692,21 +699,24 @@ def Rhats(self, var_names=None, thin = 1, resolution = 100): :return: A dictionary where the keys are variable names and the values are arrays of Rhat values. """ idata = self.idata - testvars = az.extract(idata, group='posterior', var_names=var_names, combined=False) - testvar_names = [var for var in list(testvars.data_vars.keys()) if not '_samples' in var] - rhat_dict={} + testvars = az.extract(idata, group='posterior', + var_names=var_names, combined=False) + testvar_names = [var for var in list( + testvars.data_vars.keys()) if not '_samples' in var] + rhat_dict = {} for var_name in testvar_names: - var = np.stack(testvars[var_name].to_numpy())[:,::thin] + var = np.stack(testvars[var_name].to_numpy())[:, ::thin] var = var.reshape((var.shape[0], var.shape[1], -1)) vardim = var.shape[2] - interval_skip=var.shape[1]//resolution + interval_skip = var.shape[1]//resolution rhats_var = np.zeros((resolution, vardim)) for v in range(vardim): for j in range(resolution): - rhats_var[j,v] = az.rhat(var[:,:j*interval_skip,v]) + rhats_var[j, v] = az.rhat(var[:, :j*interval_skip, v]) rhat_dict[var_name] = rhats_var return rhat_dict + class Prior: """ A wrapper class for a PyMC distribution. @@ -758,14 +768,16 @@ def make_dist(self, dist, params, pb): # Get samples samples = az.extract(pb.idata, var_names=self.name) # Define mapping to new shape + def get_new_dim_size(tup): oldsize, name = tup if name.startswith('batch_effect_'): ind = pb.batch_effect_dim_names.index(name) return len(np.unique(pb.batch_effect_indices[ind].container.data)) return oldsize - - new_shape = list(map(get_new_dim_size, zip(samples.shape,samples.dims))) + + new_shape = list( + map(get_new_dim_size, zip(samples.shape, samples.dims))) if len(new_shape) == 1: new_shape = None else: @@ -773,7 +785,7 @@ def get_new_dim_size(tup): self.dist = from_posterior( param=self.name, samples=samples.to_numpy(), - shape = new_shape, + shape=new_shape, distribution=dist, freedom=pb.configs["freedom"], ) @@ -787,7 +799,8 @@ def get_new_dim_size(tup): if dims == []: self.dist = self.distmap[dist](self.name, *params) else: - self.dist = self.distmap[dist](self.name, *params, dims=dims) + self.dist = self.distmap[dist]( + self.name, *params, dims=dims) def __getitem__(self, idx): """ @@ -864,7 +877,8 @@ def make_param(self, name, **kwargs): if self.configs.get(f"linear_{name}", False): # First make a slope and intercept, and use those to make a linear parameterization slope_parameterization = self.make_param(f"slope_{name}", **kwargs) - intercept_parameterization = self.make_param(f"intercept_{name}", **kwargs) + intercept_parameterization = self.make_param( + f"intercept_{name}", **kwargs) return LinearParameterization( name=name, slope_parameterization=slope_parameterization, @@ -917,6 +931,7 @@ class FixedParameterization(Parameterization): It does not depend on anything except its hyperparameters. This class inherits from the Parameterization class. """ + def __init__(self, name, pb: ParamBuilder, **kwargs): """ Initialize the FixedParameterization object. @@ -948,7 +963,7 @@ def get_samples(self, pb): class CentralRandomFixedParameterization(Parameterization): """ A parameterization that is fixed for each batch effect. - + This is sampled in a central fashion; the values are sampled from normal distribution with a group mean and group variance """ @@ -1146,14 +1161,16 @@ def nn_hbr(X, y, batch_effects, batch_effects_size, configs, idata=None): init_1_noise = pm.floatX( np.random.randn(feature_num, n_hidden) * np.sqrt(1 / feature_num) ) - init_out_noise = pm.floatX(np.random.randn(n_hidden) * np.sqrt(1 / n_hidden)) + init_out_noise = pm.floatX(np.random.randn( + n_hidden) * np.sqrt(1 / n_hidden)) std_init_1_noise = pm.floatX(np.random.rand(feature_num, n_hidden)) std_init_out_noise = pm.floatX(np.random.rand(n_hidden)) # If there are two hidden layers, then initialize weights for the second layer: if n_layers == 2: - init_2 = pm.floatX(np.random.randn(n_hidden, n_hidden) * np.sqrt(1 / n_hidden)) + init_2 = pm.floatX(np.random.randn( + n_hidden, n_hidden) * np.sqrt(1 / n_hidden)) std_init_2 = pm.floatX(np.random.rand(n_hidden, n_hidden)) init_2_noise = pm.floatX( np.random.randn(n_hidden, n_hidden) * np.sqrt(1 / n_hidden) @@ -1254,7 +1271,8 @@ def nn_hbr(X, y, batch_effects, batch_effects_size, configs, idata=None): ) # mu_prior_intercept = pm.Uniform('mu_prior_intercept', lower=-100, upper=100) - mu_prior_intercept = pm.Normal("mu_prior_intercept", mu=0.0, sigma=1e3) + mu_prior_intercept = pm.Normal( + "mu_prior_intercept", mu=0.0, sigma=1e3) sigma_prior_intercept = pm.HalfCauchy("sigma_prior_intercept", 5) # Now create separate weights for each group, by doing @@ -1293,17 +1311,21 @@ def nn_hbr(X, y, batch_effects, batch_effects_size, configs, idata=None): a.append(batch_effects[:, i] == b) idx = reduce(np.logical_and, a).nonzero() if idx[0].shape[0] != 0: - act_1 = pm.math.tanh(pytensor.tensor.dot(X[idx, :], weights_in_1[be])) + act_1 = pm.math.tanh(pytensor.tensor.dot( + X[idx, :], weights_in_1[be])) if n_layers == 2: - act_2 = pm.math.tanh(pytensor.tensor.dot(act_1, weights_1_2[be])) + act_2 = pm.math.tanh( + pytensor.tensor.dot(act_1, weights_1_2[be])) y_hat = pytensor.tensor.set_subtensor( y_hat[idx, 0], - intercepts[be] + pytensor.tensor.dot(act_2, weights_2_out[be]), + intercepts[be] + + pytensor.tensor.dot(act_2, weights_2_out[be]), ) else: y_hat = pytensor.tensor.set_subtensor( y_hat[idx, 0], - intercepts[be] + pytensor.tensor.dot(act_1, weights_2_out[be]), + intercepts[be] + + pytensor.tensor.dot(act_1, weights_2_out[be]), ) # If we want to estimate varying noise terms across groups: @@ -1450,11 +1472,13 @@ def nn_hbr(X, y, batch_effects, batch_effects_size, configs, idata=None): idx = reduce(np.logical_and, a).nonzero() if idx[0].shape[0] != 0: act_1_noise = pm.math.sigmoid( - pytensor.tensor.dot(X[idx, :], weights_in_1_noise[be]) + pytensor.tensor.dot( + X[idx, :], weights_in_1_noise[be]) ) if n_layers == 2: act_2_noise = pm.math.sigmoid( - pytensor.tensor.dot(act_1_noise, weights_1_2_noise[be]) + pytensor.tensor.dot( + act_1_noise, weights_1_2_noise[be]) ) temp = ( pm.math.log1pexp( @@ -1473,7 +1497,8 @@ def nn_hbr(X, y, batch_effects, batch_effects_size, configs, idata=None): ) + 1e-5 ) - sigma_y = pytensor.tensor.set_subtensor(sigma_y[idx, 0], temp) + sigma_y = pytensor.tensor.set_subtensor( + sigma_y[idx, 0], temp) else: # homoscedastic noise: if idata is not None: # Used for transferring the priors @@ -1503,7 +1528,8 @@ def nn_hbr(X, y, batch_effects, batch_effects_size, configs, idata=None): else: # do not allow for random noise terms across groups: if idata is not None: # Used for transferring the priors upper_bound = np.percentile(idata["sigma_noise"], 95) - sigma_noise = pm.Uniform("sigma_noise", lower=0, upper=2 * upper_bound) + sigma_noise = pm.Uniform( + "sigma_noise", lower=0, upper=2 * upper_bound) else: sigma_noise = pm.Uniform("sigma_noise", lower=0, upper=100) sigma_y = pytensor.tensor.zeros(y.shape) @@ -1528,7 +1554,8 @@ def nn_hbr(X, y, batch_effects, batch_effects_size, configs, idata=None): a.append(batch_effects[:, i] == b) idx = reduce(np.logical_and, a).nonzero() if idx[0].shape[0] != 0: - alpha = pytensor.tensor.set_subtensor(alpha[idx, 0], skewness[be]) + alpha = pytensor.tensor.set_subtensor( + alpha[idx, 0], skewness[be]) else: alpha = 0 # symmetrical normal distribution diff --git a/pcntoolkit/model/rfa.py b/pcntoolkit/model/rfa.py index 7e67169b..b8d1fdff 100644 --- a/pcntoolkit/model/rfa.py +++ b/pcntoolkit/model/rfa.py @@ -4,6 +4,7 @@ import numpy as np import torch + class GPRRFA: """Random Feature Approximation for Gaussian Process Regression @@ -33,9 +34,9 @@ class GPRRFA: where sn^2 is the noise variance, ell are lengthscale parameters and sf^2 is the signal variance. This provides an approximation to the covariance function:: - + k(x,z) = x'*z + sn2*exp(0.5*(x-z)'*Lambda*(x-z)) - + where Lambda = diag((ell_1^2, ... ell_D^2)) Written by A. Marquand @@ -58,37 +59,37 @@ def __init__(self, hyp=None, X=None, y=None, n_feat=None, def _numpy2torch(self, X, y=None, hyp=None): if type(X) is torch.Tensor: - pass + pass elif type(X) is np.ndarray: - X = torch.from_numpy(X) + X = torch.from_numpy(X) else: - raise(ValueError, 'Unknown data type (X)') + raise (ValueError, 'Unknown data type (X)') X = X.double() - + if y is not None: if type(y) is torch.Tensor: pass elif type(y) is np.ndarray: y = torch.from_numpy(y) else: - raise(ValueError, 'Unknown data type (y)') - + raise (ValueError, 'Unknown data type (y)') + if len(y.shape) == 1: - y.resize_(y.shape[0],1) + y.resize_(y.shape[0], 1) y = y.double() - + if hyp is not None: if type(hyp) is torch.Tensor: pass else: hyp = torch.tensor(hyp, requires_grad=True) - + return X, y, hyp - + def get_n_params(self, X): - + return X.shape[1] + 2 - + def post(self, hyp, X, y): """ Generic function to compute posterior distribution. @@ -96,42 +97,42 @@ def post(self, hyp, X, y): self.m and self.A and will also update internal parameters (e.g. N, D and the prior covariance (Sigma) and precision (iSigma). """ - + # make sure all variables are the right type X, y, hyp = self._numpy2torch(X, y, hyp) - + self.N, self.Dx = X.shape - + # ensure the number of features is specified (use 75% as a default) if self.Nf is None: self.Nf = int(0.75 * self.N) - + self.Omega = torch.zeros((self.Dx, self.Nf), dtype=torch.double) for f in range(self.Nf): - self.Omega[:,f] = torch.exp(hyp[1:-1]) * \ - torch.randn((self.Dx, 1), dtype=torch.double).squeeze() + self.Omega[:, f] = torch.exp(hyp[1:-1]) * \ + torch.randn((self.Dx, 1), dtype=torch.double).squeeze() - XO = torch.mm(X, self.Omega) + XO = torch.mm(X, self.Omega) self.Phi = torch.exp(hyp[-1])/np.sqrt(self.Nf) * \ - torch.cat((torch.cos(XO), torch.sin(XO)), 1) - - # concatenate linear weights + torch.cat((torch.cos(XO), torch.sin(XO)), 1) + + # concatenate linear weights self.Phi = torch.cat((self.Phi, X), 1) self.D = self.Phi.shape[1] if self.verbose: print("estimating posterior ... | hyp=", hyp) - + self.A = torch.mm(torch.t(self.Phi), self.Phi) / torch.exp(2*hyp[0]) + \ - torch.eye(self.D, dtype=torch.double) + torch.eye(self.D, dtype=torch.double) self.m = torch.mm(torch.solve(torch.t(self.Phi), self.A)[0], y) / \ - torch.exp(2*hyp[0]) + torch.exp(2*hyp[0]) # save hyperparameters self.hyp = hyp - + # update optimizer iteration count - if hasattr(self,'_iterations'): + if hasattr(self, '_iterations'): self._iterations += 1 def loglik(self, hyp, X, y): @@ -141,23 +142,24 @@ def loglik(self, hyp, X, y): # always recompute the posterior self.post(hyp, X, y) - #logdetA = 2*torch.sum(torch.log(torch.diag(torch.cholesky(self.A)))) + # logdetA = 2*torch.sum(torch.log(torch.diag(torch.cholesky(self.A)))) try: # compute the log determinants in a numerically stable way - logdetA = 2*torch.sum(torch.log(torch.diag(torch.cholesky(self.A)))) + logdetA = 2 * \ + torch.sum(torch.log(torch.diag(torch.cholesky(self.A)))) except Exception as e: print("Warning: Estimation of posterior distribution failed") print(e) - #nlZ = torch.tensor(1/np.finfo(float).eps) + # nlZ = torch.tensor(1/np.finfo(float).eps) nlZ = torch.tensor(np.nan) self._optim_failed = True return nlZ - + # compute negative marginal log likelihood - nlZ = -0.5 * (self.N*torch.log(1/torch.exp(2*hyp[0])) - + nlZ = -0.5 * (self.N*torch.log(1/torch.exp(2*hyp[0])) - self.N*np.log(2*np.pi) - - torch.mm(torch.t(y - torch.mm(self.Phi,self.m)), - (y - torch.mm(self.Phi,self.m))) / + torch.mm(torch.t(y - torch.mm(self.Phi, self.m)), + (y - torch.mm(self.Phi, self.m))) / torch.exp(2*hyp[0]) - torch.mm(torch.t(self.m), self.m) - logdetA) @@ -177,41 +179,41 @@ def dloglik(self, hyp, X, y): def estimate(self, hyp0, X, y, optimizer='lbfgs'): """ Function to estimate the model """ - + if type(hyp0) is torch.Tensor: hyp = hyp0 hyp0.requires_grad_() else: - hyp = torch.tensor(hyp0, requires_grad=True) + hyp = torch.tensor(hyp0, requires_grad=True) # save the starting values self.hyp0 = hyp - + if optimizer.lower() == 'lbfgs': opt = torch.optim.LBFGS([hyp]) else: - raise(ValueError, "Optimizer " + " not implemented") + raise (ValueError, "Optimizer " + " not implemented") self._iterations = 0 - + def closure(): opt.zero_grad() nlZ = self.loglik(hyp, X, y) if not torch.isnan(nlZ): nlZ.backward() return nlZ - + for r in range(self._n_restarts): self._optim_failed = False - + nlZ = opt.step(closure) - + if self._optim_failed: - print("optimization failed. retrying (", r+1, "of", - self._n_restarts,")") + print("optimization failed. retrying (", r+1, "of", + self._n_restarts, ")") hyp = torch.randn_like(hyp, requires_grad=True) self.hyp0 = hyp else: - print("Optimzation complete after", self._iterations, - "evaluations. Function value =", + print("Optimzation complete after", self._iterations, + "evaluations. Function value =", nlZ.detach().numpy().squeeze()) break @@ -223,21 +225,21 @@ def predict(self, hyp, X, y, Xs): X, y, hyp = self._numpy2torch(X, y, hyp) Xs, *_ = self._numpy2torch(Xs) - if (hyp != self.hyp).all() or not(hasattr(self, 'A')): + if (hyp != self.hyp).all() or not (hasattr(self, 'A')): self.post(hyp, X, y) - + # generate prediction tensors - XsO = torch.mm(Xs, self.Omega) + XsO = torch.mm(Xs, self.Omega) Phis = torch.exp(hyp[-1])/np.sqrt(self.Nf) * \ - torch.cat((torch.cos(XsO), torch.sin(XsO)), 1) + torch.cat((torch.cos(XsO), torch.sin(XsO)), 1) # add linear component Phis = torch.cat((Phis, Xs), 1) - + ys = torch.mm(Phis, self.m) # compute diag(Phis*(Phis'\A)) avoiding computing off-diagonal entries s2 = torch.exp(2*hyp[0]) + \ - torch.sum(Phis * torch.t(torch.solve(torch.t(Phis), self.A)[0]), 1) + torch.sum(Phis * torch.t(torch.solve(torch.t(Phis), self.A)[0]), 1) # return output as numpy arrays return ys.detach().numpy().squeeze(), s2.detach().numpy().squeeze() diff --git a/pcntoolkit/normative.py b/pcntoolkit/normative.py index 95688018..19afb9da 100755 --- a/pcntoolkit/normative.py +++ b/pcntoolkit/normative.py @@ -23,7 +23,7 @@ from sklearn.model_selection import KFold from pathlib import Path - + try: # run as a package if installed from pcntoolkit import configs from pcntoolkit.dataio import fileio @@ -36,9 +36,9 @@ path = os.path.abspath(os.path.dirname(__file__)) if path not in sys.path: sys.path.append(path) - #sys.path.append(os.path.join(path,'normative_model')) + # sys.path.append(os.path.join(path,'normative_model')) del path - + import configs from dataio import fileio @@ -48,6 +48,7 @@ PICKLE_PROTOCOL = configs.PICKLE_PROTOCOL + def load_response_vars(datafile, maskfile=None, vol=True): """ Load response variables from file. This will load the data and mask it if @@ -92,11 +93,10 @@ def get_args(*args): :returns kw_args: Additional keyword arguments """ - # parse arguments parser = argparse.ArgumentParser(description="Normative Modeling") parser.add_argument("responses") - parser.add_argument("-f", help="Function to call", dest="func", + parser.add_argument("-f", help="Function to call", dest="func", default="estimate") parser.add_argument("-m", help="mask file", dest="maskfile", default=None) parser.add_argument("-c", help="covariates file", dest="covfile", @@ -108,22 +108,22 @@ def get_args(*args): parser.add_argument("-r", help="responses (test data)", dest="testresp", default=None) parser.add_argument("-a", help="algorithm", dest="alg", default="gpr") - parser.add_argument("-x", help="algorithm specific config options", + parser.add_argument("-x", help="algorithm specific config options", dest="configparam", default=None) - # parser.add_argument('-s', action='store_false', + # parser.add_argument('-s', action='store_false', # help="Flag to skip standardization.", dest="standardize") parser.add_argument("keyword_args", nargs=argparse.REMAINDER) - + args = parser.parse_args() - - # Process required arguemnts + + # Process required arguemnts wdir = os.path.realpath(os.path.curdir) respfile = os.path.join(wdir, args.responses) if args.covfile is None: - raise(ValueError, "No covariates specified") + raise (ValueError, "No covariates specified") else: covfile = args.covfile - + # Process optional arguments if args.maskfile is None: maskfile = None @@ -150,31 +150,31 @@ def get_args(*args): kw_args = {} for kw in args.keyword_args: kw_arg = kw.split('=') - - exec("kw_args.update({'" + kw_arg[0] + "' : " + - "'" + str(kw_arg[1]) + "'" + "})") - + + exec("kw_args.update({'" + kw_arg[0] + "' : " + + "'" + str(kw_arg[1]) + "'" + "})") + return respfile, maskfile, covfile, cvfolds, \ - testcov, testresp, args.func, args.alg, \ - args.configparam, kw_args - + testcov, testresp, args.func, args.alg, \ + args.configparam, kw_args + def evaluate(Y, Yhat, S2=None, mY=None, sY=None, nlZ=None, nm=None, Xz_tr=None, alg=None, - metrics = ['Rho', 'RMSE', 'SMSE', 'EXPV', 'MSLL']): + metrics=['Rho', 'RMSE', 'SMSE', 'EXPV', 'MSLL']): ''' Compute error metrics This function will compute error metrics based on a set of predictions Yhat and a set of true response variables Y, namely: - + * Rho: Pearson correlation * RMSE: root mean squared error * SMSE: standardized mean squared error * EXPV: explained variance - + If the predictive variance is also specified the log loss will be computed (which also takes into account the predictive variance). If the mean and standard deviation are also specified these will be used to standardize this, yielding the mean standardized log loss - + :param Y: N x P array of true response variables :param Yhat: N x P array of predicted response variables :param S2: predictive variance @@ -182,68 +182,69 @@ def evaluate(Y, Yhat, S2=None, mY=None, sY=None, nlZ=None, nm=None, Xz_tr=None, :param sY: standard deviation of the training set :returns metrics: evaluation metrics - + ''' - + feature_num = Y.shape[1] - - # Remove metrics that cannot be computed with only a single data point + + # Remove metrics that cannot be computed with only a single data point if Y.shape[0] == 1: if 'MSLL' in metrics: metrics.remove('MSLL') if 'SMSE' in metrics: metrics.remove('SMSE') - + # find and remove bad variables from the response variables nz = np.where(np.bitwise_and(np.isfinite(Y).any(axis=0), np.var(Y, axis=0) != 0))[0] - + MSE = np.mean((Y - Yhat)**2, axis=0) - + results = dict() - + if 'RMSE' in metrics: RMSE = np.sqrt(MSE) results['RMSE'] = RMSE - + if 'Rho' in metrics: Rho = np.zeros(feature_num) - pRho = np.ones(feature_num) - Rho[nz], pRho[nz] = compute_pearsonr(Y[:,nz], Yhat[:,nz]) + pRho = np.ones(feature_num) + Rho[nz], pRho[nz] = compute_pearsonr(Y[:, nz], Yhat[:, nz]) results['Rho'] = Rho results['pRho'] = pRho - + if 'SMSE' in metrics: SMSE = np.zeros_like(MSE) - SMSE[nz] = MSE[nz] / np.var(Y[:,nz], axis=0) + SMSE[nz] = MSE[nz] / np.var(Y[:, nz], axis=0) results['SMSE'] = SMSE - + if 'EXPV' in metrics: EXPV = np.zeros(feature_num) - EXPV[nz] = explained_var(Y[:,nz], Yhat[:,nz]) + EXPV[nz] = explained_var(Y[:, nz], Yhat[:, nz]) results['EXPV'] = EXPV - + if 'MSLL' in metrics: if ((S2 is not None) and (mY is not None) and (sY is not None)): MSLL = np.zeros(feature_num) - MSLL[nz] = compute_MSLL(Y[:,nz], Yhat[:,nz], S2[:,nz], - mY.reshape(-1,1).T, - (sY**2).reshape(-1,1).T) + MSLL[nz] = compute_MSLL(Y[:, nz], Yhat[:, nz], S2[:, nz], + mY.reshape(-1, 1).T, + (sY**2).reshape(-1, 1).T) results['MSLL'] = MSLL - + if 'NLL' in metrics: results['NLL'] = nlZ - + if 'BIC' in metrics: if hasattr(getattr(nm, alg), 'hyp'): n = Xz_tr.shape[0] k = len(getattr(nm, alg).hyp) BIC = k * np.log(n) + 2 * nlZ - results['BIC'] = BIC - + results['BIC'] = BIC + return results -def save_results(respfile, Yhat, S2, maskvol, Z=None, Y=None, outputsuffix=None, + +def save_results(respfile, Yhat, S2, maskvol, Z=None, Y=None, outputsuffix=None, results=None, save_path=''): """ Writes the results of the normative model to disk. @@ -262,7 +263,7 @@ def save_results(respfile, Yhat, S2, maskvol, Z=None, Y=None, outputsuffix=None, Returns: None """ - + print("Writing outputs ...") if respfile is None: exfile = None @@ -280,32 +281,32 @@ def save_results(respfile, Yhat, S2, maskvol, Z=None, Y=None, outputsuffix=None, else: ext = file_ext - fileio.save(Yhat, os.path.join(save_path, 'yhat' + ext), example=exfile, - mask=maskvol) - fileio.save(S2, os.path.join(save_path, 'ys2' + ext), example=exfile, + fileio.save(Yhat, os.path.join(save_path, 'yhat' + ext), example=exfile, + mask=maskvol) + fileio.save(S2, os.path.join(save_path, 'ys2' + ext), example=exfile, mask=maskvol) if Z is not None: - fileio.save(Z, os.path.join(save_path, 'Z' + ext), example=exfile, + fileio.save(Z, os.path.join(save_path, 'Z' + ext), example=exfile, mask=maskvol) if Y is not None: - fileio.save(Y, os.path.join(save_path, 'Y' + ext), example=exfile, + fileio.save(Y, os.path.join(save_path, 'Y' + ext), example=exfile, mask=maskvol) - if results is not None: + if results is not None: for metric in list(results.keys()): if (metric == 'NLL' or metric == 'BIC') and file_ext == '.nii.gz': - fileio.save(results[metric], os.path.join(save_path, metric + str(outputsuffix) + '.pkl'), - example=exfile, mask=maskvol) + fileio.save(results[metric], os.path.join(save_path, metric + str(outputsuffix) + '.pkl'), + example=exfile, mask=maskvol) else: - fileio.save(results[metric], os.path.join(save_path, metric + ext), + fileio.save(results[metric], os.path.join(save_path, metric + ext), example=exfile, mask=maskvol) - + def estimate(covfile, respfile, **kwargs): """ Estimate a normative model This will estimate a model in one of two settings according to theparticular parameters specified (see below) - + * under k-fold cross-validation. requires respfile, covfile and cvfolds>=2 * estimating a training dataset then applying to a second test dataset. @@ -355,35 +356,36 @@ def estimate(covfile, respfile, **kwargs): The outputsuffix may be useful to estimate multiple normative models in the same directory (e.g. for custom cross-validation schemes) """ - - # parse keyword arguments - maskfile = kwargs.pop('maskfile',None) + + # parse keyword arguments + maskfile = kwargs.pop('maskfile', None) cvfolds = kwargs.pop('cvfolds', None) testcov = kwargs.pop('testcov', None) - testresp = kwargs.pop('testresp',None) - alg = kwargs.pop('alg','gpr') - outputsuffix = kwargs.pop('outputsuffix','estimate') - outputsuffix = "_" + outputsuffix.replace("_", "") # Making sure there is only one - # '_' is in the outputsuffix to - # avoid file name parsing problem. - inscaler = kwargs.pop('inscaler','None') - outscaler = kwargs.pop('outscaler','None') + testresp = kwargs.pop('testresp', None) + alg = kwargs.pop('alg', 'gpr') + outputsuffix = kwargs.pop('outputsuffix', 'estimate') + # Making sure there is only one + outputsuffix = "_" + outputsuffix.replace("_", "") + # '_' is in the outputsuffix to + # avoid file name parsing problem. + inscaler = kwargs.pop('inscaler', 'None') + outscaler = kwargs.pop('outscaler', 'None') warp = kwargs.get('warp', None) # convert from strings if necessary - saveoutput = kwargs.pop('saveoutput','True') + saveoutput = kwargs.pop('saveoutput', 'True') if type(saveoutput) is str: - saveoutput = saveoutput=='True' - savemodel = kwargs.pop('savemodel','False') + saveoutput = saveoutput == 'True' + savemodel = kwargs.pop('savemodel', 'False') if type(savemodel) is str: - savemodel = savemodel=='True' - + savemodel = savemodel == 'True' + if savemodel and not os.path.isdir('Models'): os.mkdir('Models') # which output metrics to compute - metrics = ['Rho', 'RMSE', 'SMSE', 'EXPV', 'MSLL','NLL', 'BIC'] - + metrics = ['Rho', 'RMSE', 'SMSE', 'EXPV', 'MSLL', 'NLL', 'BIC'] + # load data print("Processing data in " + respfile) X = fileio.load(covfile) @@ -393,9 +395,9 @@ def estimate(covfile, respfile, **kwargs): if len(X.shape) == 1: X = X[:, np.newaxis] Nmod = Y.shape[1] - - if (testcov is not None) and (cvfolds is None): # a separate test dataset - + + if (testcov is not None) and (cvfolds is None): # a separate test dataset + run_cv = False cvfolds = 1 Xte = fileio.load(testcov) @@ -408,28 +410,28 @@ def estimate(covfile, respfile, **kwargs): else: sub_te = Xte.shape[0] Yte = np.zeros([sub_te, Nmod]) - + # treat as a single train-test split testids = range(X.shape[0], X.shape[0]+Xte.shape[0]) splits = CustomCV((range(0, X.shape[0]),), (testids,)) Y = np.concatenate((Y, Yte), axis=0) X = np.concatenate((X, Xte), axis=0) - + else: run_cv = True # we are running under cross-validation splits = KFold(n_splits=cvfolds, shuffle=True) testids = range(0, X.shape[0]) - if alg=='hbr': - trbefile = kwargs.get('trbefile', None) - if trbefile is not None: + if alg == 'hbr': + trbefile = kwargs.get('trbefile', None) + if trbefile is not None: be = fileio.load(trbefile) if len(be.shape) == 1: be = be[:, np.newaxis] - else: + else: print('No batch-effects file! Initilizing all as zeros!') - be = np.zeros([X.shape[0],1]) + be = np.zeros([X.shape[0], 1]) # find and remove bad variables from the response variables # note: the covariates are assumed to have already been checked @@ -441,19 +443,19 @@ def estimate(covfile, respfile, **kwargs): S2 = np.zeros_like(Y) Z = np.zeros_like(Y) nlZ = np.zeros((Nmod, cvfolds)) - + scaler_resp = [] scaler_cov = [] - mean_resp = [] # this is just for computing MSLL - std_resp = [] # this is just for computing MSLL - + mean_resp = [] # this is just for computing MSLL + std_resp = [] # this is just for computing MSLL + if warp is not None: Ywarp = np.zeros_like(Yhat) - + # for warping we need to compute metrics separately for each fold results_folds = dict() for m in metrics: - results_folds[m]= np.zeros((Nmod, cvfolds)) + results_folds[m] = np.zeros((Nmod, cvfolds)) for idx in enumerate(splits.split(X)): @@ -468,7 +470,7 @@ def estimate(covfile, respfile, **kwargs): sY = np.std(Y[iy_tr, jy_tr], axis=0) mean_resp.append(mY) std_resp.append(sY) - + if inscaler in ['standardize', 'minmax', 'robminmax']: X_scaler = scaler(inscaler) Xz_tr = X_scaler.fit_transform(X[tr, :]) @@ -477,34 +479,34 @@ def estimate(covfile, respfile, **kwargs): else: Xz_tr = X[tr, :] Xz_ts = X[ts, :] - + if outscaler in ['standardize', 'minmax', 'robminmax']: Y_scaler = scaler(outscaler) Yz_tr = Y_scaler.fit_transform(Y[iy_tr, jy_tr]) scaler_resp.append(Y_scaler) else: Yz_tr = Y[iy_tr, jy_tr] - - if (run_cv==True and alg=='hbr'): - fileio.save(be[tr,:], 'be_kfold_tr_tempfile.pkl') - fileio.save(be[ts,:], 'be_kfold_ts_tempfile.pkl') + + if (run_cv == True and alg == 'hbr'): + fileio.save(be[tr, :], 'be_kfold_tr_tempfile.pkl') + fileio.save(be[ts, :], 'be_kfold_ts_tempfile.pkl') kwargs['trbefile'] = 'be_kfold_tr_tempfile.pkl' kwargs['tsbefile'] = 'be_kfold_ts_tempfile.pkl' # estimate the models for all response variables - for i in range(0, len(nz)): + for i in range(0, len(nz)): print("Estimating model ", i+1, "of", len(nz)) nm = norm_init(Xz_tr, Yz_tr[:, i], alg=alg, **kwargs) - + try: - nm = nm.estimate(Xz_tr, Yz_tr[:, i], **kwargs) + nm = nm.estimate(Xz_tr, Yz_tr[:, i], **kwargs) yhat, s2 = nm.predict(Xz_ts, Xz_tr, Yz_tr[:, i], **kwargs) - + if savemodel: - nm.save('Models/NM_' + str(fold) + '_' + str(nz[i]) + - outputsuffix + '.pkl' ) - - if outscaler == 'standardize': + nm.save('Models/NM_' + str(fold) + '_' + str(nz[i]) + + outputsuffix + '.pkl') + + if outscaler == 'standardize': Yhat[ts, nz[i]] = Y_scaler.inverse_transform(yhat, index=i) S2[ts, nz[i]] = s2 * sY[i]**2 elif outscaler in ['minmax', 'robminmax']: @@ -513,21 +515,23 @@ def estimate(covfile, respfile, **kwargs): else: Yhat[ts, nz[i]] = yhat S2[ts, nz[i]] = s2 - + nlZ[nz[i], fold] = nm.neg_log_lik - + if (run_cv or testresp is not None): if warp is not None: # TODO: Warping for scaled data if outscaler is not None and outscaler != 'None': - raise(ValueError, "outscaler not yet supported warping") - warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] - Ywarp[ts, nz[i]] = nm.blr.warp.f(Y[ts, nz[i]], warp_param) + raise ( + ValueError, "outscaler not yet supported warping") + warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] + Ywarp[ts, nz[i]] = nm.blr.warp.f( + Y[ts, nz[i]], warp_param) Ytest = Ywarp[ts, nz[i]] - + # Save warped mean of the training data (for MSLL) yw = nm.blr.warp.f(Y[tr, nz[i]], warp_param) - + # create arrays for evaluation Yhati = Yhat[ts, nz[i]] Yhati = Yhati[:, np.newaxis] @@ -535,25 +539,27 @@ def estimate(covfile, respfile, **kwargs): S2i = S2i[:, np.newaxis] # evaluate and save results - mf = evaluate(Ytest[:, np.newaxis], Yhati, S2=S2i, - mY=np.mean(yw), sY=np.std(yw), - nlZ=nm.neg_log_lik, nm=nm, Xz_tr=Xz_tr, - alg=alg, metrics = metrics) + mf = evaluate(Ytest[:, np.newaxis], Yhati, S2=S2i, + mY=np.mean(yw), sY=np.std(yw), + nlZ=nm.neg_log_lik, nm=nm, Xz_tr=Xz_tr, + alg=alg, metrics=metrics) for k in metrics: results_folds[k][nz[i]][fold] = mf[k] else: - Ytest = Y[ts, nz[i]] - - if alg=='hbr': + Ytest = Y[ts, nz[i]] + + if alg == 'hbr': if outscaler in ['standardize', 'minmax', 'robminmax']: - Ytestz = Y_scaler.transform(Ytest.reshape(-1,1), index=i) + Ytestz = Y_scaler.transform( + Ytest.reshape(-1, 1), index=i) else: - Ytestz = Ytest.reshape(-1,1) - Z[ts, nz[i]] = nm.get_mcmc_zscores(Xz_ts, Ytestz, **kwargs) + Ytestz = Ytest.reshape(-1, 1) + Z[ts, nz[i]] = nm.get_mcmc_zscores( + Xz_ts, Ytestz, **kwargs) else: Z[ts, nz[i]] = (Ytest - Yhat[ts, nz[i]]) / \ - np.sqrt(S2[ts, nz[i]]) - + np.sqrt(S2[ts, nz[i]]) + except Exception as e: exc_type, exc_obj, exc_tb = sys.exc_info() fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] @@ -572,52 +578,51 @@ def estimate(covfile, respfile, **kwargs): if testresp is not None: Z[ts, nz[i]] = float('nan') - if savemodel: print('Saving model meta-data...') - v = get_package_versions() + v = get_package_versions() with open('Models/meta_data.md', 'wb') as file: - pickle.dump({'valid_voxels':nz, 'fold_num':cvfolds, - 'mean_resp':mean_resp, 'std_resp':std_resp, - 'scaler_cov':scaler_cov, 'scaler_resp':scaler_resp, - 'regressor':alg, 'inscaler':inscaler, - 'outscaler':outscaler, 'versions':v}, - file, protocol=PICKLE_PROTOCOL) + pickle.dump({'valid_voxels': nz, 'fold_num': cvfolds, + 'mean_resp': mean_resp, 'std_resp': std_resp, + 'scaler_cov': scaler_cov, 'scaler_resp': scaler_resp, + 'regressor': alg, 'inscaler': inscaler, + 'outscaler': outscaler, 'versions': v}, + file, protocol=PICKLE_PROTOCOL) # compute performance metrics if (run_cv or testresp is not None): print("Evaluating the model ...") if warp is None: - results = evaluate(Y[testids, :], Yhat[testids, :], - S2=S2[testids, :], mY=mean_resp[0], + results = evaluate(Y[testids, :], Yhat[testids, :], + S2=S2[testids, :], mY=mean_resp[0], sY=std_resp[0], nlZ=nlZ, nm=nm, Xz_tr=Xz_tr, alg=alg, - metrics = metrics) + metrics=metrics) else: # for warped data we just aggregate across folds results = dict() for m in ['Rho', 'RMSE', 'SMSE', 'EXPV', 'MSLL']: results[m] = np.mean(results_folds[m], axis=1) results['NLL'] = results_folds['NLL'] - results['BIC'] = results_folds['BIC'] - + results['BIC'] = results_folds['BIC'] + # Set writing options if saveoutput: if (run_cv or testresp is not None): - save_results(respfile, Yhat[testids, :], S2[testids, :], maskvol, - Z=Z[testids, :], results=results, + save_results(respfile, Yhat[testids, :], S2[testids, :], maskvol, + Z=Z[testids, :], results=results, outputsuffix=outputsuffix) - + else: save_results(respfile, Yhat[testids, :], S2[testids, :], maskvol, outputsuffix=outputsuffix) - + else: if (run_cv or testresp is not None): - output = (Yhat[testids, :], S2[testids, :], nm, Z[testids, :], + output = (Yhat[testids, :], S2[testids, :], nm, Z[testids, :], results) else: output = (Yhat[testids, :], S2[testids, :], nm) - + return output @@ -638,16 +643,16 @@ def fit(covfile, respfile, **kwargs): Returns: None """ - - # parse keyword arguments - maskfile = kwargs.pop('maskfile',None) - alg = kwargs.pop('alg','gpr') - savemodel = kwargs.pop('savemodel','True')=='True' - outputsuffix = kwargs.pop('outputsuffix','fit') + + # parse keyword arguments + maskfile = kwargs.pop('maskfile', None) + alg = kwargs.pop('alg', 'gpr') + savemodel = kwargs.pop('savemodel', 'True') == 'True' + outputsuffix = kwargs.pop('outputsuffix', 'fit') outputsuffix = "_" + outputsuffix.replace("_", "") - inscaler = kwargs.pop('inscaler','None') - outscaler = kwargs.pop('outscaler','None') - + inscaler = kwargs.pop('inscaler', 'None') + outscaler = kwargs.pop('outscaler', 'None') + if savemodel and not os.path.isdir('Models'): os.mkdir('Models') @@ -659,30 +664,30 @@ def fit(covfile, respfile, **kwargs): Y = Y[:, np.newaxis] if len(X.shape) == 1: X = X[:, np.newaxis] - + # find and remove bad variables from the response variables # note: the covariates are assumed to have already been checked nz = np.where(np.bitwise_and(np.isfinite(Y).any(axis=0), - np.var(Y, axis=0) != 0))[0] - + np.var(Y, axis=0) != 0))[0] + scaler_resp = [] scaler_cov = [] - mean_resp = [] # this is just for computing MSLL + mean_resp = [] # this is just for computing MSLL std_resp = [] # this is just for computing MSLL - + # standardize responses and covariates, ignoring invalid entries mY = np.mean(Y[:, nz], axis=0) sY = np.std(Y[:, nz], axis=0) mean_resp.append(mY) std_resp.append(sY) - + if inscaler in ['standardize', 'minmax', 'robminmax']: X_scaler = scaler(inscaler) Xz = X_scaler.fit_transform(X) scaler_cov.append(X_scaler) else: Xz = X - + if outscaler in ['standardize', 'minmax', 'robminmax']: Yz = np.zeros_like(Y) Y_scaler = scaler(outscaler) @@ -692,29 +697,29 @@ def fit(covfile, respfile, **kwargs): Yz = Y # estimate the models for all subjects - for i in range(0, len(nz)): + for i in range(0, len(nz)): print("Estimating model ", i+1, "of", len(nz)) nm = norm_init(Xz, Yz[:, nz[i]], alg=alg, **kwargs) - nm = nm.estimate(Xz, Yz[:, nz[i]], **kwargs) - + nm = nm.estimate(Xz, Yz[:, nz[i]], **kwargs) + if savemodel: - nm.save('Models/NM_' + str(0) + '_' + str(nz[i]) + outputsuffix + - '.pkl' ) + nm.save('Models/NM_' + str(0) + '_' + str(nz[i]) + outputsuffix + + '.pkl') if savemodel: print('Saving model meta-data...') - v = get_package_versions() + v = get_package_versions() with open('Models/meta_data.md', 'wb') as file: - pickle.dump({'valid_voxels':nz, - 'mean_resp':mean_resp, 'std_resp':std_resp, - 'scaler_cov':scaler_cov, 'scaler_resp':scaler_resp, - 'regressor':alg, 'inscaler':inscaler, - 'outscaler':outscaler, 'versions':v}, + pickle.dump({'valid_voxels': nz, + 'mean_resp': mean_resp, 'std_resp': std_resp, + 'scaler_cov': scaler_cov, 'scaler_resp': scaler_resp, + 'regressor': alg, 'inscaler': inscaler, + 'outscaler': outscaler, 'versions': v}, file, protocol=PICKLE_PROTOCOL) - + return nm - + def predict(covfile, respfile, maskfile=None, **kwargs): ''' Make predictions on the basis of a pre-estimated normative model @@ -748,8 +753,7 @@ def predict(covfile, respfile, maskfile=None, **kwargs): * Z - Z scores * Y - response variable (if return_y is True) ''' - - + model_path = kwargs.pop('model_path', 'Models') job_id = kwargs.pop('job_id', None) batch_size = kwargs.pop('batch_size', None) @@ -758,13 +762,13 @@ def predict(covfile, respfile, maskfile=None, **kwargs): inputsuffix = kwargs.pop('inputsuffix', 'estimate') inputsuffix = "_" + inputsuffix.replace("_", "") alg = kwargs.pop('alg') - fold = kwargs.pop('fold',0) + fold = kwargs.pop('fold', 0) models = kwargs.pop('models', None) return_y = kwargs.pop('return_y', False) - + if alg == 'gpr': - raise(ValueError, "gpr is not supported with predict()") - + raise (ValueError, "gpr is not supported with predict()") + if respfile is not None and not os.path.exists(respfile): print("Response file does not exist. Only returning predictions") respfile = None @@ -792,59 +796,59 @@ def predict(covfile, respfile, maskfile=None, **kwargs): batch_size = int(batch_size) job_id = int(job_id) - 1 - # load data print("Loading data ...") X = fileio.load(covfile) if len(X.shape) == 1: X = X[:, np.newaxis] - + sample_num = X.shape[0] if models is not None: feature_num = len(models) else: - feature_num = len(glob.glob(os.path.join(model_path, 'NM_'+ str(fold) + + feature_num = len(glob.glob(os.path.join(model_path, 'NM_' + str(fold) + '_*' + inputsuffix + '.pkl'))) models = range(feature_num) Yhat = np.zeros([sample_num, feature_num]) S2 = np.zeros([sample_num, feature_num]) Z = np.zeros([sample_num, feature_num]) - + if inscaler in ['standardize', 'minmax', 'robminmax']: Xz = scaler_cov[fold].transform(X) else: Xz = X - + # estimate the models for all variabels - #TODO Z-scores adaptation for SHASH HBR + # TODO Z-scores adaptation for SHASH HBR for i, m in enumerate(models): - print("Prediction by model ", i+1, "of", feature_num) + print("Prediction by model ", i+1, "of", feature_num) nm = norm_init(Xz) - nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + + nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + str(m) + inputsuffix + '.pkl')) - if (alg!='hbr' or nm.configs['transferred']==False): + if (alg != 'hbr' or nm.configs['transferred'] == False): yhat, s2 = nm.predict(Xz, **kwargs) else: - tsbefile = kwargs.get('tsbefile') + tsbefile = kwargs.get('tsbefile') batch_effects_test = fileio.load(tsbefile) yhat, s2 = nm.predict_on_new_sites(Xz, batch_effects_test) - - if outscaler == 'standardize': + + if outscaler == 'standardize': Yhat[:, i] = scaler_resp[fold].inverse_transform(yhat, index=i) S2[:, i] = s2.squeeze() * sY[fold][i]**2 elif outscaler in ['minmax', 'robminmax']: Yhat[:, i] = scaler_resp[fold].inverse_transform(yhat, index=i) - S2[:, i] = s2 * (scaler_resp[fold].max[i] - scaler_resp[fold].min[i])**2 + S2[:, i] = s2 * (scaler_resp[fold].max[i] - + scaler_resp[fold].min[i])**2 else: Yhat[:, i] = yhat.squeeze() S2[:, i] = s2.squeeze() if respfile is None: save_results(None, Yhat, S2, None, outputsuffix=outputsuffix) - + return (Yhat, S2) - + else: Y, maskvol = load_response_vars(respfile, maskfile) if models is not None and len(Y.shape) > 1: @@ -859,54 +863,54 @@ def predict(covfile, respfile, maskfile=None, **kwargs): sY = sY[fold][models] else: sY = sY[models] - + if len(Y.shape) == 1: Y = Y[:, np.newaxis] - - # warp the targets? + + # warp the targets? if alg == 'blr' and nm.blr.warp is not None: warp = True - Yw = np.zeros_like(Y) - for i,m in enumerate(models): + Yw = np.zeros_like(Y) + for i, m in enumerate(models): nm = norm_init(Xz) - nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + + nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + str(m) + inputsuffix + '.pkl')) - warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] - Yw[:,i] = nm.blr.warp.f(Y[:,i], warp_param) + warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] + Yw[:, i] = nm.blr.warp.f(Y[:, i], warp_param) Y = Yw else: warp = False - + Z = (Y - Yhat) / np.sqrt(S2) - + print("Evaluating the model ...") if meta_data and not warp: - + results = evaluate(Y, Yhat, S2=S2, mY=mY, sY=sY) - else: - results = evaluate(Y, Yhat, S2=S2, - metrics = ['Rho', 'RMSE', 'SMSE', 'EXPV']) - + else: + results = evaluate(Y, Yhat, S2=S2, + metrics=['Rho', 'RMSE', 'SMSE', 'EXPV']) + print("Evaluations Writing outputs ...") - + if return_y: save_results(respfile, Yhat, S2, maskvol, Z=Z, Y=Y, - outputsuffix=outputsuffix, results=results) + outputsuffix=outputsuffix, results=results) return (Yhat, S2, Z, Y) else: - save_results(respfile, Yhat, S2, maskvol, Z=Z, - outputsuffix=outputsuffix, results=results) + save_results(respfile, Yhat, S2, maskvol, Z=Z, + outputsuffix=outputsuffix, results=results) return (Yhat, S2, Z) - -def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, + +def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, **kwargs): ''' Transfer learning on the basis of a pre-estimated normative model by using the posterior distribution over the parameters as an informed prior for new data. currently only supported for HBR. - + Basic usage:: transfer(covfile, respfile [extra_arguments]) @@ -930,35 +934,35 @@ def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, * Z - Z scores ''' alg = kwargs.pop('alg').lower() - + if alg != 'hbr' and alg != 'blr': print('Model transfer function is only possible for HBR and BLR models.') return # testing should not be obligatory for HBR, # but should be for BLR (since it doesn't produce transfer models) - elif (not 'model_path' in list(kwargs.keys())) or \ - (not 'trbefile' in list(kwargs.keys())): - print(f'{kwargs=}') - print('InputError: Some general mandatory arguments are missing.') - return + elif (not 'model_path' in list(kwargs.keys())) or \ + (not 'trbefile' in list(kwargs.keys())): + print(f'{kwargs=}') + print('InputError: Some general mandatory arguments are missing.') + return # hbr has one additional mandatory arguments - elif alg =='hbr': + elif alg == 'hbr': if (not 'output_path' in list(kwargs.keys())): - print('InputError: Some mandatory arguments for hbr are missing.') - return - else: - output_path = kwargs.pop('output_path',None) + print('InputError: Some mandatory arguments for hbr are missing.') + return + else: + output_path = kwargs.pop('output_path', None) if not os.path.isdir(output_path): - os.mkdir(output_path) + os.mkdir(output_path) # for hbr, testing is not mandatory, for blr's predict/transfer it is. This will be an architectural choice. - #or (testresp==None) - elif alg =='blr': - if (testcov==None) or \ - (not 'tsbefile' in list(kwargs.keys())): - print('InputError: Some mandatory arguments for blr are missing.') - return - # general arguments + # or (testresp==None) + elif alg == 'blr': + if (testcov == None) or \ + (not 'tsbefile' in list(kwargs.keys())): + print('InputError: Some mandatory arguments for blr are missing.') + return + # general arguments log_path = kwargs.pop('log_path', None) model_path = kwargs.pop('model_path') outputsuffix = kwargs.pop('outputsuffix', 'transfer') @@ -969,17 +973,17 @@ def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, trbefile = kwargs.pop('trbefile', None) job_id = kwargs.pop('job_id', None) batch_size = kwargs.pop('batch_size', None) - fold = kwargs.pop('fold',0) - + fold = kwargs.pop('fold', 0) + # for PCNonline automated parallel jobs loop - count_jobsdone = kwargs.pop('count_jobsdone','False') + count_jobsdone = kwargs.pop('count_jobsdone', 'False') if type(count_jobsdone) is str: - count_jobsdone = count_jobsdone=='True' - + count_jobsdone = count_jobsdone == 'True' + if batch_size is not None: batch_size = int(batch_size) job_id = int(job_id) - 1 - + if not os.path.isdir(model_path): print('Models directory does not exist!') return @@ -997,7 +1001,7 @@ def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, inscaler = 'None' outscaler = 'None' meta_data = False - + # load adaptation data print("Loading data ...") X = fileio.load(covfile) @@ -1006,19 +1010,19 @@ def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, Y = Y[:, np.newaxis] if len(X.shape) == 1: X = X[:, np.newaxis] - + if inscaler in ['standardize', 'minmax', 'robminmax']: X = scaler_cov[0].transform(X) - + feature_num = Y.shape[1] mY = np.mean(Y, axis=0) - sY = np.std(Y, axis=0) - + sY = np.std(Y, axis=0) + if outscaler in ['standardize', 'minmax', 'robminmax']: Y = scaler_resp[0].transform(Y) - + batch_effects_train = fileio.load(trbefile) - + # load test data if testcov is not None: # we have a separate test dataset @@ -1028,60 +1032,60 @@ def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, ts_sample_num = Xte.shape[0] if inscaler in ['standardize', 'minmax', 'robminmax']: Xte = scaler_cov[0].transform(Xte) - + if testresp is not None: Yte, testmask = load_response_vars(testresp, maskfile) if len(Yte.shape) == 1: Yte = Yte[:, np.newaxis] else: Yte = np.zeros([ts_sample_num, feature_num]) - + if tsbefile is not None: batch_effects_test = fileio.load(tsbefile) else: - batch_effects_test = np.zeros([Xte.shape[0],2]) + batch_effects_test = np.zeros([Xte.shape[0], 2]) else: - ts_sample_num = 0 + ts_sample_num = 0 Yhat = np.zeros([ts_sample_num, feature_num]) S2 = np.zeros([ts_sample_num, feature_num]) Z = np.zeros([ts_sample_num, feature_num]) - + # estimate the models for all subjects for i in range(feature_num): - - if alg == 'hbr': + + if alg == 'hbr': print("Using HBR transform...") nm = norm_init(X) - if batch_size is not None: # when using normative_parallel + if batch_size is not None: # when using normative_parallel print("Transferring model ", job_id*batch_size+i) - nm = nm.load(os.path.join(model_path, 'NM_0_' + - str(job_id*batch_size+i) + inputsuffix + + nm = nm.load(os.path.join(model_path, 'NM_0_' + + str(job_id*batch_size+i) + inputsuffix + '.pkl')) else: print("Transferring model ", i+1, "of", feature_num) - nm = nm.load(os.path.join(model_path, 'NM_0_' + str(i) + + nm = nm.load(os.path.join(model_path, 'NM_0_' + str(i) + inputsuffix + '.pkl')) - - nm = nm.estimate_on_new_sites(X, Y[:,i], batch_effects_train) - if batch_size is not None: - nm.save(os.path.join(output_path, 'NM_0_' + - str(job_id*batch_size+i) + outputsuffix + '.pkl')) + + nm = nm.estimate_on_new_sites(X, Y[:, i], batch_effects_train) + if batch_size is not None: + nm.save(os.path.join(output_path, 'NM_0_' + + str(job_id*batch_size+i) + outputsuffix + '.pkl')) else: - nm.save(os.path.join(output_path, 'NM_0_' + - str(i) + outputsuffix + '.pkl')) - + nm.save(os.path.join(output_path, 'NM_0_' + + str(i) + outputsuffix + '.pkl')) + if testcov is not None: yhat, s2 = nm.predict_on_new_sites(Xte, batch_effects_test) - + # We basically use normative.predict script here. if alg == 'blr': print("Using BLR transform...") - print("Transferring model ", i+1, "of", feature_num) + print("Transferring model ", i+1, "of", feature_num) nm = norm_init(X) - nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + + nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + str(i) + inputsuffix + '.pkl')) - + # translate the syntax to what blr understands # first strip existing blr keyword arguments to avoid redundancy adapt_cov = kwargs.pop('adaptcovfile', None) @@ -1089,79 +1093,79 @@ def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, adapt_vg = kwargs.pop('adaptvargroupfile', None) test_vg = kwargs.pop('testvargroupfile', None) if adapt_cov is not None or adapt_res is not None \ - or adapt_vg is not None or test_vg is not None: - print("Warning: redundant batch effect parameterisation. Using HBR syntax") - + or adapt_vg is not None or test_vg is not None: + print( + "Warning: redundant batch effect parameterisation. Using HBR syntax") + yhat, s2 = nm.predict(Xte, X, Y[:, i], - adaptcov = X, - adaptresp = Y[:, i], - adaptvargroup = batch_effects_train, - testvargroup = batch_effects_test, + adaptcov=X, + adaptresp=Y[:, i], + adaptvargroup=batch_effects_train, + testvargroup=batch_effects_test, **kwargs) - + if testcov is not None: - if outscaler == 'standardize': - Yhat[:, i] = scaler_resp[0].inverse_transform(yhat.squeeze(), index=i) + if outscaler == 'standardize': + Yhat[:, i] = scaler_resp[0].inverse_transform( + yhat.squeeze(), index=i) S2[:, i] = s2.squeeze() * sY[i]**2 elif outscaler in ['minmax', 'robminmax']: Yhat[:, i] = scaler_resp[0].inverse_transform(yhat, index=i) - S2[:, i] = s2 * (scaler_resp[0].max[i] - scaler_resp[0].min[i])**2 + S2[:, i] = s2 * (scaler_resp[0].max[i] - + scaler_resp[0].min[i])**2 else: Yhat[:, i] = yhat.squeeze() S2[:, i] = s2.squeeze() - - - + if testresp is None: save_results(respfile, Yhat, S2, maskvol, outputsuffix=outputsuffix) return (Yhat, S2) else: - # warp the targets? + # warp the targets? if alg == 'blr' and nm.blr.warp is not None: warp = True - Yw = np.zeros_like(Yte) + Yw = np.zeros_like(Yte) for i in range(feature_num): nm = norm_init(Xte) - nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + + nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + str(i) + inputsuffix + '.pkl')) - warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] - Yw[:,i] = nm.blr.warp.f(Yte[:,i], warp_param) - Yte = Yw; + warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] + Yw[:, i] = nm.blr.warp.f(Yte[:, i], warp_param) + Yte = Yw else: warp = False - - #TODO Z-scores adaptation for SHASH HBR + + # TODO Z-scores adaptation for SHASH HBR Z = (Yte - Yhat) / np.sqrt(S2) - + print("Evaluating the model ...") - if meta_data and not warp: + if meta_data and not warp: results = evaluate(Yte, Yhat, S2=S2, mY=mY, sY=sY) - else: - results = evaluate(Yte, Yhat, S2=S2, - metrics = ['Rho', 'RMSE', 'SMSE', 'EXPV']) - + else: + results = evaluate(Yte, Yhat, S2=S2, + metrics=['Rho', 'RMSE', 'SMSE', 'EXPV']) + save_results(respfile, Yhat, S2, maskvol, Z=Z, results=results, outputsuffix=outputsuffix) - + # Creates a file for every job succesfully completed (for tracking failed jobs). - if count_jobsdone==True: + if count_jobsdone == True: done_path = os.path.join(log_path, str(job_id)+".jobsdone") Path(done_path).touch() - + return (Yhat, S2, Z) - + # Creates a file for every job succesfully completed (for tracking failed jobs). - if count_jobsdone==True: + if count_jobsdone == True: done_path = os.path.join(log_path, str(job_id)+".jobsdone") Path(done_path).touch() def extend(covfile, respfile, maskfile=None, **kwargs): - ''' This function extends an existing HBR model with data from new sites/scanners. - + Basic usage:: extend(covfile, respfile [extra_arguments]) @@ -1184,23 +1188,23 @@ def extend(covfile, respfile, maskfile=None, **kwargs): All outputs are written to disk in the same format as the input. - + ''' - + alg = kwargs.pop('alg') if alg != 'hbr': print('Model extention is only possible for HBR models.') return elif (not 'model_path' in list(kwargs.keys())) or \ (not 'output_path' in list(kwargs.keys())) or \ - (not 'trbefile' in list(kwargs.keys())): - print('InputError: Some mandatory arguments are missing.') - return + (not 'trbefile' in list(kwargs.keys())): + print('InputError: Some mandatory arguments are missing.') + return else: model_path = kwargs.pop('model_path') output_path = kwargs.pop('output_path') trbefile = kwargs.pop('trbefile') - + outputsuffix = kwargs.pop('outputsuffix', 'extend') outputsuffix = "_" + outputsuffix.replace("_", "") inputsuffix = kwargs.pop('inputsuffix', 'estimate') @@ -1212,7 +1216,7 @@ def extend(covfile, respfile, maskfile=None, **kwargs): if batch_size is not None: batch_size = int(batch_size) job_id = int(job_id) - 1 - + if not os.path.isdir(model_path): print('Models directory does not exist!') return @@ -1220,60 +1224,58 @@ def extend(covfile, respfile, maskfile=None, **kwargs): if os.path.exists(os.path.join(model_path, 'meta_data.md')): with open(os.path.join(model_path, 'meta_data.md'), 'rb') as file: meta_data = pickle.load(file) - if (meta_data['inscaler'] != 'None' or - meta_data['outscaler'] != 'None'): + if (meta_data['inscaler'] != 'None' or + meta_data['outscaler'] != 'None'): print('Models extention on scaled data is not possible!') return - + if not os.path.isdir(output_path): os.mkdir(output_path) - + # load data print("Loading data ...") - X = fileio.load(covfile) + X = fileio.load(covfile) Y, maskvol = load_response_vars(respfile, maskfile) batch_effects_train = fileio.load(trbefile) - + if len(Y.shape) == 1: Y = Y[:, np.newaxis] if len(X.shape) == 1: X = X[:, np.newaxis] feature_num = Y.shape[1] - + # estimate the models for all subjects for i in range(feature_num): - + nm = norm_init(X) - if batch_size is not None: # when using nirmative_parallel + if batch_size is not None: # when using nirmative_parallel print("Extending model ", job_id*batch_size+i) - nm = nm.load(os.path.join(model_path, 'NM_0_' + - str(job_id*batch_size+i) + inputsuffix + + nm = nm.load(os.path.join(model_path, 'NM_0_' + + str(job_id*batch_size+i) + inputsuffix + '.pkl')) else: print("Extending model ", i+1, "of", feature_num) - nm = nm.load(os.path.join(model_path, 'NM_0_' + str(i) + - inputsuffix +'.pkl')) - - nm = nm.extend(X, Y[:,i:i+1], batch_effects_train, - samples=generation_factor, + nm = nm.load(os.path.join(model_path, 'NM_0_' + str(i) + + inputsuffix + '.pkl')) + + nm = nm.extend(X, Y[:, i:i+1], batch_effects_train, + samples=generation_factor, informative_prior=informative_prior) - - if batch_size is not None: - nm.save(os.path.join(output_path, 'NM_0_' + - str(job_id*batch_size+i) + outputsuffix + '.pkl')) - nm.save(os.path.join('Models', 'NM_0_' + - str(i) + outputsuffix + '.pkl')) + + if batch_size is not None: + nm.save(os.path.join(output_path, 'NM_0_' + + str(job_id*batch_size+i) + outputsuffix + '.pkl')) + nm.save(os.path.join('Models', 'NM_0_' + + str(i) + outputsuffix + '.pkl')) else: - nm.save(os.path.join(output_path, 'NM_0_' + - str(i) + outputsuffix + '.pkl')) - - + nm.save(os.path.join(output_path, 'NM_0_' + + str(i) + outputsuffix + '.pkl')) + def tune(covfile, respfile, maskfile=None, **kwargs): - ''' This function tunes an existing HBR model with real data. - + Basic usage:: tune(covfile, respfile [extra_arguments]) @@ -1290,30 +1292,30 @@ def tune(covfile, respfile, maskfile=None, **kwargs): :param output_path: the path for saving the the extended model :param informative_prior: use initial model prior or learn from scracth (default is False). :param generation_factor: see below - - + + generation factor refers to the number of samples generated for each combination of covariates and batch effects. Default is 10. All outputs are written to disk in the same format as the input. - + ''' - + alg = kwargs.pop('alg') if alg != 'hbr': print('Model extention is only possible for HBR models.') return elif (not 'model_path' in list(kwargs.keys())) or \ (not 'output_path' in list(kwargs.keys())) or \ - (not 'trbefile' in list(kwargs.keys())): - print('InputError: Some mandatory arguments are missing.') - return + (not 'trbefile' in list(kwargs.keys())): + print('InputError: Some mandatory arguments are missing.') + return else: model_path = kwargs.pop('model_path') output_path = kwargs.pop('output_path') trbefile = kwargs.pop('trbefile') - + outputsuffix = kwargs.pop('outputsuffix', 'tuned') outputsuffix = "_" + outputsuffix.replace("_", "") inputsuffix = kwargs.pop('inputsuffix', 'estimate') @@ -1325,7 +1327,7 @@ def tune(covfile, respfile, maskfile=None, **kwargs): if batch_size is not None: batch_size = int(batch_size) job_id = int(job_id) - 1 - + if not os.path.isdir(model_path): print('Models directory does not exist!') return @@ -1333,59 +1335,58 @@ def tune(covfile, respfile, maskfile=None, **kwargs): if os.path.exists(os.path.join(model_path, 'meta_data.md')): with open(os.path.join(model_path, 'meta_data.md'), 'rb') as file: meta_data = pickle.load(file) - if (meta_data['inscaler'] != 'None' or - meta_data['outscaler'] != 'None'): + if (meta_data['inscaler'] != 'None' or + meta_data['outscaler'] != 'None'): print('Models extention on scaled data is not possible!') return - + if not os.path.isdir(output_path): os.mkdir(output_path) - + # load data print("Loading data ...") - X = fileio.load(covfile) + X = fileio.load(covfile) Y, maskvol = load_response_vars(respfile, maskfile) batch_effects_train = fileio.load(trbefile) - + if len(Y.shape) == 1: Y = Y[:, np.newaxis] if len(X.shape) == 1: X = X[:, np.newaxis] feature_num = Y.shape[1] - + # estimate the models for all subjects for i in range(feature_num): - + nm = norm_init(X) - if batch_size is not None: # when using nirmative_parallel + if batch_size is not None: # when using nirmative_parallel print("Tuning model ", job_id*batch_size+i) - nm = nm.load(os.path.join(model_path, 'NM_0_' + - str(job_id*batch_size+i) + inputsuffix + + nm = nm.load(os.path.join(model_path, 'NM_0_' + + str(job_id*batch_size+i) + inputsuffix + '.pkl')) else: print("Tuning model ", i+1, "of", feature_num) - nm = nm.load(os.path.join(model_path, 'NM_0_' + str(i) + - inputsuffix +'.pkl')) - - nm = nm.tune(X, Y[:,i:i+1], batch_effects_train, - samples=generation_factor, - informative_prior=informative_prior) - - if batch_size is not None: - nm.save(os.path.join(output_path, 'NM_0_' + - str(job_id*batch_size+i) + outputsuffix + '.pkl')) - nm.save(os.path.join('Models', 'NM_0_' + - str(i) + outputsuffix + '.pkl')) + nm = nm.load(os.path.join(model_path, 'NM_0_' + str(i) + + inputsuffix + '.pkl')) + + nm = nm.tune(X, Y[:, i:i+1], batch_effects_train, + samples=generation_factor, + informative_prior=informative_prior) + + if batch_size is not None: + nm.save(os.path.join(output_path, 'NM_0_' + + str(job_id*batch_size+i) + outputsuffix + '.pkl')) + nm.save(os.path.join('Models', 'NM_0_' + + str(i) + outputsuffix + '.pkl')) else: - nm.save(os.path.join(output_path, 'NM_0_' + - str(i) + outputsuffix + '.pkl')) + nm.save(os.path.join(output_path, 'NM_0_' + + str(i) + outputsuffix + '.pkl')) def merge(covfile=None, respfile=None, **kwargs): - ''' This function extends an existing HBR model with data from new sites/scanners. - + Basic usage:: merge(model_path1, model_path2 [extra_arguments]) @@ -1406,23 +1407,23 @@ def merge(covfile=None, respfile=None, **kwargs): All outputs are written to disk in the same format as the input. - + ''' - + alg = kwargs.pop('alg') if alg != 'hbr': print('Merging models is only possible for HBR models.') return elif (not 'model_path1' in list(kwargs.keys())) or \ (not 'model_path2' in list(kwargs.keys())) or \ - (not 'output_path' in list(kwargs.keys())): - print('InputError: Some mandatory arguments are missing.') - return + (not 'output_path' in list(kwargs.keys())): + print('InputError: Some mandatory arguments are missing.') + return else: model_path1 = kwargs.pop('model_path1') model_path2 = kwargs.pop('model_path2') output_path = kwargs.pop('output_path') - + outputsuffix = kwargs.pop('outputsuffix', 'merge') outputsuffix = "_" + outputsuffix.replace("_", "") inputsuffix = kwargs.pop('inputsuffix', 'estimate') @@ -1433,7 +1434,7 @@ def merge(covfile=None, respfile=None, **kwargs): if batch_size is not None: batch_size = int(batch_size) job_id = int(job_id) - 1 - + if (not os.path.isdir(model_path1)) or (not os.path.isdir(model_path2)): print('Models directory does not exist!') return @@ -1450,41 +1451,40 @@ def merge(covfile=None, respfile=None, **kwargs): feature_num = meta_data1['valid_voxels'].shape[0] else: feature_num = batch_size - - + if not os.path.isdir(output_path): os.mkdir(output_path) - + # mergeing the models for i in range(feature_num): - - nm1 = norm_init(np.random.rand(100,10)) - nm2 = norm_init(np.random.rand(100,10)) - if batch_size is not None: # when using nirmative_parallel + + nm1 = norm_init(np.random.rand(100, 10)) + nm2 = norm_init(np.random.rand(100, 10)) + if batch_size is not None: # when using nirmative_parallel print("Merging model ", job_id*batch_size+i) - nm1 = nm1.load(os.path.join(model_path1, 'NM_0_' + - str(job_id*batch_size+i) + inputsuffix + - '.pkl')) - nm2 = nm2.load(os.path.join(model_path2, 'NM_0_' + - str(job_id*batch_size+i) + inputsuffix + - '.pkl')) + nm1 = nm1.load(os.path.join(model_path1, 'NM_0_' + + str(job_id*batch_size+i) + inputsuffix + + '.pkl')) + nm2 = nm2.load(os.path.join(model_path2, 'NM_0_' + + str(job_id*batch_size+i) + inputsuffix + + '.pkl')) else: print("Merging model ", i+1, "of", feature_num) - nm1 = nm1.load(os.path.join(model_path1, 'NM_0_' + str(i) + - inputsuffix +'.pkl')) - nm2 = nm1.load(os.path.join(model_path2, 'NM_0_' + str(i) + - inputsuffix +'.pkl')) - + nm1 = nm1.load(os.path.join(model_path1, 'NM_0_' + str(i) + + inputsuffix + '.pkl')) + nm2 = nm1.load(os.path.join(model_path2, 'NM_0_' + str(i) + + inputsuffix + '.pkl')) + nm_merged = nm1.merge(nm2, samples=generation_factor) - - if batch_size is not None: - nm_merged.save(os.path.join(output_path, 'NM_0_' + - str(job_id*batch_size+i) + outputsuffix + '.pkl')) - nm_merged.save(os.path.join('Models', 'NM_0_' + - str(i) + outputsuffix + '.pkl')) + + if batch_size is not None: + nm_merged.save(os.path.join(output_path, 'NM_0_' + + str(job_id*batch_size+i) + outputsuffix + '.pkl')) + nm_merged.save(os.path.join('Models', 'NM_0_' + + str(i) + outputsuffix + '.pkl')) else: - nm_merged.save(os.path.join(output_path, 'NM_0_' + - str(i) + outputsuffix + '.pkl')) + nm_merged.save(os.path.join(output_path, 'NM_0_' + + str(i) + outputsuffix + '.pkl')) def main(*args): @@ -1493,11 +1493,12 @@ def main(*args): np.seterr(invalid='ignore') - rfile, mfile, cfile, cv, tcfile, trfile, func, alg, cfg, kw = get_args(args) - + rfile, mfile, cfile, cv, tcfile, trfile, func, alg, cfg, kw = get_args( + args) + # collect required arguments pos_args = ['cfile', 'rfile'] - + # collect basic keyword arguments controlling model estimation kw_args = ['maskfile=mfile', 'cvfolds=cv', @@ -1505,7 +1506,7 @@ def main(*args): 'testresp=trfile', 'alg=alg', 'configparam=cfg'] - + # add additional keyword arguments for k in kw: kw_args.append(k + '=' + "'" + kw[k] + "'") @@ -1514,6 +1515,7 @@ def main(*args): # Executing the target function exec(func + '(' + all_args + ')') + # For running from the command line: if __name__ == "__main__": - main(sys.argv[1:]) \ No newline at end of file + main(sys.argv[1:]) diff --git a/pcntoolkit/normative_NP.py b/pcntoolkit/normative_NP.py index 9392f3c6..a2c1fac1 100644 --- a/pcntoolkit/normative_NP.py +++ b/pcntoolkit/normative_NP.py @@ -8,10 +8,10 @@ """ # ------------------------------------------------------------------------------ # Usage: -# python normative_NP.py -r /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/responses.nii.gz -# -c /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/covariates.pickle -# --tr /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/test_responses.nii.gz -# --tc /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/test_covariates.pickle +# python normative_NP.py -r /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/responses.nii.gz +# -c /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/covariates.pickle +# --tr /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/test_responses.nii.gz +# --tc /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/test_covariates.pickle # -o /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/Results # # @@ -47,6 +47,7 @@ del path import configs + def get_args(*args): """ Parses command-line arguments for the Neural Processes (NP) for Deep Normative Modeling script. @@ -70,19 +71,21 @@ def get_args(*args): """ ############################ Parsing inputs ############################### - - parser = argparse.ArgumentParser(description='Neural Processes (NP) for Deep Normative Modeling') - parser.add_argument("-r", help="Training response nifti file address", + + parser = argparse.ArgumentParser( + description='Neural Processes (NP) for Deep Normative Modeling') + parser.add_argument("-r", help="Training response nifti file address", required=True, dest="respfile", default=None) - parser.add_argument("-c", help="Training covariates pickle file address", + parser.add_argument("-c", help="Training covariates pickle file address", required=True, dest="covfile", default=None) - parser.add_argument("--tc", help="Test covariates pickle file address", + parser.add_argument("--tc", help="Test covariates pickle file address", required=True, dest="testcovfile", default=None) - parser.add_argument("--tr", help="Test response nifti file address", + parser.add_argument("--tr", help="Test response nifti file address", dest="testrespfile", default=None) - parser.add_argument("--mask", help="Mask nifti file address", + parser.add_argument("--mask", help="Mask nifti file address", dest="mask", default=None) - parser.add_argument("-o", help="Output directory address", dest="outdir", default=None) + parser.add_argument("-o", help="Output directory address", + dest="outdir", default=None) parser.add_argument('-m', type=int, default=10, dest='m', help='number of fixed-effect estimations') parser.add_argument('--batchnum', type=int, default=10, dest='batchnum', @@ -97,18 +100,19 @@ def get_args(*args): args = parser.parse_args() if (args.respfile == None or args.covfile == None or args.testcovfile == None): - raise(ValueError, "Training response nifti file, Training covariates pickle file, and \ + raise (ValueError, "Training response nifti file, Training covariates pickle file, and \ Test covariates pickle file must be specified.") if (args.outdir == None): args.outdir = os.getcwd() - - cuda = args.device=='cuda' and torch.cuda.is_available() + + cuda = args.device == 'cuda' and torch.cuda.is_available() args.device = torch.device("cuda" if cuda else "cpu") args.kwargs = {'num_workers': 1, 'pin_memory': True} if cuda else {} - args.type= 'MT' + args.type = 'MT' return args - + + def estimate(args): """ Estimates the fixed-effects for the Neural Processes (NP) for Deep Normative Modeling script. @@ -134,80 +138,91 @@ def estimate(args): torch.set_default_dtype(torch.float32) args.type = 'MT' print('Loading the input Data ...') - responses = fileio.load_nifti(args.respfile, vol=True).transpose([3,0,1,2]) + responses = fileio.load_nifti( + args.respfile, vol=True).transpose([3, 0, 1, 2]) response_shape = responses.shape with open(args.covfile, 'rb') as handle: - covariates = pickle.load(handle)['covariates'] + covariates = pickle.load(handle)['covariates'] with open(args.testcovfile, 'rb') as handle: test_covariates = pickle.load(handle)['test_covariates'] if args.mask is not None: mask = fileio.load_nifti(args.mask, vol=True) mask = fileio.create_mask(mask, mask=None) - else: - mask = fileio.create_mask(responses[0,:,:,:], mask=None) + else: + mask = fileio.create_mask(responses[0, :, :, :], mask=None) if args.testrespfile is not None: - test_responses = fileio.load_nifti(args.testrespfile, vol=True).transpose([3,0,1,2]) + test_responses = fileio.load_nifti( + args.testrespfile, vol=True).transpose([3, 0, 1, 2]) test_responses_shape = test_responses.shape - + print('Normalizing the input Data ...') covariates_scaler = StandardScaler() covariates = covariates_scaler.fit_transform(covariates) test_covariates = covariates_scaler.transform(test_covariates) response_scaler = MinMaxScaler() - responses = unravel_2D(response_scaler.fit_transform(ravel_2D(responses)), response_shape) + responses = unravel_2D(response_scaler.fit_transform( + ravel_2D(responses)), response_shape) if args.testrespfile is not None: - test_responses = unravel_2D(response_scaler.transform(ravel_2D(test_responses)), test_responses_shape) + test_responses = unravel_2D(response_scaler.transform( + ravel_2D(test_responses)), test_responses_shape) test_responses = np.expand_dims(test_responses, axis=1) - + factor = args.m - - x_context = np.zeros([covariates.shape[0], factor, covariates.shape[1]], dtype=np.float32) - y_context = np.zeros([responses.shape[0], factor, responses.shape[1], - responses.shape[2], responses.shape[3]], dtype=np.float32) - x_all = np.zeros([covariates.shape[0], factor, covariates.shape[1]], dtype=np.float32) - x_context_test = np.zeros([test_covariates.shape[0], factor, test_covariates.shape[1]], dtype=np.float32) - y_context_test = np.zeros([test_covariates.shape[0], factor, responses.shape[1], + + x_context = np.zeros([covariates.shape[0], factor, + covariates.shape[1]], dtype=np.float32) + y_context = np.zeros([responses.shape[0], factor, responses.shape[1], responses.shape[2], responses.shape[3]], dtype=np.float32) - + x_all = np.zeros([covariates.shape[0], factor, + covariates.shape[1]], dtype=np.float32) + x_context_test = np.zeros( + [test_covariates.shape[0], factor, test_covariates.shape[1]], dtype=np.float32) + y_context_test = np.zeros([test_covariates.shape[0], factor, responses.shape[1], + responses.shape[2], responses.shape[3]], dtype=np.float32) + print('Estimating the fixed-effects ...') for i in range(factor): - x_context[:,i,:] = covariates[:,:] - x_context_test[:,i,:] = test_covariates[:,:] - idx = np.random.randint(0,covariates.shape[0], covariates.shape[0]) - if args.estimator=='ST': + x_context[:, i, :] = covariates[:, :] + x_context_test[:, i, :] = test_covariates[:, :] + idx = np.random.randint(0, covariates.shape[0], covariates.shape[0]) + if args.estimator == 'ST': for j in range(responses.shape[1]): for k in range(responses.shape[2]): for l in range(responses.shape[3]): reg = LinearRegression() - reg.fit(x_context[idx,i,:], responses[idx,j,k,l]) - y_context[:,i,j,k,l] = reg.predict(x_context[:,i,:]) - y_context_test[:,i,j,k,l] = reg.predict(x_context_test[:,i,:]) - elif args.estimator=='MT': + reg.fit(x_context[idx, i, :], responses[idx, j, k, l]) + y_context[:, i, j, k, l] = reg.predict( + x_context[:, i, :]) + y_context_test[:, i, j, k, l] = reg.predict( + x_context_test[:, i, :]) + elif args.estimator == 'MT': reg = MultiTaskLasso(alpha=0.1) - reg.fit(x_context[idx,i,:], np.reshape(responses[idx,:,:,:], [covariates.shape[0],np.prod(responses.shape[1:])])) - y_context[:,i,:,:,:] = np.reshape(reg.predict(x_context[:,i,:]), - [x_context.shape[0],responses.shape[1],responses.shape[2],responses.shape[3]]) - y_context_test[:,i,:,:,:] = np.reshape(reg.predict(x_context_test[:,i,:]), - [x_context_test.shape[0],responses.shape[1],responses.shape[2],responses.shape[3]]) - print('Fixed-effect %d of %d is computed!' %(i+1, factor)) - + reg.fit(x_context[idx, i, :], np.reshape(responses[idx, :, :, :], [ + covariates.shape[0], np.prod(responses.shape[1:])])) + y_context[:, i, :, :, :] = np.reshape(reg.predict(x_context[:, i, :]), + [x_context.shape[0], responses.shape[1], responses.shape[2], responses.shape[3]]) + y_context_test[:, i, :, :, :] = np.reshape(reg.predict(x_context_test[:, i, :]), + [x_context_test.shape[0], responses.shape[1], responses.shape[2], responses.shape[3]]) + print('Fixed-effect %d of %d is computed!' % (i+1, factor)) + x_all = x_context responses = np.expand_dims(responses, axis=1).repeat(factor, axis=1) - - ################################## TRAINING ################################# - + + ################################## TRAINING ################################# + encoder = Encoder(x_context, y_context, args).to(args.device) args.cnn_feature_num = encoder.cnn_feature_num decoder = Decoder(x_context, y_context, args).to(args.device) model = NP(encoder, decoder, args).to(args.device) - + print('Estimating the Random-effect ...') k = 1 - epochs = [int(args.epochs/4),int(args.epochs/2),int(args.epochs/5),int(args.epochs-args.epochs/4-args.epochs/2-args.epochs/5)] + epochs = [int(args.epochs/4), int(args.epochs/2), int(args.epochs/5), + int(args.epochs-args.epochs/4-args.epochs/2-args.epochs/5)] mini_batch_num = args.batchnum batch_size = int(x_context.shape[0]/mini_batch_num) model.train() - for e in range(len(epochs)): + for e in range(len(epochs)): optimizer = optim.Adam(model.parameters(), lr=10**(-e-2)) for j in range(epochs[e]): train_loss = 0 @@ -215,85 +230,95 @@ def estimate(args): for i in range(mini_batch_num): optimizer.zero_grad() idx = rand_idx[i*batch_size:(i+1)*batch_size] - y_hat, z_all, z_context, dummy = model(torch.tensor(x_context[idx,:,:], device = args.device), - torch.tensor(y_context[idx,:,:,:,:], device = args.device), - torch.tensor(x_all[idx,:,:], device = args.device), - torch.tensor(responses[idx,:,:,:,:], device = args.device)) - loss = np_loss(y_hat, torch.tensor(responses[idx,:,:,:,:], device = args.device), z_all, z_context) + y_hat, z_all, z_context, dummy = model(torch.tensor(x_context[idx, :, :], device=args.device), + torch.tensor( + y_context[idx, :, :, :, :], device=args.device), + torch.tensor( + x_all[idx, :, :], device=args.device), + torch.tensor(responses[idx, :, :, :, :], device=args.device)) + loss = np_loss(y_hat, torch.tensor( + responses[idx, :, :, :, :], device=args.device), z_all, z_context) loss.backward() train_loss += loss.item() optimizer.step() - print('Epoch: %d, Loss:%f, Average Loss:%f' %(k, train_loss, train_loss/responses.shape[0])) + print('Epoch: %d, Loss:%f, Average Loss:%f' % + (k, train_loss, train_loss/responses.shape[0])) k += 1 - + ################################## Evaluation ################################# - + print('Predicting on Test Data ...') model.eval() model.apply(apply_dropout_test) with torch.no_grad(): - y_hat, z_all, z_context, y_sigma = model(torch.tensor(x_context_test, device = args.device), - torch.tensor(y_context_test, device = args.device), n = 15) - if args.testrespfile is not None: - test_loss = np_loss(y_hat[0:test_responses_shape[0],:], - torch.tensor(test_responses, device = args.device), + y_hat, z_all, z_context, y_sigma = model(torch.tensor(x_context_test, device=args.device), + torch.tensor(y_context_test, device=args.device), n=15) + if args.testrespfile is not None: + test_loss = np_loss(y_hat[0:test_responses_shape[0], :], + torch.tensor(test_responses, device=args.device), z_all, z_context).item() - print('Average Test Loss:%f' %(test_loss/test_responses_shape[0])) - - RMSE = np.sqrt(np.mean((test_responses - y_hat[0:test_responses_shape[0],:].cpu().numpy())**2, axis = 0)).squeeze() * mask + print('Average Test Loss:%f' % (test_loss/test_responses_shape[0])) + + RMSE = np.sqrt(np.mean( + (test_responses - y_hat[0:test_responses_shape[0], :].cpu().numpy())**2, axis=0)).squeeze() * mask SMSE = RMSE ** 2 / np.var(test_responses, axis=0).squeeze() - Rho, pRho = compute_pearsonr(test_responses.squeeze(), y_hat[0:test_responses_shape[0],:].cpu().numpy().squeeze()) - EXPV = explained_var(test_responses.squeeze(), y_hat[0:test_responses_shape[0],:].cpu().numpy().squeeze()) * mask - MSLL = compute_MSLL(test_responses.squeeze(), y_hat[0:test_responses_shape[0],:].cpu().numpy().squeeze(), - y_sigma[0:test_responses_shape[0],:].cpu().numpy().squeeze()**2, train_mean = test_responses.mean(0), - train_var = test_responses.var(0)).squeeze() * mask - - NPMs = (test_responses - y_hat[0:test_responses_shape[0],:].cpu().numpy()) / (y_sigma[0:test_responses_shape[0],:].cpu().numpy()) + Rho, pRho = compute_pearsonr(test_responses.squeeze( + ), y_hat[0:test_responses_shape[0], :].cpu().numpy().squeeze()) + EXPV = explained_var(test_responses.squeeze( + ), y_hat[0:test_responses_shape[0], :].cpu().numpy().squeeze()) * mask + MSLL = compute_MSLL(test_responses.squeeze(), y_hat[0:test_responses_shape[0], :].cpu().numpy().squeeze(), + y_sigma[0:test_responses_shape[0], :].cpu().numpy().squeeze()**2, train_mean=test_responses.mean(0), + train_var=test_responses.var(0)).squeeze() * mask + + NPMs = (test_responses - y_hat[0:test_responses_shape[0], :].cpu().numpy()) / ( + y_sigma[0:test_responses_shape[0], :].cpu().numpy()) NPMs = NPMs.squeeze() NPMs = NPMs * mask NPMs = np.nan_to_num(NPMs) - - temp=NPMs.reshape([NPMs.shape[0],NPMs.shape[1]*NPMs.shape[2]*NPMs.shape[3]]) + + temp = NPMs.reshape([NPMs.shape[0], NPMs.shape[1] + * NPMs.shape[2]*NPMs.shape[3]]) EVD_params = extreme_value_prob_fit(temp, 0.01) abnormal_probs = extreme_value_prob(EVD_params, temp, 0.01) - + ############################## SAVING RESULTS ################################# - print('Saving Results to: %s' %(args.outdir)) + print('Saving Results to: %s' % (args.outdir)) exfile = args.respfile y_hat = y_hat.squeeze().cpu().numpy() y_hat = response_scaler.inverse_transform(ravel_2D(y_hat)) - y_hat = y_hat[:,mask.flatten()] - fileio.save(y_hat.T, args.outdir + + y_hat = y_hat[:, mask.flatten()] + fileio.save(y_hat.T, args.outdir + '/yhat.nii.gz', example=exfile, mask=mask) ys2 = y_sigma.squeeze().cpu().numpy() - ys2 = ravel_2D(ys2) * (response_scaler.data_max_ - response_scaler.data_min_) + ys2 = ravel_2D(ys2) * (response_scaler.data_max_ - + response_scaler.data_min_) ys2 = ys2**2 - ys2 = ys2[:,mask.flatten()] - fileio.save(ys2.T, args.outdir + + ys2 = ys2[:, mask.flatten()] + fileio.save(ys2.T, args.outdir + '/ys2.nii.gz', example=exfile, mask=mask) - if args.testrespfile is not None: - NPMs = ravel_2D(NPMs)[:,mask.flatten()] - fileio.save(NPMs.T, args.outdir + + if args.testrespfile is not None: + NPMs = ravel_2D(NPMs)[:, mask.flatten()] + fileio.save(NPMs.T, args.outdir + '/Z.nii.gz', example=exfile, mask=mask) - fileio.save(Rho.flatten()[mask.flatten()], args.outdir + + fileio.save(Rho.flatten()[mask.flatten()], args.outdir + '/Rho.nii.gz', example=exfile, mask=mask) - fileio.save(pRho.flatten()[mask.flatten()], args.outdir + + fileio.save(pRho.flatten()[mask.flatten()], args.outdir + '/pRho.nii.gz', example=exfile, mask=mask) - fileio.save(RMSE.flatten()[mask.flatten()], args.outdir + + fileio.save(RMSE.flatten()[mask.flatten()], args.outdir + '/rmse.nii.gz', example=exfile, mask=mask) - fileio.save(SMSE.flatten()[mask.flatten()], args.outdir + + fileio.save(SMSE.flatten()[mask.flatten()], args.outdir + '/smse.nii.gz', example=exfile, mask=mask) - fileio.save(EXPV.flatten()[mask.flatten()], args.outdir + + fileio.save(EXPV.flatten()[mask.flatten()], args.outdir + '/expv.nii.gz', example=exfile, mask=mask) - fileio.save(MSLL.flatten()[mask.flatten()], args.outdir + + fileio.save(MSLL.flatten()[mask.flatten()], args.outdir + '/msll.nii.gz', example=exfile, mask=mask) - - with open(args.outdir +'model.pkl', 'wb') as handle: - pickle.dump({'model':model, 'covariates_scaler':covariates_scaler, - 'response_scaler': response_scaler, 'EVD_params':EVD_params, - 'abnormal_probs':abnormal_probs}, handle, protocol=configs.PICKLE_PROTOCOL) - + + with open(args.outdir + 'model.pkl', 'wb') as handle: + pickle.dump({'model': model, 'covariates_scaler': covariates_scaler, + 'response_scaler': response_scaler, 'EVD_params': EVD_params, + 'abnormal_probs': abnormal_probs}, handle, protocol=configs.PICKLE_PROTOCOL) + ############################################################################### print('DONE!') @@ -301,10 +326,11 @@ def estimate(args): def main(*args): """ Parse arguments and estimate model """ - + np.seterr(invalid='ignore') args = get_args(args) estimate(args) - + + if __name__ == "__main__": main(sys.argv[1:]) diff --git a/pcntoolkit/normative_model/__init__.py b/pcntoolkit/normative_model/__init__.py index 772a3653..7a6c3b08 100644 --- a/pcntoolkit/normative_model/__init__.py +++ b/pcntoolkit/normative_model/__init__.py @@ -3,4 +3,4 @@ from . import norm_blr from . import norm_rfa from . import norm_hbr -from . import norm_utils \ No newline at end of file +from . import norm_utils diff --git a/pcntoolkit/normative_model/norm_base.py b/pcntoolkit/normative_model/norm_base.py index 3e46ef93..7839e740 100644 --- a/pcntoolkit/normative_model/norm_base.py +++ b/pcntoolkit/normative_model/norm_base.py @@ -40,7 +40,7 @@ def predict(self, Xs, X, y): @abstractmethod def n_params(self): """ Report the number of parameters required by the model """ - + def save(self, save_path): try: with open(save_path, 'wb') as handle: @@ -49,7 +49,7 @@ def save(self, save_path): except Exception as err: print('Error:', err) raise - + def load(self, load_path): try: with open(load_path, 'rb') as handle: diff --git a/pcntoolkit/normative_model/norm_blr.py b/pcntoolkit/normative_model/norm_blr.py index b3083797..bb7e2949 100644 --- a/pcntoolkit/normative_model/norm_blr.py +++ b/pcntoolkit/normative_model/norm_blr.py @@ -12,7 +12,7 @@ from pcntoolkit.normative_model.norm_base import NormBase from pcntoolkit.dataio import fileio from pcntoolkit.util.utils import create_poly_basis, WarpBoxCox, \ - WarpAffine, WarpCompose, WarpSinArcsinh + WarpAffine, WarpCompose, WarpSinArcsinh except ImportError: pass @@ -25,12 +25,13 @@ from norm_base import NormBase from dataio import fileio from util.utils import create_poly_basis, WarpBoxCox, \ - WarpAffine, WarpCompose, WarpSinArcsinh + WarpAffine, WarpCompose, WarpSinArcsinh + class NormBLR(NormBase): """ Normative modelling based on Bayesian Linear Regression - """ - + """ + def __init__(self, **kwargs): """ Initialize the NormBLR object. @@ -52,32 +53,32 @@ def __init__(self, **kwargs): theta = kwargs.pop('theta', None) if isinstance(theta, str): theta = np.array(literal_eval(theta)) - self.optim_alg = kwargs.get('optimizer','powell') + self.optim_alg = kwargs.get('optimizer', 'powell') if X is None: - raise(ValueError, "Data matrix must be specified") + raise (ValueError, "Data matrix must be specified") if len(X.shape) == 1: self.D = 1 else: self.D = X.shape[1] - + # Parse model order if kwargs is None: model_order = 1 - elif 'configparam' in kwargs: # deprecated syntax + elif 'configparam' in kwargs: # deprecated syntax model_order = kwargs.pop('configparam') - elif 'model_order' in kwargs: + elif 'model_order' in kwargs: model_order = kwargs.pop('model_order') else: model_order = 1 - + # Force a default model order and check datatype if model_order is None: model_order = 1 if type(model_order) is not int: model_order = int(model_order) - + # configure heteroskedastic noise if 'varcovfile' in kwargs: var_cov_file = kwargs.get('varcovfile') @@ -103,9 +104,9 @@ def __init__(self, **kwargs): self.var_groups = None self.var_covariates = None n_beta = 1 - + # are we using ARD? - if 'use_ard' in kwargs: + if 'use_ard' in kwargs: self.use_ard = kwargs.pop('use_ard') else: self.use_ard = False @@ -113,7 +114,7 @@ def __init__(self, **kwargs): n_alpha = self.D * model_order else: n_alpha = 1 - + # Configure warped likelihood if 'warp' in kwargs: warp_str = kwargs.pop('warp') @@ -130,7 +131,7 @@ def __init__(self, **kwargs): self._n_params = n_alpha + n_beta + n_gamma self._model_order = model_order - + print("configuring BLR ( order", model_order, ")") if (theta is None) or (len(theta) != self._n_params): print("Using default hyperparameters") @@ -138,19 +139,19 @@ def __init__(self, **kwargs): else: self.theta0 = theta self.theta = self.theta0 - + # initialise the BLR object if the required parameters are present if (theta is not None) and (y is not None): Phi = create_poly_basis(X, self._model_order) - self.blr = BLR(theta=theta, X=Phi, y=y, + self.blr = BLR(theta=theta, X=Phi, y=y, warp=self.warp, **kwargs) else: - self.blr = BLR(**kwargs) - + self.blr = BLR(**kwargs) + @property def n_params(self): return self._n_params - + @property def neg_log_lik(self): return self.blr.nlZ @@ -173,28 +174,28 @@ def estimate(self, X, y, **kwargs): theta = kwargs.pop('theta', None) if isinstance(theta, str): theta = np.array(literal_eval(theta)) - + # remove warp string to prevent it being passed to the blr object - kwargs.pop('warp',None) - + kwargs.pop('warp', None) + Phi = create_poly_basis(X, self._model_order) if len(y.shape) > 1: y = y.ravel() - + if theta is None: - theta = self.theta0 - + theta = self.theta0 + # (re-)initialize BLR object because parameters were not specified - self.blr = BLR(theta=theta, X=Phi, y=y, - var_groups=self.var_groups, + self.blr = BLR(theta=theta, X=Phi, y=y, + var_groups=self.var_groups, warp=self.warp, **kwargs) - self.theta = self.blr.estimate(theta, Phi, y, + self.theta = self.blr.estimate(theta, Phi, y, var_covariates=self.var_covariates, **kwargs) - + return self - def predict(self, Xs, X=None, y=None, **kwargs): + def predict(self, Xs, X=None, y=None, **kwargs): """ Predict the target values for the given test data. @@ -217,18 +218,18 @@ def predict(self, Xs, X=None, y=None, **kwargs): - 'adaptvargroupfile': File containing the variance groups to adapt to. Optional. :return: The predicted target values for the test data. """ - - theta = self.theta # always use the estimated coefficients + + theta = self.theta # always use the estimated coefficients # remove from kwargs to avoid downstream problems kwargs.pop('theta', None) Phis = create_poly_basis(Xs, self._model_order) - + if X is None: Phi = None else: Phi = create_poly_basis(X, self._model_order) - + # process variance groups for the test data if 'testvargroup' in kwargs: var_groups_te = kwargs.pop('testvargroup') @@ -241,7 +242,7 @@ def predict(self, Xs, X=None, y=None, **kwargs): var_groups_te = np.loadtxt(var_groups_test_file) else: var_groups_te = None - + # process test variance covariates if 'testvarcov' in kwargs: var_cov_te = kwargs.pop('testvarcov') @@ -254,7 +255,7 @@ def predict(self, Xs, X=None, y=None, **kwargs): var_cov_te = np.loadtxt(var_cov_test_file) else: var_cov_te = None - + # do we want to adjust the responses? if 'adaptresp' in kwargs: y_adapt = kwargs.pop('adaptresp') @@ -265,7 +266,7 @@ def predict(self, Xs, X=None, y=None, **kwargs): y_adapt = y_adapt[:, np.newaxis] else: y_adapt = None - + if 'adaptcov' in kwargs: X_adapt = kwargs.pop('adaptcov') Phi_adapt = create_poly_basis(X_adapt, self._model_order) @@ -275,11 +276,11 @@ def predict(self, Xs, X=None, y=None, **kwargs): Phi_adapt = create_poly_basis(X_adapt, self._model_order) else: Phi_adapt = None - + if 'adaptvargroup' in kwargs: var_groups_ad = kwargs.pop('adaptvargroup') else: - if 'adaptvargroupfile' in kwargs: + if 'adaptvargroupfile' in kwargs: var_groups_adapt_file = kwargs.pop('adaptvargroupfile') if var_groups_adapt_file.endswith('.pkl'): var_groups_ad = pd.read_pickle(var_groups_adapt_file) @@ -287,17 +288,16 @@ def predict(self, Xs, X=None, y=None, **kwargs): var_groups_ad = np.loadtxt(var_groups_adapt_file) else: var_groups_ad = None - + if y_adapt is None: - yhat, s2 = self.blr.predict(theta, Phi, y, Phis, + yhat, s2 = self.blr.predict(theta, Phi, y, Phis, var_groups_test=var_groups_te, - var_covariates_test=var_cov_te, + var_covariates_test=var_cov_te, **kwargs) else: - yhat, s2 = self.blr.predict_and_adjust(theta, Phi_adapt, y_adapt, Phis, + yhat, s2 = self.blr.predict_and_adjust(theta, Phi_adapt, y_adapt, Phis, var_groups_test=var_groups_te, - var_groups_adapt=var_groups_ad, + var_groups_adapt=var_groups_ad, **kwargs) - + return yhat, s2 - \ No newline at end of file diff --git a/pcntoolkit/normative_model/norm_gpr.py b/pcntoolkit/normative_model/norm_gpr.py index c7926f8c..36e8d5c3 100644 --- a/pcntoolkit/normative_model/norm_gpr.py +++ b/pcntoolkit/normative_model/norm_gpr.py @@ -19,11 +19,12 @@ from model.gp import GPR, CovSum from norm_base import NormBase + class NormGPR(NormBase): """ Classical GPR-based normative modelling approach """ - def __init__(self, **kwargs): #X=None, y=None, theta=None, + def __init__(self, **kwargs): # X=None, y=None, theta=None, """ Initialize the NormGPR object. @@ -42,20 +43,20 @@ def __init__(self, **kwargs): #X=None, y=None, theta=None, self.covfunc = CovSum(X, ('CovLin', 'CovSqExpARD')) self.theta0 = np.zeros(self.covfunc.get_n_params() + 1) self.theta = self.theta0 - + if (theta is not None) and (X is not None) and (y is not None): self.gpr = GPR(theta, self.covfunc, X, y) self._n_params = self.covfunc.get_n_params() + 1 else: self.gpr = GPR() - + @property def n_params(self): - if not hasattr(self,'_n_params'): - self._n_params = self.covfunc.get_n_params() + 1 - + if not hasattr(self, '_n_params'): + self._n_params = self.covfunc.get_n_params() + 1 + return self._n_params - + @property def neg_log_lik(self): return self.gpr.nlZ @@ -80,7 +81,7 @@ def estimate(self, X, y, **kwargs): theta = self.theta0 self.gpr = GPR(theta, self.covfunc, X, y) self.theta = self.gpr.estimate(theta, self.covfunc, X, y) - + return self def predict(self, Xs, X, y, **kwargs): @@ -103,10 +104,9 @@ def predict(self, Xs, X, y, **kwargs): if theta is None: theta = self.theta yhat, s2 = self.gpr.predict(theta, X, y, Xs) - + # only return the marginal variances if len(s2.shape) == 2: s2 = np.diag(s2) - + return yhat, s2 - \ No newline at end of file diff --git a/pcntoolkit/normative_model/norm_hbr.py b/pcntoolkit/normative_model/norm_hbr.py index e71fe78b..4ec368ed 100644 --- a/pcntoolkit/normative_model/norm_hbr.py +++ b/pcntoolkit/normative_model/norm_hbr.py @@ -131,17 +131,20 @@ def __init__(self, **kwargs): self.configs["tsbefile"] = kwargs.get("tsbefile", None) # Model settings self.configs["type"] = kwargs.get("model_type", "linear") - self.configs["random_noise"] = kwargs.get("random_noise", "True") == "True" + self.configs["random_noise"] = kwargs.get( + "random_noise", "True") == "True" self.configs["likelihood"] = kwargs.get("likelihood", "Normal") # sampler settings self.configs["n_samples"] = int(kwargs.get("n_samples", "1000")) self.configs["n_tuning"] = int(kwargs.get("n_tuning", "500")) self.configs["n_chains"] = int(kwargs.get("n_chains", "1")) self.configs["sampler"] = kwargs.get("sampler", "NUTS") - self.configs["target_accept"] = float(kwargs.get("target_accept", "0.8")) + self.configs["target_accept"] = float( + kwargs.get("target_accept", "0.8")) self.configs["init"] = kwargs.get("init", "jitter+adapt_diag") self.configs["cores"] = int(kwargs.get("cores", "1")) - self.configs["remove_datapoints_from_posterior"] = kwargs.get("remove_datapoints_from_posterior","True") == "True" + self.configs["remove_datapoints_from_posterior"] = kwargs.get( + "remove_datapoints_from_posterior", "True") == "True" # model transfer setting self.configs["freedom"] = int(kwargs.get("freedom", "1")) self.configs["transferred"] = False @@ -185,7 +188,7 @@ def __init__(self, **kwargs): kwargs.get(f"linear_{p}", "False") == "True" ) - ######## Deprecations (remove in later version) + # Deprecations (remove in later version) if f"{p}_linear" in kwargs.keys(): print( f"The keyword '{p}_linear' is deprecated. It is now automatically replaced with 'linear_{p}'" @@ -193,16 +196,17 @@ def __init__(self, **kwargs): self.configs[f"linear_{p}"] = ( kwargs.get(f"{p}_linear", "False") == "True" ) - ##### End Deprecations + # End Deprecations for c in ["centered", "random"]: - self.configs[f"{c}_{p}"] = kwargs.get(f"{c}_{p}", "False") == "True" + self.configs[f"{c}_{p}"] = kwargs.get( + f"{c}_{p}", "False") == "True" for sp in ["slope", "intercept"]: self.configs[f"{c}_{sp}_{p}"] = ( kwargs.get(f"{c}_{sp}_{p}", "False") == "True" ) - ######## Deprecations (remove in later version) + # Deprecations (remove in later version) if self.configs["linear_sigma"]: if "random_noise" in kwargs.keys(): print( @@ -225,9 +229,9 @@ def __init__(self, **kwargs): self.configs["random_slope_mu"] = ( kwargs.get("random_slope", "True") == "True" ) - ##### End Deprecations + # End Deprecations - ## Default parameters + # Default parameters self.configs["linear_mu"] = kwargs.get("linear_mu", "True") == "True" self.configs["random_mu"] = kwargs.get("random_mu", "True") == "True" self.configs["random_intercept_mu"] = ( @@ -236,9 +240,11 @@ def __init__(self, **kwargs): self.configs["random_slope_mu"] = ( kwargs.get("random_slope_mu", "True") == "True" ) - self.configs["random_sigma"] = kwargs.get("random_sigma", "True") == "True" - self.configs["centered_sigma"] = kwargs.get("centered_sigma", "True") == "True" - ## End default parameters + self.configs["random_sigma"] = kwargs.get( + "random_sigma", "True") == "True" + self.configs["centered_sigma"] = kwargs.get( + "centered_sigma", "True") == "True" + # End default parameters self.hbr = HBR(self.configs) @@ -321,8 +327,6 @@ def predict(self, Xs, X=None, Y=None, **kwargs): return yhat.squeeze(), s2.squeeze() - - def estimate_on_new_sites(self, X, y, batch_effects): """ Samples from the posterior of the Hierarchical Bayesian Regression model. @@ -365,7 +369,6 @@ def extend( samples=10, informative_prior=False, ): - """ Extend the Hierarchical Bayesian Regression model using data sampled from the posterior predictive distribution. @@ -382,7 +385,8 @@ def extend( :param informative_prior: Whether to use the adapt method for estimation. Default is False. :return: The instance of the NormHBR object. """ - X_dummy, batch_effects_dummy = self.hbr.create_dummy_inputs(X_dummy_ranges) + X_dummy, batch_effects_dummy = self.hbr.create_dummy_inputs( + X_dummy_ranges) X_dummy, batch_effects_dummy, Y_dummy = self.hbr.generate( X_dummy, batch_effects_dummy, samples @@ -419,15 +423,17 @@ def tune( samples=10, informative_prior=False, ): - - #TODO need to check if this is correct - + + # TODO need to check if this is correct + tune_ids = list(np.unique(batch_effects[:, merge_batch_dim])) - - X_dummy, batch_effects_dummy = self.hbr.create_dummy_inputs(X_dummy_ranges) + + X_dummy, batch_effects_dummy = self.hbr.create_dummy_inputs( + X_dummy_ranges) for idx in tune_ids: - X_dummy = X_dummy[batch_effects_dummy[:, merge_batch_dim] != idx, :] + X_dummy = X_dummy[batch_effects_dummy[:, + merge_batch_dim] != idx, :] batch_effects_dummy = batch_effects_dummy[ batch_effects_dummy[:, merge_batch_dim] != idx, : ] @@ -465,8 +471,10 @@ def merge( :param samples: Number of samples to generate for the dummy data. Default is 10. """ - X_dummy1, batch_effects_dummy1 = self.hbr.create_dummy_inputs(X_dummy_ranges) - X_dummy2, batch_effects_dummy2 = nm.hbr.create_dummy_inputs(X_dummy_ranges) + X_dummy1, batch_effects_dummy1 = self.hbr.create_dummy_inputs( + X_dummy_ranges) + X_dummy2, batch_effects_dummy2 = nm.hbr.create_dummy_inputs( + X_dummy_ranges) X_dummy1, batch_effects_dummy1, Y_dummy1 = self.hbr.generate( X_dummy1, batch_effects_dummy1, samples @@ -494,9 +502,8 @@ def generate(self, X, batch_effects, samples=10): X, batch_effects, samples ) return X, batch_effects, generated_samples - - def get_mcmc_quantiles(self, X, batch_effects=None, z_scores=None): + def get_mcmc_quantiles(self, X, batch_effects=None, z_scores=None): """ Computes quantiles of an estimated normative model. @@ -508,16 +515,15 @@ def get_mcmc_quantiles(self, X, batch_effects=None, z_scores=None): # Set batch effects to zero if none are provided if batch_effects is None: batch_effects = batch_effects_test = np.zeros([X.shape[0], 1]) - # Set the z_scores for which the quantiles are computed if z_scores is None: z_scores = np.arange(-3, 4) - likelihood=self.configs['likelihood'] + likelihood = self.configs['likelihood'] # Determine the variables to predict if self.configs["likelihood"] == "Normal": - var_names = ["mu_samples", "sigma_samples","sigma_plus_samples"] + var_names = ["mu_samples", "sigma_samples", "sigma_plus_samples"] elif self.configs["likelihood"].startswith("SHASH"): var_names = [ "mu_samples", @@ -533,7 +539,7 @@ def get_mcmc_quantiles(self, X, batch_effects=None, z_scores=None): # Delete the posterior predictive if it already exists if 'posterior_predictive' in self.hbr.idata.groups(): del self.hbr.idata.posterior_predictive - + # Do a forward to get the posterior predictive in the idata self.hbr.predict( X=X, @@ -547,18 +553,19 @@ def get_mcmc_quantiles(self, X, batch_effects=None, z_scores=None): post_pred = az.extract( self.hbr.idata, "posterior_predictive", var_names=var_names ) - + # Remove superfluous var_nammes var_names.remove('sigma_samples') if 'delta_samples' in var_names: var_names.remove('delta_samples') - # Separate the samples into a list so that they can be unpacked + # Separate the samples into a list so that they can be unpacked array_of_vars = list(map(lambda x: post_pred[x], var_names)) # Create an array to hold the quantiles len_synth_data, n_mcmc_samples = post_pred["mu_samples"].shape - quantiles = np.zeros((z_scores.shape[0], len_synth_data, n_mcmc_samples)) + quantiles = np.zeros( + (z_scores.shape[0], len_synth_data, n_mcmc_samples)) # Compute the quantile iteratively for each z-score for i, j in enumerate(z_scores): @@ -569,10 +576,8 @@ def get_mcmc_quantiles(self, X, batch_effects=None, z_scores=None): kwargs={"zs": zs, "likelihood": self.configs['likelihood']}, ) return quantiles.mean(axis=-1) - def get_mcmc_zscores(self, X, y, **kwargs): - """ Computes zscores of data given an estimated model @@ -580,9 +585,9 @@ def get_mcmc_zscores(self, X, y, **kwargs): X ([N*p]ndarray): covariates y ([N*1]ndarray): response variables """ - + print(self.configs['likelihood']) - + tsbefile = kwargs.get("tsbefile", None) if tsbefile is not None: batch_effects_test = fileio.load(tsbefile) @@ -592,7 +597,7 @@ def get_mcmc_zscores(self, X, y, **kwargs): # Determine the variables to predict if self.configs["likelihood"] == "Normal": - var_names = ["mu_samples", "sigma_samples","sigma_plus_samples"] + var_names = ["mu_samples", "sigma_samples", "sigma_plus_samples"] elif self.configs["likelihood"].startswith("SHASH"): var_names = [ "mu_samples", @@ -608,7 +613,7 @@ def get_mcmc_zscores(self, X, y, **kwargs): # Delete the posterior predictive if it already exists if 'posterior_predictive' in self.hbr.idata.groups(): del self.hbr.idata.posterior_predictive - + # Do a forward to get the posterior predictive in the idata self.hbr.predict( X=X, @@ -622,31 +627,31 @@ def get_mcmc_zscores(self, X, y, **kwargs): post_pred = az.extract( self.hbr.idata, "posterior_predictive", var_names=var_names ) - + # Remove superfluous var_names var_names.remove('sigma_samples') if 'delta_samples' in var_names: var_names.remove('delta_samples') - # Separate the samples into a list so that they can be unpacked + # Separate the samples into a list so that they can be unpacked array_of_vars = list(map(lambda x: post_pred[x], var_names)) # Create an array to hold the quantiles len_data, n_mcmc_samples = post_pred["mu_samples"].shape # Compute the quantile iteratively for each z-score - z_scores = xarray.apply_ufunc( + z_scores = xarray.apply_ufunc( z_score, *array_of_vars, kwargs={"y": y, "likelihood": self.configs['likelihood']}, ) return z_scores.mean(axis=-1).values - def S_inv(x, e, d): return np.sinh((np.arcsinh(x) + e) / d) + def K(p, x): """ Computes the values of spp.kv(p,x) for only the unique values of p @@ -655,6 +660,7 @@ def K(p, x): ps, idxs = np.unique(p, return_inverse=True) return spp.kv(ps, x)[idxs].reshape(p.shape) + def P(q): """ The P function as given in Jones et al. @@ -667,6 +673,7 @@ def P(q): a = (K1 + K2) * frac return a + def m(epsilon, delta, r): """ The r'th uncentered moment. Given by Jones et al. @@ -681,27 +688,28 @@ def m(epsilon, delta, r): acc += combs * flip * ex * p return frac1 * acc -def quantile( mu, sigma, epsilon=None, delta=None, zs=0, likelihood = "Normal"): + +def quantile(mu, sigma, epsilon=None, delta=None, zs=0, likelihood="Normal"): """Get the zs'th quantiles given likelihood parameters""" if likelihood.startswith('SHASH'): if likelihood == "SHASHo": - quantiles = S_inv(zs,epsilon,delta)*sigma + mu + quantiles = S_inv(zs, epsilon, delta)*sigma + mu elif likelihood == "SHASHo2": sigma_d = sigma/delta - quantiles = S_inv(zs,epsilon,delta)*sigma_d + mu + quantiles = S_inv(zs, epsilon, delta)*sigma_d + mu elif likelihood == "SHASHb": true_mu = m(epsilon, delta, 1) true_sigma = np.sqrt((m(epsilon, delta, 2) - true_mu ** 2)) - SHASH_c = ((S_inv(zs,epsilon,delta)-true_mu)/true_sigma) - quantiles = SHASH_c *sigma + mu + SHASH_c = ((S_inv(zs, epsilon, delta)-true_mu)/true_sigma) + quantiles = SHASH_c * sigma + mu elif likelihood == 'Normal': quantiles = zs*sigma + mu else: exit("Unsupported likelihood") return quantiles - -def z_score(mu, sigma, epsilon=None, delta=None, y=None, likelihood = "Normal"): + +def z_score(mu, sigma, epsilon=None, delta=None, y=None, likelihood="Normal"): """Get the z-scores of Y, given likelihood parameters""" if likelihood.startswith('SHASH'): if likelihood == "SHASHo": @@ -722,4 +730,3 @@ def z_score(mu, sigma, epsilon=None, delta=None, y=None, likelihood = "Normal"): else: exit("Unsupported likelihood") return Z - diff --git a/pcntoolkit/normative_model/norm_np.py b/pcntoolkit/normative_model/norm_np.py index 7986cc46..83e59218 100755 --- a/pcntoolkit/normative_model/norm_np.py +++ b/pcntoolkit/normative_model/norm_np.py @@ -33,9 +33,11 @@ from model.NPR import NPR, np_loss from norm_base import NormBase + class struct(object): pass - + + class Encoder(nn.Module): """ Encoder module for the Neural Process Regression model. @@ -50,6 +52,7 @@ class Encoder(nn.Module): - z_dim: Dimension of the latent variable. - hidden_neuron_num: Number of neurons in the hidden layers. """ + def __init__(self, x, y, args): """ Initialize the Encoder module. @@ -84,8 +87,8 @@ def forward(self, x, y): x_y = F.relu(self.h_3(x_y)) r = torch.mean(x_y, dim=1) return r - - + + class Decoder(nn.Module): """ Decoder module for the Neural Process Regression model. @@ -100,6 +103,7 @@ class Decoder(nn.Module): - z_dim: Dimension of the latent variable. - hidden_neuron_num: Number of neurons in the hidden layers. """ + def __init__(self, x, y, args): """ Initialize the Decoder module. @@ -115,15 +119,15 @@ def __init__(self, x, y, args): self.r_dim = args.r_dim self.z_dim = args.z_dim self.hidden_neuron_num = args.hidden_neuron_num - + self.g_1 = nn.Linear(self.z_dim, self.hidden_neuron_num) self.g_2 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) self.g_3 = nn.Linear(self.hidden_neuron_num, y.shape[1]) - + self.g_1_84 = nn.Linear(self.z_dim, self.hidden_neuron_num) self.g_2_84 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) self.g_3_84 = nn.Linear(self.hidden_neuron_num, y.shape[1]) - + def forward(self, z_sample): """ Forward pass of the Decoder module. @@ -134,20 +138,18 @@ def forward(self, z_sample): z_hat = F.relu(self.g_1(z_sample)) z_hat = F.relu(self.g_2(z_hat)) y_hat = torch.sigmoid(self.g_3(z_hat)) - + z_hat_84 = F.relu(self.g_1(z_sample)) z_hat_84 = F.relu(self.g_2_84(z_hat_84)) y_hat_84 = torch.sigmoid(self.g_3_84(z_hat_84)) - + return y_hat, y_hat_84 - - class NormNP(NormBase): """ Classical GPR-based normative modelling approach """ - + def __init__(self, X, y, configparam=None): """ Initialize the NormNP object. @@ -161,9 +163,9 @@ def __init__(self, X, y, configparam=None): :param configparam: Path to a pickle file containing configuration parameters. Optional. """ self.configparam = configparam - if configparam is not None: + if configparam is not None: with open(configparam, 'rb') as handle: - config = pickle.load(handle) + config = pickle.load(handle) args = struct() if 'batch_size' in config: args.batch_size = config['batch_size'] @@ -207,24 +209,23 @@ def __init__(self, X, y, configparam=None): args.r_dim = 5 args.z_dim = 3 args.nv = 0.01 - + if y is not None: if y.ndim == 1: - y = y.reshape(-1,1) + y = y.reshape(-1, 1) self.args = args self.encoder = Encoder(X, y, args) self.decoder = Decoder(X, y, args) self.model = NPR(self.encoder, self.decoder, args) - - + @property def n_params(self): return 1 - + @property def neg_log_lik(self): return -1 - + def estimate(self, X, y): """ Estimate the parameters of the Neural Process Regression model. @@ -237,57 +238,64 @@ def estimate(self, X, y): :return: The instance of the norm_np object with updated parameters. """ if y.ndim == 1: - y = y.reshape(-1,1) + y = y.reshape(-1, 1) sample_num = X.shape[0] batch_size = self.args.batch_size factor_num = self.args.m mini_batch_num = int(np.floor(sample_num/batch_size)) device = self.args.device - + self.scaler = MinMaxScaler() y = self.scaler.fit_transform(y) - + self.reg = [] for i in range(factor_num): self.reg.append(LinearRegression()) - idx = np.random.randint(0, sample_num, sample_num)#int(sample_num/10)) - self.reg[i].fit(X[idx,:],y[idx,:]) - + # int(sample_num/10)) + idx = np.random.randint(0, sample_num, sample_num) + self.reg[i].fit(X[idx, :], y[idx, :]) + x_context = np.zeros([sample_num, factor_num, X.shape[1]]) y_context = np.zeros([sample_num, factor_num, 1]) - + s = X.std(axis=0) for j in range(factor_num): - x_context[:,j,:] = X + np.sqrt(self.args.nv) * s * np.random.randn(X.shape[0], X.shape[1]) - y_context[:,j,:] = self.reg[j].predict(x_context[:,j,:]) - - x_context = torch.tensor(x_context, device=device, dtype = torch.float) - y_context = torch.tensor(y_context, device=device, dtype = torch.float) - - x_all = torch.tensor(np.expand_dims(X,axis=1), device=device, dtype = torch.float) - y_all = torch.tensor(y.reshape(-1, 1, y.shape[1]), device=device, dtype = torch.float) - + x_context[:, j, :] = X + \ + np.sqrt(self.args.nv) * s * \ + np.random.randn(X.shape[0], X.shape[1]) + y_context[:, j, :] = self.reg[j].predict(x_context[:, j, :]) + + x_context = torch.tensor(x_context, device=device, dtype=torch.float) + y_context = torch.tensor(y_context, device=device, dtype=torch.float) + + x_all = torch.tensor(np.expand_dims(X, axis=1), + device=device, dtype=torch.float) + y_all = torch.tensor( + y.reshape(-1, 1, y.shape[1]), device=device, dtype=torch.float) + self.model.train() - epochs = [int(self.args.epochs/4),int(self.args.epochs/2),int(self.args.epochs/5), + epochs = [int(self.args.epochs/4), int(self.args.epochs/2), int(self.args.epochs/5), int(self.args.epochs-self.args.epochs/4-self.args.epochs/2-self.args.epochs/5)] k = 1 - for e in range(len(epochs)): + for e in range(len(epochs)): optimizer = optim.Adam(self.model.parameters(), lr=10**(-e-2)) for j in range(epochs[e]): train_loss = 0 for i in range(mini_batch_num): optimizer.zero_grad() - idx = np.arange(i*batch_size,(i+1)*batch_size) - y_hat, y_hat_84, z_all, z_context, dummy, dummy = self.model(x_context[idx,:,:], y_context[idx,:,:], x_all[idx,:,:], y_all[idx,:,:]) - loss = np_loss(y_hat, y_hat_84, y_all[idx,0,:], z_all, z_context) + idx = np.arange(i*batch_size, (i+1)*batch_size) + y_hat, y_hat_84, z_all, z_context, dummy, dummy = self.model( + x_context[idx, :, :], y_context[idx, :, :], x_all[idx, :, :], y_all[idx, :, :]) + loss = np_loss(y_hat, y_hat_84, + y_all[idx, 0, :], z_all, z_context) loss.backward() train_loss += loss.item() optimizer.step() - print('Epoch: %d, Loss:%f' %( k, train_loss)) + print('Epoch: %d, Loss:%f' % (k, train_loss)) k += 1 return self - - def predict(self, Xs, X=None, Y=None, theta=None): + + def predict(self, Xs, X=None, Y=None, theta=None): """ Predict the target values for the given test data. @@ -304,18 +312,22 @@ def predict(self, Xs, X=None, Y=None, theta=None): x_context_test = np.zeros([sample_num, factor_num, Xs.shape[1]]) y_context_test = np.zeros([sample_num, factor_num, 1]) for j in range(factor_num): - x_context_test[:,j,:] = Xs - y_context_test[:,j,:] = self.reg[j].predict(x_context_test[:,j,:]) - x_context_test = torch.tensor(x_context_test, device=self.args.device, dtype = torch.float) - y_context_test = torch.tensor(y_context_test, device=self.args.device, dtype = torch.float) + x_context_test[:, j, :] = Xs + y_context_test[:, j, :] = self.reg[j].predict( + x_context_test[:, j, :]) + x_context_test = torch.tensor( + x_context_test, device=self.args.device, dtype=torch.float) + y_context_test = torch.tensor( + y_context_test, device=self.args.device, dtype=torch.float) self.model.eval() with torch.no_grad(): - y_hat, y_hat_84, z_all, z_context, y_sigma, y_sigma_84 = self.model(x_context_test, y_context_test, n = 100) - + y_hat, y_hat_84, z_all, z_context, y_sigma, y_sigma_84 = self.model( + x_context_test, y_context_test, n=100) + y_hat = self.scaler.inverse_transform(y_hat.cpu().numpy()) y_hat_84 = self.scaler.inverse_transform(y_hat_84.cpu().numpy()) y_sigma = y_sigma.cpu().numpy() * (self.scaler.data_max_ - self.scaler.data_min_) y_sigma_84 = y_sigma_84.cpu().numpy() * (self.scaler.data_max_ - self.scaler.data_min_) sigma_al = y_hat - y_hat_84 - return y_hat.squeeze(), (y_sigma**2 + sigma_al**2).squeeze() #, z_context[0].cpu().numpy(), z_context[1].cpu().numpy() - \ No newline at end of file + # , z_context[0].cpu().numpy(), z_context[1].cpu().numpy() + return y_hat.squeeze(), (y_sigma**2 + sigma_al**2).squeeze() diff --git a/pcntoolkit/normative_model/norm_rfa.py b/pcntoolkit/normative_model/norm_rfa.py index 39b7cf5d..e514ab4a 100644 --- a/pcntoolkit/normative_model/norm_rfa.py +++ b/pcntoolkit/normative_model/norm_rfa.py @@ -7,7 +7,7 @@ try: # run as a package if installed from pcntoolkit.normative_model.norm_base import NormBase - from pcntoolkit.model.rfa import GPRRFA + from pcntoolkit.model.rfa import GPRRFA except ImportError: pass @@ -19,6 +19,7 @@ from model.rfa import GPRRFA from norm_base import NormBase + class NormRFA(NormBase): """ Classical GPR-based normative modelling approach """ @@ -44,24 +45,24 @@ def __init__(self, X, y=None, theta=None, n_feat=None): self.gprrfa = GPRRFA(theta, X, n_feat=n_feat) self._n_params = self.gprrfa.get_n_params(X) else: - raise(ValueError, 'please specify covariates') + raise (ValueError, 'please specify covariates') return - + if theta is None: self.theta0 = np.zeros(self._n_params) else: if len(theta) == self._n_params: self.theta0 = theta else: - raise(ValueError, 'hyperparameter vector has incorrect size') - + raise (ValueError, 'hyperparameter vector has incorrect size') + self.theta = self.theta0 - + @property def n_params(self): - + return self._n_params - + @property def neg_log_lik(self): return self.gprrfa.nlZ @@ -83,7 +84,7 @@ def estimate(self, X, y, theta=None): theta = self.theta0 self.gprrfa = GPRRFA(theta, X, y) self.theta = self.gprrfa.estimate(theta, X, y) - + return self def predict(self, Xs, X, y, theta=None): @@ -104,6 +105,5 @@ def predict(self, Xs, X, y, theta=None): if theta is None: theta = self.theta yhat, s2 = self.gprrfa.predict(theta, X, y, Xs) - + return yhat, s2 - \ No newline at end of file diff --git a/pcntoolkit/normative_model/norm_utils.py b/pcntoolkit/normative_model/norm_utils.py index 48e79980..4e748468 100644 --- a/pcntoolkit/normative_model/norm_utils.py +++ b/pcntoolkit/normative_model/norm_utils.py @@ -11,10 +11,11 @@ from norm_hbr import NormHBR from norm_np import NormNP + def norm_init(X, y=None, theta=None, alg='gpr', **kwargs): if alg == 'gpr': nm = NormGPR(X=X, y=y, theta=theta, **kwargs) - elif alg =='blr': + elif alg == 'blr': nm = NormBLR(X=X, y=y, theta=theta, **kwargs) elif alg == 'rfa': nm = NormRFA(X=X, y=y, theta=theta, **kwargs) @@ -23,6 +24,6 @@ def norm_init(X, y=None, theta=None, alg='gpr', **kwargs): elif alg == 'np': nm = NormNP(X=X, y=y, **kwargs) else: - raise(ValueError, "Algorithm " + alg + " not known.") - - return nm \ No newline at end of file + raise (ValueError, "Algorithm " + alg + " not known.") + + return nm diff --git a/pcntoolkit/normative_parallel.py b/pcntoolkit/normative_parallel.py index 119e5055..de3005ec 100755 --- a/pcntoolkit/normative_parallel.py +++ b/pcntoolkit/normative_parallel.py @@ -14,7 +14,7 @@ # Second, the splits are submitted to the cluster. # Third, the output is collected and combined. # -# witten by (primarily) T Wolfers, (adaptated) SM Kia, H Huijsdens, L Parks, +# witten by (primarily) T Wolfers, (adaptated) SM Kia, H Huijsdens, L Parks, # S Rutherford, AF Marquand # ----------------------------------------------------------------------------- @@ -37,8 +37,8 @@ import pcntoolkit as ptk import pcntoolkit.dataio.fileio as fileio from pcntoolkit import configs - from pcntoolkit.util.utils import yes_or_no - ptkpath = ptk.__path__[0] + from pcntoolkit.util.utils import yes_or_no + ptkpath = ptk.__path__[0] except ImportError: pass ptkpath = os.path.abspath(os.path.dirname(__file__)) @@ -46,9 +46,9 @@ sys.path.append(ptkpath) import dataio.fileio as fileio import configs - from util.utils import yes_or_no - - + from util.utils import yes_or_no + + PICKLE_PROTOCOL = configs.PICKLE_PROTOCOL @@ -64,7 +64,6 @@ def execute_nm(processing_dir, func='estimate', interactive=False, **kwargs): - ''' Execute parallel normative models This function is a mother function that executes all parallel normative modelling routines. Different specifications are possible using the sub- @@ -100,18 +99,18 @@ def execute_nm(processing_dir, written by (primarily) T Wolfers, (adapted) SM Kia The documentation is adapated by S Rutherford. ''' - + if normative_path is None: normative_path = ptkpath + '/normative.py' - + cv_folds = kwargs.get('cv_folds', None) testcovfile_path = kwargs.get('testcovfile_path', None) - testrespfile_path= kwargs.get('testrespfile_path', None) + testrespfile_path = kwargs.get('testrespfile_path', None) outputsuffix = kwargs.get('outputsuffix', 'estimate') cluster_spec = kwargs.pop('cluster_spec', 'torque') log_path = kwargs.get('log_path', None) binary = kwargs.pop('binary', False) - + split_nm(processing_dir, respfile_path, batch_size, @@ -127,14 +126,14 @@ def execute_nm(processing_dir, file_extentions = '.pkl' else: file_extentions = '.txt' - - kwargs.update({'batch_size':str(batch_size)}) + + kwargs.update({'batch_size': str(batch_size)}) job_ids = [] for n in range(1, number_of_batches+1): - kwargs.update({'job_id':str(n)}) + kwargs.update({'job_id': str(n)}) if testrespfile_path is not None: if cv_folds is not None: - raise(ValueError, """If the response file is specified + raise (ValueError, """If the response file is specified cv_folds must be equal to None""") else: # specified train/test split @@ -147,9 +146,9 @@ def execute_nm(processing_dir, str(n) + file_extentions) batch_job_path = batch_processing_dir + batch_job_name if cluster_spec == 'torque': - - # update the response file - kwargs.update({'testrespfile_path': \ + + # update the response file + kwargs.update({'testrespfile_path': batch_testrespfile_path}) bashwrap_nm(batch_processing_dir, python_path, @@ -160,29 +159,29 @@ def execute_nm(processing_dir, func=func, **kwargs) job_id = qsub_nm(job_path=batch_job_path, - log_path=log_path, - memory=memory, - duration=duration) + log_path=log_path, + memory=memory, + duration=duration) job_ids.append(job_id) elif cluster_spec == 'sbatch': - # update the response file - kwargs.update({'testrespfile_path': \ + # update the response file + kwargs.update({'testrespfile_path': batch_testrespfile_path}) sbatchwrap_nm(batch_processing_dir, - python_path, - normative_path, - batch_job_name, - covfile_path, - batch_respfile_path, - func=func, - memory=memory, - duration=duration, - **kwargs) + python_path, + normative_path, + batch_job_name, + covfile_path, + batch_respfile_path, + func=func, + memory=memory, + duration=duration, + **kwargs) sbatch_nm(job_path=batch_job_path, - log_path=log_path) + log_path=log_path) elif cluster_spec == 'new': # this part requires addition in different envioronment [ - sbatchwrap_nm(processing_dir=batch_processing_dir, + sbatchwrap_nm(processing_dir=batch_processing_dir, func=func, **kwargs) sbatch_nm(processing_dir=batch_processing_dir) # ] @@ -204,21 +203,21 @@ def execute_nm(processing_dir, func=func, **kwargs) job_id = qsub_nm(job_path=batch_job_path, - log_path=log_path, - memory=memory, - duration=duration) + log_path=log_path, + memory=memory, + duration=duration) job_ids.append(job_id) elif cluster_spec == 'sbatch': sbatchwrap_nm(batch_processing_dir, - python_path, - normative_path, - batch_job_name, - covfile_path, - batch_respfile_path, - func=func, - memory=memory, - duration=duration, - **kwargs) + python_path, + normative_path, + batch_job_name, + covfile_path, + batch_respfile_path, + func=func, + memory=memory, + duration=duration, + **kwargs) sbatch_nm(job_path=batch_job_path, log_path=log_path) elif cluster_spec == 'new': @@ -246,23 +245,23 @@ def execute_nm(processing_dir, func=func, **kwargs) job_id = qsub_nm(job_path=batch_job_path, - log_path=log_path, - memory=memory, - duration=duration) + log_path=log_path, + memory=memory, + duration=duration) job_ids.append(job_id) elif cluster_spec == 'sbatch': sbatchwrap_nm(batch_processing_dir, - python_path, - normative_path, - batch_job_name, - covfile_path, - batch_respfile_path, - func=func, - memory=memory, - duration=duration, - **kwargs) + python_path, + normative_path, + batch_job_name, + covfile_path, + batch_respfile_path, + func=func, + memory=memory, + duration=duration, + **kwargs) sbatch_nm(job_path=batch_job_path, - log_path=log_path) + log_path=log_path) elif cluster_spec == 'new': # this part requires addition in different envioronment [ bashwrap_nm(processing_dir=batch_processing_dir, func=func, @@ -271,66 +270,66 @@ def execute_nm(processing_dir, # ] if interactive: - + check_jobs(job_ids, delay=60) - + success = False while (not success): success = collect_nm(processing_dir, - job_name, - func=func, - collect=False, - binary=binary, - batch_size=batch_size, - outputsuffix=outputsuffix) + job_name, + func=func, + collect=False, + binary=binary, + batch_size=batch_size, + outputsuffix=outputsuffix) if success: break else: if interactive == 'query': response = yes_or_no('Rerun the failed jobs?') if response: - rerun_nm(processing_dir, log_path=log_path, memory=memory, - duration=duration, binary=binary, + rerun_nm(processing_dir, log_path=log_path, memory=memory, + duration=duration, binary=binary, interactive=interactive) else: success = True else: print('Reruning the failed jobs ...') - rerun_nm(processing_dir, log_path=log_path, memory=memory, - duration=duration, binary=binary, - interactive=interactive) - + rerun_nm(processing_dir, log_path=log_path, memory=memory, + duration=duration, binary=binary, + interactive=interactive) + if interactive == 'query': response = yes_or_no('Collect the results?') if response: success = collect_nm(processing_dir, - job_name, - func=func, - collect=True, - binary=binary, - batch_size=batch_size, - outputsuffix=outputsuffix) + job_name, + func=func, + collect=True, + binary=binary, + batch_size=batch_size, + outputsuffix=outputsuffix) else: print('Collecting the results ...') success = collect_nm(processing_dir, - job_name, - func=func, - collect=True, - binary=binary, - batch_size=batch_size, - outputsuffix=outputsuffix) + job_name, + func=func, + collect=True, + binary=binary, + batch_size=batch_size, + outputsuffix=outputsuffix) """routines that are environment independent""" + def split_nm(processing_dir, respfile_path, batch_size, binary, **kwargs): - ''' This function prepares the input files for normative_parallel. - + Basic usage:: split_nm(processing_dir, respfile_path, batch_size, binary, testrespfile_path) @@ -344,21 +343,21 @@ def split_nm(processing_dir, :outputs: The creation of a folder struture for batch-wise processing. witten by (primarily) T Wolfers (adapted) SM Kia, (adapted) S Rutherford. - ''' - + ''' + testrespfile_path = kwargs.pop('testrespfile_path', None) dummy, respfile_extension = os.path.splitext(respfile_path) if (binary and respfile_extension != '.pkl'): - raise(ValueError, """If binary is True the file format for the + raise (ValueError, """If binary is True the file format for the testrespfile file must be .pkl""") - elif (binary==False and respfile_extension != '.txt'): - raise(ValueError, """If binary is False the file format for the + elif (binary == False and respfile_extension != '.txt'): + raise (ValueError, """If binary is False the file format for the testrespfile file must be .txt""") # splits response into batches if testrespfile_path is None: - if (binary==False): + if (binary == False): respfile = fileio.load_ascii(respfile_path) else: respfile = pd.read_pickle(respfile_path) @@ -371,7 +370,7 @@ def split_nm(processing_dir, batch_size) batch_vec = np.append(batch_vec, numsub) - + for n in range(0, (len(batch_vec) - 1)): resp_batch = respfile.iloc[:, (batch_vec[n]): batch_vec[n + 1]] os.chdir(processing_dir) @@ -380,7 +379,7 @@ def split_nm(processing_dir, if not os.path.exists(processing_dir + batch): os.makedirs(processing_dir + batch) os.makedirs(processing_dir + batch + '/Models/') - if (binary==False): + if (binary == False): fileio.save_pd(resp_batch, processing_dir + batch + '/' + resp + '.txt') @@ -392,13 +391,13 @@ def split_nm(processing_dir, else: dummy, testrespfile_extension = os.path.splitext(testrespfile_path) if (binary and testrespfile_extension != '.pkl'): - raise(ValueError, """If binary is True the file format for the + raise (ValueError, """If binary is True the file format for the testrespfile file must be .pkl""") - elif(binary==False and testrespfile_extension != '.txt'): - raise(ValueError, """If binary is False the file format for the + elif (binary == False and testrespfile_extension != '.txt'): + raise (ValueError, """If binary is False the file format for the testrespfile file must be .txt""") - if (binary==False): + if (binary == False): respfile = fileio.load_ascii(respfile_path) testrespfile = fileio.load_ascii(testrespfile_path) else: @@ -416,7 +415,7 @@ def split_nm(processing_dir, for n in range(0, (len(batch_vec) - 1)): resp_batch = respfile.iloc[:, (batch_vec[n]): batch_vec[n + 1]] testresp_batch = testrespfile.iloc[:, (batch_vec[n]): batch_vec[n + - 1]] + 1]] os.chdir(processing_dir) resp = str('resp_batch_' + str(n+1)) testresp = str('testresp_batch_' + str(n+1)) @@ -424,7 +423,7 @@ def split_nm(processing_dir, if not os.path.exists(processing_dir + batch): os.makedirs(processing_dir + batch) os.makedirs(processing_dir + batch + '/Models/') - if (binary==False): + if (binary == False): fileio.save_pd(resp_batch, processing_dir + batch + '/' + resp + '.txt') @@ -435,7 +434,7 @@ def split_nm(processing_dir, resp_batch.to_pickle(processing_dir + batch + '/' + resp + '.pkl', protocol=PICKLE_PROTOCOL) testresp_batch.to_pickle(processing_dir + batch + '/' + - testresp + '.pkl', + testresp + '.pkl', protocol=PICKLE_PROTOCOL) @@ -446,7 +445,6 @@ def collect_nm(processing_dir, binary=False, batch_size=None, outputsuffix='_estimate'): - '''Function to checks and collects all batches. Basic usage:: @@ -473,17 +471,17 @@ def collect_nm(processing_dir, # detect number of subjects, batches, hyperparameters and CV batches = glob.glob(processing_dir + 'batch_*/') - + count = 0 batch_fail = [] - - if (func!='fit' and func!='extend' and func!='merge' and func!='tune'): + + if (func != 'fit' and func != 'extend' and func != 'merge' and func != 'tune'): file_example = [] - # TODO: Collect_nm only depends on yhat, thus does not work when no - # prediction is made (when test cov is not specified). + # TODO: Collect_nm only depends on yhat, thus does not work when no + # prediction is made (when test cov is not specified). for batch in batches: if file_example == []: - file_example = glob.glob(batch + 'yhat' + outputsuffix + file_example = glob.glob(batch + 'yhat' + outputsuffix + file_extentions) else: break @@ -493,10 +491,11 @@ def collect_nm(processing_dir, file_example = pd.read_pickle(file_example[0]) numsubjects = file_example.shape[0] try: - batch_size = file_example.shape[1] # doesn't exist if size=1, and txt file + # doesn't exist if size=1, and txt file + batch_size = file_example.shape[1] except: batch_size = 1 - + # artificially creates files for batches that were not executed batch_dirs = glob.glob(processing_dir + 'batch_*/') batch_dirs = fileio.sort_nicely(batch_dirs) @@ -511,124 +510,127 @@ def collect_nm(processing_dir, pRho = np.ones(batch_size) pRho = pRho.transpose() pRho = pd.Series(pRho) - fileio.save(pRho, batch + 'pRho' + outputsuffix + + fileio.save(pRho, batch + 'pRho' + outputsuffix + file_extentions) - + Rho = np.zeros(batch_size) Rho = Rho.transpose() Rho = pd.Series(Rho) - fileio.save(Rho, batch + 'Rho' + outputsuffix + + fileio.save(Rho, batch + 'Rho' + outputsuffix + file_extentions) - + rmse = np.zeros(batch_size) rmse = rmse.transpose() rmse = pd.Series(rmse) - fileio.save(rmse, batch + 'RMSE' + outputsuffix + + fileio.save(rmse, batch + 'RMSE' + outputsuffix + file_extentions) - + smse = np.zeros(batch_size) smse = smse.transpose() smse = pd.Series(smse) - fileio.save(smse, batch + 'SMSE' + outputsuffix + + fileio.save(smse, batch + 'SMSE' + outputsuffix + file_extentions) - + expv = np.zeros(batch_size) expv = expv.transpose() expv = pd.Series(expv) - fileio.save(expv, batch + 'EXPV' + outputsuffix + + fileio.save(expv, batch + 'EXPV' + outputsuffix + file_extentions) - + msll = np.zeros(batch_size) msll = msll.transpose() msll = pd.Series(msll) - fileio.save(msll, batch + 'MSLL' + outputsuffix + + fileio.save(msll, batch + 'MSLL' + outputsuffix + file_extentions) - + yhat = np.zeros([numsubjects, batch_size]) yhat = pd.DataFrame(yhat) - fileio.save(yhat, batch + 'yhat' + outputsuffix + + fileio.save(yhat, batch + 'yhat' + outputsuffix + file_extentions) - + ys2 = np.zeros([numsubjects, batch_size]) ys2 = pd.DataFrame(ys2) - fileio.save(ys2, batch + 'ys2' + outputsuffix + + fileio.save(ys2, batch + 'ys2' + outputsuffix + file_extentions) - + Z = np.zeros([numsubjects, batch_size]) Z = pd.DataFrame(Z) - fileio.save(Z, batch + 'Z' + outputsuffix + + fileio.save(Z, batch + 'Z' + outputsuffix + file_extentions) - + nll = np.zeros(batch_size) nll = nll.transpose() nll = pd.Series(nll) - fileio.save(nll, batch + 'NLL' + outputsuffix + + fileio.save(nll, batch + 'NLL' + outputsuffix + file_extentions) - + bic = np.zeros(batch_size) bic = bic.transpose() bic = pd.Series(bic) - fileio.save(bic, batch + 'BIC' + outputsuffix + + fileio.save(bic, batch + 'BIC' + outputsuffix + file_extentions) - + if not os.path.isdir(batch + 'Models'): os.mkdir('Models') - - - else: # if more than 10% of yhat is nan then it is a failed batch + + else: # if more than 10% of yhat is nan then it is a failed batch yhat = fileio.load(filepath[0]) - if np.count_nonzero(~np.isnan(yhat))/(np.prod(yhat.shape))<0.9: + if np.count_nonzero(~np.isnan(yhat))/(np.prod(yhat.shape)) < 0.9: count = count+1 batch1 = glob.glob(batch + '/' + job_name + '*.sh') - print('More than 10% nans in '+ batch1[0]) + print('More than 10% nans in ' + batch1[0]) batch_fail.append(batch1) - + else: batch_dirs = glob.glob(processing_dir + 'batch_*/') batch_dirs = fileio.sort_nicely(batch_dirs) for batch in batch_dirs: - filepath = glob.glob(batch + 'Models/' + 'NM_' + '*' + outputsuffix + filepath = glob.glob(batch + 'Models/' + 'NM_' + '*' + outputsuffix + '*') if len(filepath) < batch_size: count = count+1 batch1 = glob.glob(batch + '/' + job_name + '*.sh') print(batch1) batch_fail.append(batch1) - + # combines all output files across batches if collect is True: - pRho_filenames = glob.glob(processing_dir + 'batch_*/' + 'pRho' + + pRho_filenames = glob.glob(processing_dir + 'batch_*/' + 'pRho' + outputsuffix + '*') if pRho_filenames: pRho_filenames = fileio.sort_nicely(pRho_filenames) pRho_dfs = [] for pRho_filename in pRho_filenames: - if batch_size == 1 and binary is False: #if batch size = 1 and .txt file - pRho_dfs.append(pd.DataFrame(fileio.load(pRho_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array - else: + if batch_size == 1 and binary is False: # if batch size = 1 and .txt file + # from 0d (scalar) to 1d-array + pRho_dfs.append(pd.DataFrame( + fileio.load(pRho_filename)[np.newaxis,])) + else: pRho_dfs.append(pd.DataFrame(fileio.load(pRho_filename))) pRho_dfs = pd.concat(pRho_dfs, ignore_index=True, axis=0) fileio.save(pRho_dfs, processing_dir + 'pRho' + outputsuffix + file_extentions) del pRho_dfs - Rho_filenames = glob.glob(processing_dir + 'batch_*/' + 'Rho' + - outputsuffix + '*') + Rho_filenames = glob.glob(processing_dir + 'batch_*/' + 'Rho' + + outputsuffix + '*') if Rho_filenames: Rho_filenames = fileio.sort_nicely(Rho_filenames) Rho_dfs = [] for Rho_filename in Rho_filenames: - if batch_size == 1 and binary is False: #if batch size = 1 and .txt file - Rho_dfs.append(pd.DataFrame(fileio.load(Rho_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array - else: + if batch_size == 1 and binary is False: # if batch size = 1 and .txt file + # from 0d (scalar) to 1d-array + Rho_dfs.append(pd.DataFrame( + fileio.load(Rho_filename)[np.newaxis,])) + else: Rho_dfs.append(pd.DataFrame(fileio.load(Rho_filename))) Rho_dfs = pd.concat(Rho_dfs, ignore_index=True, axis=0) fileio.save(Rho_dfs, processing_dir + 'Rho' + outputsuffix + file_extentions) del Rho_dfs - Z_filenames = glob.glob(processing_dir + 'batch_*/' + 'Z' + - outputsuffix + '*') + Z_filenames = glob.glob(processing_dir + 'batch_*/' + 'Z' + + outputsuffix + '*') if Z_filenames: Z_filenames = fileio.sort_nicely(Z_filenames) Z_dfs = [] @@ -638,8 +640,8 @@ def collect_nm(processing_dir, fileio.save(Z_dfs, processing_dir + 'Z' + outputsuffix + file_extentions) del Z_dfs - - yhat_filenames = glob.glob(processing_dir + 'batch_*/' + 'yhat' + + + yhat_filenames = glob.glob(processing_dir + 'batch_*/' + 'yhat' + outputsuffix + '*') if yhat_filenames: yhat_filenames = fileio.sort_nicely(yhat_filenames) @@ -651,8 +653,8 @@ def collect_nm(processing_dir, file_extentions) del yhat_dfs - ys2_filenames = glob.glob(processing_dir + 'batch_*/' + 'ys2' + - outputsuffix + '*') + ys2_filenames = glob.glob(processing_dir + 'batch_*/' + 'ys2' + + outputsuffix + '*') if ys2_filenames: ys2_filenames = fileio.sort_nicely(ys2_filenames) ys2_dfs = [] @@ -663,74 +665,84 @@ def collect_nm(processing_dir, file_extentions) del ys2_dfs - rmse_filenames = glob.glob(processing_dir + 'batch_*/' + 'RMSE' + + rmse_filenames = glob.glob(processing_dir + 'batch_*/' + 'RMSE' + outputsuffix + '*') if rmse_filenames: rmse_filenames = fileio.sort_nicely(rmse_filenames) rmse_dfs = [] for rmse_filename in rmse_filenames: - if batch_size == 1 and binary is False: #if batch size = 1 and .txt file - rmse_dfs.append(pd.DataFrame(fileio.load(rmse_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array - else: + if batch_size == 1 and binary is False: # if batch size = 1 and .txt file + # from 0d (scalar) to 1d-array + rmse_dfs.append(pd.DataFrame( + fileio.load(rmse_filename)[np.newaxis,])) + else: rmse_dfs.append(pd.DataFrame(fileio.load(rmse_filename))) rmse_dfs = pd.concat(rmse_dfs, ignore_index=True, axis=0) fileio.save(rmse_dfs, processing_dir + 'RMSE' + outputsuffix + file_extentions) del rmse_dfs - smse_filenames = glob.glob(processing_dir + 'batch_*/' + 'SMSE' + + smse_filenames = glob.glob(processing_dir + 'batch_*/' + 'SMSE' + outputsuffix + '*') if smse_filenames: smse_filenames = fileio.sort_nicely(smse_filenames) smse_dfs = [] for smse_filename in smse_filenames: - if batch_size == 1 and binary is False: #if batch size = 1 and .txt file - smse_dfs.append(pd.DataFrame(fileio.load(smse_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array + if batch_size == 1 and binary is False: # if batch size = 1 and .txt file + # from 0d (scalar) to 1d-array + smse_dfs.append(pd.DataFrame( + fileio.load(smse_filename)[np.newaxis,])) else: smse_dfs.append(pd.DataFrame(fileio.load(smse_filename))) smse_dfs = pd.concat(smse_dfs, ignore_index=True, axis=0) fileio.save(smse_dfs, processing_dir + 'SMSE' + outputsuffix + file_extentions) del smse_dfs - - expv_filenames = glob.glob(processing_dir + 'batch_*/' + 'EXPV' + + + expv_filenames = glob.glob(processing_dir + 'batch_*/' + 'EXPV' + outputsuffix + '*') if expv_filenames: expv_filenames = fileio.sort_nicely(expv_filenames) expv_dfs = [] for expv_filename in expv_filenames: - if batch_size == 1 and binary is False: #if batch size = 1 and .txt file - expv_dfs.append(pd.DataFrame(fileio.load(expv_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array + if batch_size == 1 and binary is False: # if batch size = 1 and .txt file + # from 0d (scalar) to 1d-array + expv_dfs.append(pd.DataFrame( + fileio.load(expv_filename)[np.newaxis,])) else: expv_dfs.append(pd.DataFrame(fileio.load(expv_filename))) expv_dfs = pd.concat(expv_dfs, ignore_index=True, axis=0) fileio.save(expv_dfs, processing_dir + 'EXPV' + outputsuffix + file_extentions) del expv_dfs - - msll_filenames = glob.glob(processing_dir + 'batch_*/' + 'MSLL' + + + msll_filenames = glob.glob(processing_dir + 'batch_*/' + 'MSLL' + outputsuffix + '*') if msll_filenames: msll_filenames = fileio.sort_nicely(msll_filenames) msll_dfs = [] for msll_filename in msll_filenames: - if batch_size == 1 and binary is False: #if batch size = 1 and .txt file - msll_dfs.append(pd.DataFrame(fileio.load(msll_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array + if batch_size == 1 and binary is False: # if batch size = 1 and .txt file + # from 0d (scalar) to 1d-array + msll_dfs.append(pd.DataFrame( + fileio.load(msll_filename)[np.newaxis,])) else: msll_dfs.append(pd.DataFrame(fileio.load(msll_filename))) msll_dfs = pd.concat(msll_dfs, ignore_index=True, axis=0) fileio.save(msll_dfs, processing_dir + 'MSLL' + outputsuffix + file_extentions) del msll_dfs - + nll_filenames = glob.glob(processing_dir + 'batch_*/' + 'NLL' + outputsuffix + '*') if nll_filenames: nll_filenames = fileio.sort_nicely(nll_filenames) nll_dfs = [] for nll_filename in nll_filenames: - if batch_size == 1 and binary is False: #if batch size = 1 and .txt file - nll_dfs.append(pd.DataFrame(fileio.load(nll_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array + if batch_size == 1 and binary is False: # if batch size = 1 and .txt file + # from 0d (scalar) to 1d-array + nll_dfs.append(pd.DataFrame( + fileio.load(nll_filename)[np.newaxis,])) else: nll_dfs.append(pd.DataFrame(fileio.load(nll_filename))) nll_dfs = pd.concat(nll_dfs, ignore_index=True, axis=0) @@ -744,21 +756,23 @@ def collect_nm(processing_dir, bic_filenames = fileio.sort_nicely(bic_filenames) bic_dfs = [] for bic_filename in bic_filenames: - if batch_size == 1 and binary is False: #if batch size = 1 and .txt file - bic_dfs.append(pd.DataFrame(fileio.load(bic_filename)[np.newaxis,])) # from 0d (scalar) to 1d-array + if batch_size == 1 and binary is False: # if batch size = 1 and .txt file + # from 0d (scalar) to 1d-array + bic_dfs.append(pd.DataFrame( + fileio.load(bic_filename)[np.newaxis,])) else: bic_dfs.append(pd.DataFrame(fileio.load(bic_filename))) bic_dfs = pd.concat(bic_dfs, ignore_index=True, axis=0) fileio.save(bic_dfs, processing_dir + 'BIC' + outputsuffix + file_extentions) del bic_dfs - - if (func!='predict' and func!='extend' and func!='merge' and func!='tune'): + + if (func != 'predict' and func != 'extend' and func != 'merge' and func != 'tune'): if not os.path.isdir(processing_dir + 'Models') and \ os.path.exists(os.path.join(batches[0], 'Models')): os.mkdir(processing_dir + 'Models') - - meta_filenames = glob.glob(processing_dir + 'batch_*/Models/' + + + meta_filenames = glob.glob(processing_dir + 'batch_*/Models/' + 'meta_data.md') mY = [] sY = [] @@ -768,32 +782,32 @@ def collect_nm(processing_dir, meta_filenames = fileio.sort_nicely(meta_filenames) with open(meta_filenames[0], 'rb') as file: meta_data = pickle.load(file) - + for meta_filename in meta_filenames: with open(meta_filename, 'rb') as file: meta_data = pickle.load(file) mY.append(meta_data['mean_resp']) sY.append(meta_data['std_resp']) - if meta_data['inscaler'] in ['standardize', 'minmax', - 'robminmax']: + if meta_data['inscaler'] in ['standardize', 'minmax', + 'robminmax']: X_scalers.append(meta_data['scaler_cov']) - if meta_data['outscaler'] in ['standardize', 'minmax', - 'robminmax']: + if meta_data['outscaler'] in ['standardize', 'minmax', + 'robminmax']: Y_scalers.append(meta_data['scaler_resp']) - meta_data['mean_resp'] = np.squeeze(np.column_stack(mY)) + meta_data['mean_resp'] = np.squeeze(np.column_stack(mY)) meta_data['std_resp'] = np.squeeze(np.column_stack(sY)) - meta_data['scaler_cov'] = X_scalers + meta_data['scaler_cov'] = X_scalers meta_data['scaler_resp'] = Y_scalers - - with open(os.path.join(processing_dir, 'Models', + + with open(os.path.join(processing_dir, 'Models', 'meta_data.md'), 'wb') as file: pickle.dump(meta_data, file, protocol=PICKLE_PROTOCOL) - + batch_dirs = glob.glob(processing_dir + 'batch_*/') if batch_dirs: batch_dirs = fileio.sort_nicely(batch_dirs) for b, batch_dir in enumerate(batch_dirs): - src_files = glob.glob(batch_dir + 'Models/NM*' + + src_files = glob.glob(batch_dir + 'Models/NM*' + outputsuffix + '.pkl') if src_files: src_files = fileio.sort_nicely(src_files) @@ -803,30 +817,31 @@ def collect_nm(processing_dir, n = file_name.split('_') n[-2] = str(b * batch_size + f) n = '_'.join(n) - shutil.copy(full_file_name, processing_dir + + shutil.copy(full_file_name, processing_dir + 'Models/' + n) - elif func=='fit': + elif func == 'fit': count = count+1 batch1 = glob.glob(batch_dir + '/' + job_name + '*.sh') print('Failed batch: ' + batch1[0]) batch_fail.append(batch1) - + # list batches that were not executed print('Number of batches that failed:' + str(count)) batch_fail_df = pd.DataFrame(batch_fail) if file_extentions == '.txt': - fileio.save_pd(batch_fail_df, processing_dir + 'failed_batches'+ - file_extentions) + fileio.save_pd(batch_fail_df, processing_dir + 'failed_batches' + + file_extentions) else: fileio.save(batch_fail_df, processing_dir + - 'failed_batches' + - file_extentions) + 'failed_batches' + + file_extentions) if not batch_fail: return True else: return False + def delete_nm(processing_dir, binary=False): '''This function deletes all processing for normative modelling and just keeps the combined output. @@ -840,7 +855,7 @@ def delete_nm(processing_dir, written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. ''' - + if binary: file_extentions = '.pkl' else: @@ -863,7 +878,6 @@ def bashwrap_nm(processing_dir, respfile_path, func='estimate', **kwargs): - ''' This function wraps normative modelling into a bash script to run it on a torque cluster system. @@ -887,9 +901,9 @@ def bashwrap_nm(processing_dir, written by (primarily) T Wolfers, (adapted) S Rutherford. ''' - - # here we use pop not get to remove the arguments as they used - cv_folds = kwargs.pop('cv_folds',None) + + # here we use pop not get to remove the arguments as they used + cv_folds = kwargs.pop('cv_folds', None) testcovfile_path = kwargs.pop('testcovfile_path', None) testrespfile_path = kwargs.pop('testrespfile_path', None) alg = kwargs.pop('alg', None) @@ -912,34 +926,34 @@ def bashwrap_nm(processing_dir, covfile_path + ' -t ' + testcovfile_path + ' -f ' + func] elif cv_folds is not None: job_call = [python_path + ' ' + normative_path + ' -c ' + - covfile_path + ' -k ' + str(cv_folds) + ' -f ' + func] + covfile_path + ' -k ' + str(cv_folds) + ' -f ' + func] elif func != 'estimate': job_call = [python_path + ' ' + normative_path + ' -c ' + - covfile_path + ' -f ' + func] + covfile_path + ' -f ' + func] else: - raise(ValueError, """For 'estimate' function either testcov or cvfold + raise (ValueError, """For 'estimate' function either testcov or cvfold must be specified.""") - + # add algorithm-specific parameters if alg is not None: job_call = [job_call[0] + ' -a ' + alg] if configparam is not None: job_call = [job_call[0] + ' -x ' + str(configparam)] - + # add standardization flag if it is false # if not standardize: # job_call = [job_call[0] + ' -s'] - + # add responses file job_call = [job_call[0] + ' ' + respfile_path] - - # add in optional arguments. + + # add in optional arguments. for k in kwargs: job_call = [job_call[0] + ' ' + k + '=' + str(kwargs[k])] # writes bash file into processing dir with open(processing_dir+job_name, 'w') as bash_file: - bash_file.writelines(bash_environment + output_changedir + \ + bash_file.writelines(bash_environment + output_changedir + job_call + ["\n"]) # changes permissoins for bash.sh file @@ -950,9 +964,8 @@ def qsub_nm(job_path, log_path, memory, duration): - '''This function submits a job.sh scipt to the torque custer using the qsub command. - + Basic usage:: @@ -966,20 +979,21 @@ def qsub_nm(job_path, written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. ''' - + # created qsub command if log_path is None: qsub_call = ['echo ' + job_path + ' | qsub -N ' + job_path + ' -l ' + 'procs=1' + ',mem=' + memory + ',walltime=' + duration] else: qsub_call = ['echo ' + job_path + ' | qsub -N ' + job_path + - ' -l ' + 'procs=1' + ',mem=' + memory + ',walltime=' + + ' -l ' + 'procs=1' + ',mem=' + memory + ',walltime=' + duration + ' -o ' + log_path + ' -e ' + log_path] # submits job to cluster - #call(qsub_call, shell=True) - job_id = check_output(qsub_call, shell=True).decode(sys.stdout.encoding).replace("\n", "") - + # call(qsub_call, shell=True) + job_id = check_output(qsub_call, shell=True).decode( + sys.stdout.encoding).replace("\n", "") + return job_id @@ -987,7 +1001,7 @@ def rerun_nm(processing_dir, log_path, memory, duration, - binary=False, + binary=False, interactive=False): '''This function reruns all failed batched in processing_dir after collect_nm has identified the failed batches. Basic usage:: @@ -1000,40 +1014,39 @@ def rerun_nm(processing_dir, written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. ''' - + job_ids = [] - if binary: file_extentions = '.pkl' failed_batches = fileio.load(processing_dir + - 'failed_batches' + file_extentions) + 'failed_batches' + file_extentions) shape = failed_batches.shape for n in range(0, shape[0]): jobpath = failed_batches[n, 0] print(jobpath) job_id = qsub_nm(job_path=jobpath, - log_path=log_path, - memory=memory, - duration=duration) + log_path=log_path, + memory=memory, + duration=duration) job_ids.append(job_id) else: file_extentions = '.txt' failed_batches = fileio.load_pd(processing_dir + - 'failed_batches' + file_extentions) + 'failed_batches' + file_extentions) shape = failed_batches.shape for n in range(0, shape[0]): jobpath = failed_batches.iloc[n, 0] print(jobpath) job_id = qsub_nm(job_path=jobpath, - log_path=log_path, - memory=memory, - duration=duration) + log_path=log_path, + memory=memory, + duration=duration) job_ids.append(job_id) - - if interactive: + + if interactive: check_jobs(job_ids, delay=60) - + # COPY the rotines above here and aadapt those to your cluster # bashwarp_nm; qsub_nm; rerun_nm @@ -1048,7 +1061,6 @@ def sbatchwrap_nm(processing_dir, duration, func='estimate', **kwargs): - '''This function wraps normative modelling into a bash script to run it on a torque cluster system. @@ -1072,33 +1084,33 @@ def sbatchwrap_nm(processing_dir, written by (primarily) T Wolfers, (adapted) S Rutherford ''' - - # here we use pop not get to remove the arguments as they used - cv_folds = kwargs.pop('cv_folds',None) + + # here we use pop not get to remove the arguments as they used + cv_folds = kwargs.pop('cv_folds', None) testcovfile_path = kwargs.pop('testcovfile_path', None) testrespfile_path = kwargs.pop('testrespfile_path', None) alg = kwargs.pop('alg', None) configparam = kwargs.pop('configparam', None) - + # change to processing dir os.chdir(processing_dir) output_changedir = ['cd ' + processing_dir + '\n'] - sbatch_init='#!/bin/bash\n' - sbatch_jobname='#SBATCH --job-name=' + processing_dir + '\n' - sbatch_account='#SBATCH --account=p33_norment\n' - sbatch_nodes='#SBATCH --nodes=1\n' - sbatch_tasks='#SBATCH --ntasks=1\n' - sbatch_time='#SBATCH --time=' + str(duration) + '\n' - sbatch_memory='#SBATCH --mem-per-cpu=' + str(memory) + '\n' - sbatch_module='module purge\n' - sbatch_anaconda='module load anaconda3\n' - sbatch_exit='set -o errexit\n' - - #echo -n "This script is running on " - #hostname - - bash_environment = [sbatch_init + + sbatch_init = '#!/bin/bash\n' + sbatch_jobname = '#SBATCH --job-name=' + processing_dir + '\n' + sbatch_account = '#SBATCH --account=p33_norment\n' + sbatch_nodes = '#SBATCH --nodes=1\n' + sbatch_tasks = '#SBATCH --ntasks=1\n' + sbatch_time = '#SBATCH --time=' + str(duration) + '\n' + sbatch_memory = '#SBATCH --mem-per-cpu=' + str(memory) + '\n' + sbatch_module = 'module purge\n' + sbatch_anaconda = 'module load anaconda3\n' + sbatch_exit = 'set -o errexit\n' + + # echo -n "This script is running on " + # hostname + + bash_environment = [sbatch_init + sbatch_jobname + sbatch_account + sbatch_nodes + @@ -1117,42 +1129,42 @@ def sbatchwrap_nm(processing_dir, covfile_path + ' -t ' + testcovfile_path + ' -f ' + func] elif cv_folds is not None: job_call = [python_path + ' ' + normative_path + ' -c ' + - covfile_path + ' -k ' + str(cv_folds) + ' -f ' + func] + covfile_path + ' -k ' + str(cv_folds) + ' -f ' + func] elif func != 'estimate': job_call = [python_path + ' ' + normative_path + ' -c ' + - covfile_path + ' -f ' + func] + covfile_path + ' -f ' + func] else: - raise(ValueError, """For 'estimate' function either testcov or cvfold + raise (ValueError, """For 'estimate' function either testcov or cvfold must be specified.""") - + # add algorithm-specific parameters if alg is not None: job_call = [job_call[0] + ' -a ' + alg] if configparam is not None: job_call = [job_call[0] + ' -x ' + str(configparam)] - + # add standardization flag if it is false # if not standardize: # job_call = [job_call[0] + ' -s'] - + # add responses file job_call = [job_call[0] + ' ' + respfile_path] - - # add in optional arguments. + + # add in optional arguments. for k in kwargs: job_call = [job_call[0] + ' ' + k + '=' + kwargs[k]] # writes bash file into processing dir with open(processing_dir+job_name, 'w') as bash_file: - bash_file.writelines(bash_environment + output_changedir + \ + bash_file.writelines(bash_environment + output_changedir + job_call + ["\n"] + [sbatch_exit]) # changes permissoins for bash.sh file os.chmod(processing_dir + job_name, 0o770) + def sbatch_nm(job_path, log_path): - '''This function submits a job.sh scipt to the torque custer using the qsub command. @@ -1173,17 +1185,17 @@ def sbatch_nm(job_path, # submits job to cluster call(sbatch_call, shell=True) - + + def sbatchrerun_nm(processing_dir, - memory, - duration, - new_memory=False, - new_duration=False, - binary=False, - **kwargs): - + memory, + duration, + new_memory=False, + new_duration=False, + binary=False, + **kwargs): '''This function reruns all failed batched in processing_dir after collect_nm has identified he failed batches. - + Basic usage:: rerun_nm(processing_dir, memory, duration) @@ -1195,14 +1207,15 @@ def sbatchrerun_nm(processing_dir, :param new_duration: If you want to change the duration you have to indicate it here. :outputs: Re-runs failed batches. - + written by (primarily) T Wolfers, (adapted) S Rutherford. ''' log_path = kwargs.pop('log_path', None) - + if binary: file_extentions = '.pkl' - failed_batches = fileio.load(processing_dir + 'failed_batches' + file_extentions) + failed_batches = fileio.load( + processing_dir + 'failed_batches' + file_extentions) shape = failed_batches.shape for n in range(0, shape[0]): jobpath = failed_batches[n, 0] @@ -1219,7 +1232,8 @@ def sbatchrerun_nm(processing_dir, else: file_extentions = '.txt' - failed_batches = fileio.load_pd(processing_dir + 'failed_batches' + file_extentions) + failed_batches = fileio.load_pd( + processing_dir + 'failed_batches' + file_extentions) shape = failed_batches.shape for n in range(0, shape[0]): jobpath = failed_batches.iloc[n, 0] @@ -1239,11 +1253,11 @@ def sbatchrerun_nm(processing_dir, def retrieve_jobs(): """ A utility function to retrieve task status from the outputs of qstat. - + :return: a dictionary of jobs. """ - + output = check_output('qstat', shell=True).decode(sys.stdout.encoding) output = output.split('\n') jobs = dict() @@ -1253,20 +1267,20 @@ def retrieve_jobs(): jobs[Job_ID]['name'] = Job_Name jobs[Job_ID]['walltime'] = Wall_Time jobs[Job_ID]['status'] = Status - + return jobs - + def check_job_status(jobs): """ A utility function to count the tasks with different status. - + :param jobs: List of job ids. :return: returns the number of taks athat are queued, running, completed etc """ running_jobs = retrieve_jobs() - + r = 0 c = 0 q = 0 @@ -1281,31 +1295,29 @@ def check_job_status(jobs): r += 1 else: u += 1 - except: # probably meanwhile the job is finished. - c += 1 + except: # probably meanwhile the job is finished. + c += 1 continue - - print('Total Jobs:%d, Queued:%d, Running:%d, Completed:%d, Unknown:%d' - %(len(jobs), q, r, c, u)) - return q,r,c,u - + + print('Total Jobs:%d, Queued:%d, Running:%d, Completed:%d, Unknown:%d' + % (len(jobs), q, r, c, u)) + return q, r, c, u + def check_jobs(jobs, delay=60): """ A utility function for chacking the status of submitted jobs. - + :param jobs: list of job ids. :param delay: the delay (in sec) between two consequative checks, defaults to 60. """ - + n = len(jobs) - - while(True): - q,r,c,u = check_job_status(jobs) + + while (True): + q, r, c, u = check_job_status(jobs) if c == n: print('All jobs are completed!') break time.sleep(delay) - - diff --git a/pcntoolkit/trendsurf.py b/pcntoolkit/trendsurf.py index 3695379d..e7860e37 100644 --- a/pcntoolkit/trendsurf.py +++ b/pcntoolkit/trendsurf.py @@ -30,7 +30,6 @@ from model.bayesreg import BLR - def load_data(datafile, maskfile=None): """ Load data from disk @@ -184,7 +183,7 @@ def get_args(*args): maskfile = os.path.join(wdir, args.maskfile) basis = args.basis if args.covfile is not None: - raise(NotImplementedError, "Covariates not implemented yet.") + raise (NotImplementedError, "Covariates not implemented yet.") return filename, maskfile, basis, args.a, args.o @@ -222,10 +221,10 @@ def estimate(filename, maskfile, basis, ard=False, outputall=False, * explainedvar - explained variance * rmse - standardised mean squared error """ - + # parse arguments optim = kwargs.get('optimizer', 'powell') - + # load data print("Processing data in", filename) Y, X, mask = load_data(filename, maskfile) @@ -301,12 +300,14 @@ def estimate(filename, maskfile, basis, ard=False, outputall=False, out.append(bs2) return out + def main(*args): np.seterr(invalid='ignore') filename, maskfile, basis, ard, outputall = get_args(args) estimate(filename, maskfile, basis, ard, outputall) + # For running from the command line: if __name__ == "__main__": main(sys.argv[1:]) diff --git a/pcntoolkit/util/__init__.py b/pcntoolkit/util/__init__.py index 9f9161bf..eb018c3f 100644 --- a/pcntoolkit/util/__init__.py +++ b/pcntoolkit/util/__init__.py @@ -1 +1 @@ -from . import utils \ No newline at end of file +from . import utils diff --git a/pcntoolkit/util/hbr_utils.py b/pcntoolkit/util/hbr_utils.py index 89213100..13a11cb2 100644 --- a/pcntoolkit/util/hbr_utils.py +++ b/pcntoolkit/util/hbr_utils.py @@ -15,31 +15,32 @@ @author: augub """ + def MCMC_estimate(f, trace): """Get an MCMC estimate of f given a trace""" out = np.zeros_like(f(trace.point(0))) - n=0 + n = 0 for p in trace.points(): out += f(p) - n+=1 + n += 1 return out/n def get_MCMC_zscores(X, Y, Z, model): """Get an MCMC estimate of the z-scores of Y""" def f(sample): - return get_single_zscores(X, Y, Z, model,sample) + return get_single_zscores(X, Y, Z, model, sample) return MCMC_estimate(f, model.hbr.trace) def get_single_zscores(X, Y, Z, model, sample): """Get the z-scores of y, given clinical covariates and a model""" likelihood = model.configs['likelihood'] - params = forward(X,Z,model,sample) - return z_score(Y, params, likelihood = likelihood) - + params = forward(X, Z, model, sample) + return z_score(Y, params, likelihood=likelihood) + -def z_score(Y, params, likelihood = "Normal"): +def z_score(Y, params, likelihood="Normal"): """Get the z-scores of Y, given likelihood parameters""" if likelihood.startswith('SHASH'): mu = params['mu'] @@ -71,13 +72,14 @@ def get_MCMC_quantiles(synthetic_X, z_scores, model, be): """This does not use the get_single_quantiles function, for memory efficiency""" resolution = synthetic_X.shape[0] synthetic_X_transformed = model.hbr.transform_X(synthetic_X) - be = np.reshape(np.array(be),(1,-1)) - synthetic_Z = np.repeat(be, resolution, axis = 0) - z_scores = np.reshape(np.array(z_scores),(1,-1)) + be = np.reshape(np.array(be), (1, -1)) + synthetic_Z = np.repeat(be, resolution, axis=0) + z_scores = np.reshape(np.array(z_scores), (1, -1)) zs = np.repeat(z_scores, resolution, axis=0) + def f(sample): - params = forward(synthetic_X_transformed,synthetic_Z, model,sample) - q = quantile(zs, params, likelihood = model.configs['likelihood']) + params = forward(synthetic_X_transformed, synthetic_Z, model, sample) + q = quantile(zs, params, likelihood=model.configs['likelihood']) return q out = MCMC_estimate(f, model.hbr.trace) return out @@ -87,16 +89,16 @@ def get_single_quantiles(synthetic_X, z_scores, model, be, sample): """Get the quantiles within a given range of covariates, given a model""" resolution = synthetic_X.shape[0] synthetic_X_transformed = model.hbr.transform_X(synthetic_X) - be = np.reshape(np.array(be),(1,-1)) - synthetic_Z = np.repeat(be, resolution, axis = 0) - z_scores = np.reshape(np.array(z_scores),(1,-1)) + be = np.reshape(np.array(be), (1, -1)) + synthetic_Z = np.repeat(be, resolution, axis=0) + z_scores = np.reshape(np.array(z_scores), (1, -1)) zs = np.repeat(z_scores, resolution, axis=0) - params = forward(synthetic_X_transformed,synthetic_Z, model,sample) - q = quantile(zs, params, likelihood = model.configs['likelihood']) + params = forward(synthetic_X_transformed, synthetic_Z, model, sample) + q = quantile(zs, params, likelihood=model.configs['likelihood']) return q -def quantile(zs, params, likelihood = "Normal"): +def quantile(zs, params, likelihood="Normal"): """Get the zs'th quantiles given likelihood parameters""" if likelihood.startswith('SHASH'): mu = params['mu'] @@ -104,15 +106,15 @@ def quantile(zs, params, likelihood = "Normal"): epsilon = params['epsilon'] delta = params['delta'] if likelihood == "SHASHo": - quantiles = S_inv(zs,epsilon,delta)*sigma + mu + quantiles = S_inv(zs, epsilon, delta)*sigma + mu elif likelihood == "SHASHo2": sigma_d = sigma/delta - quantiles = S_inv(zs,epsilon,delta)*sigma_d + mu + quantiles = S_inv(zs, epsilon, delta)*sigma_d + mu elif likelihood == "SHASHb": true_mu = m(epsilon, delta, 1) true_sigma = np.sqrt((m(epsilon, delta, 2) - true_mu ** 2)) - SHASH_c = ((S_inv(zs,epsilon,delta)-true_mu)/true_sigma) - quantiles = SHASH_c *sigma + mu + SHASH_c = ((S_inv(zs, epsilon, delta)-true_mu)/true_sigma) + quantiles = SHASH_c * sigma + mu if likelihood == 'Normal': quantiles = zs*params['sigma'] + params['mu'] else: @@ -122,11 +124,11 @@ def quantile(zs, params, likelihood = "Normal"): def single_parameter_forward(X, Z, model, sample, p_name): """Get a likelihood paramameter given covariates, batch-effects and model parameters""" - outs = np.zeros(X.shape[0])[:,None] - all_bes = np.unique(Z,axis=0) + outs = np.zeros(X.shape[0])[:, None] + all_bes = np.unique(Z, axis=0) for be in all_bes: bet = tuple(be) - idx = (Z==be).all(1) + idx = (Z == be).all(1) if model.configs[f"linear_{p_name}"]: if model.configs[f'random_slope_{p_name}']: slope_be = sample[f"slope_{p_name}"][bet] @@ -137,13 +139,13 @@ def single_parameter_forward(X, Z, model, sample, p_name): else: intercept_be = sample[f"intercept_{p_name}"] - out = (X[np.squeeze(idx),:]@slope_be)[:,None] + intercept_be - outs[np.squeeze(idx),:] = out + out = (X[np.squeeze(idx), :]@slope_be)[:, None] + intercept_be + outs[np.squeeze(idx), :] = out else: if model.configs[f'random_{p_name}']: - outs[np.squeeze(idx),:] = sample[p_name][bet] + outs[np.squeeze(idx), :] = sample[p_name][bet] else: - outs[np.squeeze(idx),:] = sample[p_name] + outs[np.squeeze(idx), :] = sample[p_name] return outs @@ -151,12 +153,13 @@ def single_parameter_forward(X, Z, model, sample, p_name): def forward(X, Z, model, sample): """Get all likelihood paramameters given covariates and batch-effects and model parameters""" # TODO think if this is the correct spot for this - mapfuncs={'sigma': lambda x: np.log(1+np.exp(x)), 'delta':lambda x :np.log(1+np.exp(x)) + 0.3} + mapfuncs = {'sigma': lambda x: np.log( + 1+np.exp(x)), 'delta': lambda x: np.log(1+np.exp(x)) + 0.3} likelihood = model.configs['likelihood'] if likelihood == 'Normal': - parameter_list = ['mu','sigma'] + parameter_list = ['mu', 'sigma'] # elif likelihood in ['SHASHb','SHASHo','SHASHo2']: # parameter_list = ['mu','sigma','epsilon','delta'] else: @@ -166,46 +169,51 @@ def forward(X, Z, model, sample): if not (i in mapfuncs.keys()): mapfuncs[i] = lambda x: x - output_dict = {p_name:np.zeros(X.shape) for p_name in parameter_list} + output_dict = {p_name: np.zeros(X.shape) for p_name in parameter_list} for p_name in parameter_list: - output_dict[p_name] = mapfuncs[p_name](single_parameter_forward(X,Z,model,sample,p_name)) + output_dict[p_name] = mapfuncs[p_name]( + single_parameter_forward(X, Z, model, sample, p_name)) return output_dict -def Rhats(model, thin = 1, resolution = 100, varnames = None): +def Rhats(model, thin=1, resolution=100, varnames=None): """Get Rhat as function of sampling iteration""" trace = model.hbr.trace if varnames == None: varnames = trace.varnames - chain_length = trace.get_values(varnames[0],chains=trace.chains[0], thin=thin).shape[0] - - interval_skip=chain_length//resolution + chain_length = trace.get_values( + varnames[0], chains=trace.chains[0], thin=thin).shape[0] + + interval_skip = chain_length//resolution rhat_dict = {} for varname in varnames: - testvar = np.stack(trace.get_values(varname,combine=False)) - vardim = testvar.reshape((testvar.shape[0], testvar.shape[1], -1)).shape[2] + testvar = np.stack(trace.get_values(varname, combine=False)) + vardim = testvar.reshape( + (testvar.shape[0], testvar.shape[1], -1)).shape[2] rhats_var = np.zeros((resolution, vardim)) - var = np.stack(trace.get_values(varname,combine=False)) - var = var.reshape((var.shape[0], var.shape[1], -1)) + var = np.stack(trace.get_values(varname, combine=False)) + var = var.reshape((var.shape[0], var.shape[1], -1)) for v in range(var.shape[2]): for j in range(resolution): - rhats_var[j,v] = pm.rhat(var[:,:j*interval_skip,v]) + rhats_var[j, v] = pm.rhat(var[:, :j*interval_skip, v]) rhat_dict[varname] = rhats_var return rhat_dict def S_inv(x, e, d): return np.sinh((np.arcsinh(x) + e) / d) - + + def K(p, x): return np.array(spp.kv(p, x)) + def P(q): """ The P function as given in Jones et al. @@ -218,6 +226,7 @@ def P(q): a = (K1 + K2) * frac return a + def m(epsilon, delta, r): """ The r'th uncentered moment. Given by Jones et al. @@ -231,6 +240,3 @@ def m(epsilon, delta, r): p = P((r - 2 * i) / delta) acc += combs * flip * ex * p return frac1 * acc - - - diff --git a/pcntoolkit/util/utils.py b/pcntoolkit/util/utils.py index e96d9b2c..5679e36d 100644 --- a/pcntoolkit/util/utils.py +++ b/pcntoolkit/util/utils.py @@ -28,23 +28,25 @@ pass path = os.path.abspath(os.path.dirname(__file__)) - rootpath = os.path.dirname(path) # parent directory + rootpath = os.path.dirname(path) # parent directory if rootpath not in sys.path: sys.path.append(rootpath) del path, rootpath import configs - + PICKLE_PROTOCOL = configs.PICKLE_PROTOCOL # ----------------- # Utility functions # ----------------- + + def create_poly_basis(X, dimpoly): """ Compute a polynomial basis expansion of the specified order - + """ - + if len(X.shape) == 1: X = X[:, np.newaxis] D = X.shape[1] @@ -53,25 +55,27 @@ def create_poly_basis(X, dimpoly): for d in range(1, dimpoly+1): Phi[:, colid] = X ** d colid += D - + return Phi -def create_bspline_basis(xmin, xmax, p = 3, nknots = 5): + +def create_bspline_basis(xmin, xmax, p=3, nknots=5): """ Compute a Bspline basis set where: - + :param p: order of spline (3 = cubic) :param nknots: number of knots (endpoints only counted once) """ - + knots = np.linspace(xmin, xmax, nknots) k = splinelab.augknt(knots, p) # pad the knot vector - B = bspline.Bspline(k, p) + B = bspline.Bspline(k, p) return B -def create_design_matrix(X, intercept = True, basis = 'bspline', - basis_column = 0, site_ids=None, all_sites=None, + +def create_design_matrix(X, intercept=True, basis='bspline', + basis_column=0, site_ids=None, all_sites=None, **kwargs): """ Prepare a design matrix from a set of covariates sutiable for @@ -79,7 +83,7 @@ def create_design_matrix(X, intercept = True, basis = 'bspline', a set of user defined covariates, optoinal site intercepts (fixed effects) and also optionally a nonlinear basis expansion over one of the columns - + :param X: matrix of covariates :param basis: type of basis expansion to use :param basis_column: which colume to perform the expansion over? @@ -87,7 +91,7 @@ def create_design_matrix(X, intercept = True, basis = 'bspline', :param all_sites: list of unique site ids :param p: order of spline (3 = cubic) :param nknots: number of knots (endpoints only counted once) - + if site_ids is specified, this must have the same number of entries as there are rows in X. If all_sites is specfied, these will be used to create the site identifiers in place of site_ids. This accommocdates @@ -95,67 +99,71 @@ def create_design_matrix(X, intercept = True, basis = 'bspline', present in the test set (i.e. there will be some empty site columns). """ - + xmin = kwargs.pop('xmin', 0) xmax = kwargs.pop('xmax', 100) - + N = X.shape[0] - + if type(X) is pd.DataFrame: X = X.to_numpy() - - # add intercept column - if intercept: + + # add intercept column + if intercept: Phi = np.concatenate((np.ones((N, 1)), X), axis=1) else: Phi = X - # add dummy coded site columns - if all_sites is None: + # add dummy coded site columns + if all_sites is None: if site_ids is not None: - all_sites = sorted(pd.unique(site_ids)) - + all_sites = sorted(pd.unique(site_ids)) + if site_ids is None: if all_sites is None: site_cols = None else: # site ids are not specified, but all_sites are site_cols = np.zeros((N, len(all_sites))) - else: + else: # site ids are defined # make sure the data are in pandas format if type(site_ids) is not pd.Series: site_ids = pd.Series(data=site_ids) - #site_ids = pd.Series(data=site_ids) - + # site_ids = pd.Series(data=site_ids) + # make sure all_sites is defined - if all_sites is None: - all_sites = sorted(pd.unique(site_ids)) - - # dummy code the sites + if all_sites is None: + all_sites = sorted(pd.unique(site_ids)) + + # dummy code the sites site_cols = np.zeros((N, len(all_sites))) for i, s in enumerate(all_sites): site_cols[:, i] = site_ids == s - - if site_cols.shape[0] != N: - raise ValueError('site cols must have the same number of rows as X') - + + if site_cols.shape[0] != N: + raise ValueError( + 'site cols must have the same number of rows as X') + if site_cols is not None: Phi = np.concatenate((Phi, site_cols), axis=1) - - # create Bspline basis set + + # create Bspline basis set if basis == 'bspline': - B = create_bspline_basis(xmin, xmax, **kwargs) - Phi = np.concatenate((Phi, np.array([B(i) for i in X[:,basis_column]])), axis=1) - elif basis == 'poly': - Phi = np.concatenate(Phi, create_poly_basis(X[:,basis_column], **kwargs)) - + B = create_bspline_basis(xmin, xmax, **kwargs) + Phi = np.concatenate( + (Phi, np.array([B(i) for i in X[:, basis_column]])), axis=1) + elif basis == 'poly': + Phi = np.concatenate(Phi, create_poly_basis( + X[:, basis_column], **kwargs)) + return Phi + def squared_dist(x, z=None): """ Compute sum((x-z) ** 2) for all vectors in a 2d array. - + """ # do some basic checks @@ -204,7 +212,7 @@ def compute_pearsonr(A, B): This function is useful when M is large and only the diagonal entries of the resulting correlation matrix are of interest. This function does not compute the full correlation matrix as an intermediate step - + """ # N = A.shape[1] @@ -216,19 +224,20 @@ def compute_pearsonr(A, B): # then normalize An = Am / np.sqrt(np.sum(Am**2, axis=0)) Bn = Bm / np.sqrt(np.sum(Bm**2, axis=0)) - del(Am, Bm) + del (Am, Bm) Rho = np.sum(An * Bn, axis=0) - del(An, Bn) + del (An, Bn) # Fisher r-to-z Zr = (np.arctanh(Rho) - np.arctanh(0)) * np.sqrt(N - 3) N = stats.norm() pRho = 2*N.cdf(-np.abs(Zr)) # pRho = 1-N.cdf(Zr) - + return Rho, pRho + def explained_var(ytrue, ypred): """ Computes the explained variance of predicted values. @@ -245,54 +254,57 @@ def explained_var(ytrue, ypred): and p is the number of features. :returns exp_var: p dimentional vector of explained variances for each feature. - + """ - exp_var = 1 - (ytrue - ypred).var(axis = 0) / ytrue.var(axis = 0) - + exp_var = 1 - (ytrue - ypred).var(axis=0) / ytrue.var(axis=0) + return exp_var -def compute_MSLL(ytrue, ypred, ypred_var, train_mean = None, train_var = None): + +def compute_MSLL(ytrue, ypred, ypred_var, train_mean=None, train_var=None): """ Computes the MSLL or MLL (not standardized) if 'train_mean' and 'train_var' are None. - + Basic usage:: - + MSLL = compute_MSLL(ytrue, ypred, ytrue_sig, noise_variance, train_mean, train_var) - + where - + :param ytrue : n*p matrix of true values where n is the number of samples and p is the number of features. :param ypred : n*p matrix of predicted values where n is the number of samples and p is the number of features. :param ypred_var : n*p matrix of summed noise variances and prediction variances where n is the number of samples and p is the number of features. - + :param train_mean: p dimensional vector of mean values of the training data for each feature. - + :param train_var : p dimensional vector of covariances of the training data for each feature. :returns loss : p dimensional vector of MSLL or MLL for each feature. """ - - if train_mean is not None and train_var is not None: - + + if train_mean is not None and train_var is not None: + # make sure y_train_mean and y_train_sig have right dimensions (subjects x voxels): - Y_train_mean = np.repeat(train_mean, ytrue.shape[0], axis = 0) - Y_train_sig = np.repeat(train_var, ytrue.shape[0], axis = 0) - + Y_train_mean = np.repeat(train_mean, ytrue.shape[0], axis=0) + Y_train_sig = np.repeat(train_var, ytrue.shape[0], axis=0) + # compute MSLL: - loss = np.mean(0.5 * np.log(2 * np.pi * ypred_var) + (ytrue - ypred)**2 / (2 * ypred_var) - - 0.5 * np.log(2 * np.pi * Y_train_sig) - (ytrue - Y_train_mean)**2 / (2 * Y_train_sig), axis = 0) - - else: + loss = np.mean(0.5 * np.log(2 * np.pi * ypred_var) + (ytrue - ypred)**2 / (2 * ypred_var) - + 0.5 * np.log(2 * np.pi * Y_train_sig) - (ytrue - Y_train_mean)**2 / (2 * Y_train_sig), axis=0) + + else: # compute MLL: - loss = np.mean(0.5 * np.log(2 * np.pi * ypred_var) + (ytrue - ypred)**2 / (2 * ypred_var), axis = 0) - + loss = np.mean(0.5 * np.log(2 * np.pi * ypred_var) + + (ytrue - ypred)**2 / (2 * ypred_var), axis=0) + return loss + def calibration_descriptives(x): """ Compute statistics useful to assess the calibration of normative models, @@ -303,28 +315,29 @@ def calibration_descriptives(x): stats = calibration_descriptives(Z) where - + :param x : n*p matrix of statistics you wish to assess :returns stats :[skew, sdskew, kurtosis, sdkurtosis, semean, sesd] - + """ - + n = np.shape(x)[0] - m1 = np.mean(x,axis=0) + m1 = np.mean(x, axis=0) m2 = sum((x-m1)**2) m3 = sum((x-m1)**3) m4 = sum((x-m1)**4) - s1 = np.std(x,axis=0) + s1 = np.std(x, axis=0) skew = n*m3/(n-1)/(n-2)/s1**3 - sdskew = np.sqrt( 6*n*(n-1) / ((n-2)*(n+1)*(n+3)) ) + sdskew = np.sqrt(6*n*(n-1) / ((n-2)*(n+1)*(n+3))) kurtosis = (n*(n+1)*m4 - 3*m2**2*(n-1)) / ((n-1)*(n-2)*(n-3)*s1**4) - sdkurtosis = np.sqrt( 4*(n**2-1) * sdskew**2 / ((n-3)*(n+5)) ) + sdkurtosis = np.sqrt(4*(n**2-1) * sdskew**2 / ((n-3)*(n+5))) semean = np.sqrt(np.var(x)/n) sesd = s1/np.sqrt(2*(n-1)) cd = [skew, sdskew, kurtosis, sdkurtosis, semean, sesd] - + return cd + class WarpBase(with_metaclass(ABCMeta)): """ Base class for likelihood warping following: @@ -356,7 +369,7 @@ def warp_predictions(self, mu, s2, param, percentiles=[0.025, 0.975]): """ Compute the warped predictions from a gaussian predictive distribution, specifed by a mean (mu) and variance (s2) - + :param mu: Gassian predictive mean :param s2: Predictive variance :param param: warping parameters @@ -370,17 +383,17 @@ def warp_predictions(self, mu, s2, param, percentiles=[0.025, 0.975]): # Compute percentiles of a standard Gaussian N = norm Z = N.ppf(percentiles) - + # find the median (using mu = median) median = self.invf(mu, param) # compute the predictive intervals (non-stationary) pred_interval = np.zeros((len(mu), len(Z))) for i, z in enumerate(Z): - pred_interval[:,i] = self.invf(mu + np.sqrt(s2)*z, param) + pred_interval[:, i] = self.invf(mu + np.sqrt(s2)*z, param) return median, pred_interval - + @abstractmethod def f(self, x, param): """ Evaluate the warping function (mapping non-Gaussian respone @@ -397,6 +410,7 @@ def invf(self, y, param): def df(self, x, param): """ Return the derivative of the warp, dw(x)/dx """ + class WarpLog(WarpBase): """ Affine warp y = a + b*x @@ -404,25 +418,26 @@ class WarpLog(WarpBase): def __init__(self): self.n_params = 0 - + def f(self, x, params=None): - + y = np.log(x) - + return y - + def invf(self, y, params=None): - + x = np.exp(y) - + return x def df(self, x, params): - + df = 1/x - + return df + class WarpAffine(WarpBase): """ Affine warp y = a + b*x @@ -430,60 +445,61 @@ class WarpAffine(WarpBase): def __init__(self): self.n_params = 2 - + def _get_params(self, param): if len(param) != self.n_params: - raise(ValueError, - 'number of parameters must be ' + str(self.n_params)) + raise (ValueError, + 'number of parameters must be ' + str(self.n_params)) return param[0], np.exp(param[1]) def f(self, x, params): a, b = self._get_params(params) - - y = a + b*x + + y = a + b*x return y - + def invf(self, y, params): a, b = self._get_params(params) - - x = (y - a) / b - + + x = (y - a) / b + return x def df(self, x, params): a, b = self._get_params(params) - + df = np.ones(x.shape)*b return df + class WarpBoxCox(WarpBase): """ Box cox transform having a single parameter (lambda), i.e. - + y = (sign(x) * abs(x) ** lamda - 1) / lambda - + This follows the generalization in Bicken and Doksum (1981) JASA 76 and allows x to assume negative values. """ def __init__(self): self.n_params = 1 - + def _get_params(self, param): - + return np.exp(param) def f(self, x, params): lam = self._get_params(params) - + if lam == 0: y = np.log(x) else: - y = (np.sign(x) * np.abs(x) ** lam - 1) / lam + y = (np.sign(x) * np.abs(x) ** lam - 1) / lam return y - + def invf(self, y, params): lam = self._get_params(params) - + if lam == 0: x = np.exp(y) else: @@ -493,36 +509,37 @@ def invf(self, y, params): def df(self, x, params): lam = self._get_params(params) - + dx = np.abs(x) ** (lam - 1) - + return dx + class WarpSinArcsinh(WarpBase): """ Sin-hyperbolic arcsin warp having two parameters (a, b) and defined by - + y = sinh(b * arcsinh(x) - a) - + Using the parametrisation of Rios et al, Neural Networks 118 (2017) where a controls skew and b controls kurtosis, such that: - + * a = 0 : symmetric * a > 0 : positive skew * a < 0 : negative skew * b = 1 : mesokurtic * b > 1 : leptokurtic * b < 1 : platykurtic - + where b > 0. However, it is more convenentent to use an alternative parameterisation, given in Jones and Pewsey 2019 JRSS Significance 16 https://doi.org/10.1111/j.1740-9713.2019.01245.x - + where: y = sinh(b * arcsinh(x) + epsilon * b) - + and a = -epsilon*b - + see also Jones and Pewsey 2009 Biometrika, 96 (4) for more details about the SHASH distribution https://www.jstor.org/stable/27798865 @@ -530,38 +547,39 @@ class WarpSinArcsinh(WarpBase): def __init__(self): self.n_params = 2 - + def _get_params(self, param): if len(param) != self.n_params: - raise(ValueError, - 'number of parameters must be ' + str(self.n_params)) + raise (ValueError, + 'number of parameters must be ' + str(self.n_params)) epsilon = param[0] b = np.exp(param[1]) a = -epsilon*b - + return a, b def f(self, x, params): a, b = self._get_params(params) - + y = np.sinh(b * np.arcsinh(x) - a) return y - + def invf(self, y, params): a, b = self._get_params(params) - + x = np.sinh((np.arcsinh(y)+a)/b) - + return x def df(self, x, params): a, b = self._get_params(params) - - dx = (b *np.cosh(b * np.arcsinh(x) - a))/np.sqrt(1 + x ** 2) - + + dx = (b * np.cosh(b * np.arcsinh(x) - a))/np.sqrt(1 + x ** 2) + return dx - + + class WarpCompose(WarpBase): """ Composition of warps. These are passed in as an array and intialised automatically. For example:: @@ -591,12 +609,12 @@ def f(self, x, theta): for ci, warp in enumerate(self.warps): n_params_c = warp.get_n_params() theta_c = [theta[c] for c in - range(theta_offset, theta_offset + n_params_c)] - theta_offset += n_params_c + range(theta_offset, theta_offset + n_params_c)] + theta_offset += n_params_c if self.debugwarp: print('f:', ci, theta_c, warp) - + if ci == 0: fw = warp.f(x, theta_c) else: @@ -608,7 +626,7 @@ def invf(self, x, theta): n_warps = 0 if self.debugwarp: print('begin composition') - + for ci, warp in enumerate(self.warps): n_params += warp.get_n_params() n_warps += 1 @@ -618,17 +636,17 @@ def invf(self, x, theta): theta_offset -= n_params_c theta_c = [theta[c] for c in range(theta_offset, theta_offset + n_params_c)] - + if self.debugwarp: print('invf:', theta_c, warp) - + if ci == n_warps-1: finvw = warp.invf(x, theta_c) else: finvw = warp.invf(finvw, theta_c) return finvw - + def df(self, x, theta): theta_offset = 0 if self.debugwarp: @@ -639,21 +657,22 @@ def df(self, x, theta): theta_c = [theta[c] for c in range(theta_offset, theta_offset + n_params_c)] theta_offset += n_params_c - + if self.debugwarp: print('df:', ci, theta_c, warp) - + if ci == 0: dfw = warp.df(x, theta_c) else: dfw = warp.df(dfw, theta_c) - + return dfw # ----------------------- # Functions for inference # ----------------------- + class CustomCV: """ Custom cross-validation approach. This function does not do much, it merely provides a wrapper designed to be compatible with @@ -685,9 +704,9 @@ def split(self, X, y=None): te = self.test[i] yield tr, te + def bashwrap(processing_dir, python_path, script_command, job_name, bash_environment=None): - """ This function wraps normative modelling into a bash script to run it on a torque cluster system. @@ -701,7 +720,7 @@ def bashwrap(processing_dir, python_path, script_command, job_name, :param testcovfile_path: Full path to test covariates :param testrespfile_path: Full path to tes responses :param bash_environment: A file containing enviornment specific commands - + :returns: A .sh file containing the commands for normative modelling written by Thomas Wolfers @@ -722,7 +741,7 @@ def bashwrap(processing_dir, python_path, script_command, job_name, bash_environment = [bash_lines + bash_cores] command = [python_path + ' ' + script_command + '\n'] - + # writes bash file into processing dir bash_file_name = os.path.join(processing_dir, job_name + '.sh') with open(bash_file_name, 'w') as bash_file: @@ -730,12 +749,13 @@ def bashwrap(processing_dir, python_path, script_command, job_name, # changes permissoins for bash.sh file os.chmod(bash_file_name, 0o700) - + return bash_file_name + def qsub(job_path, memory, duration, logdir=None): """This function submits a job.sh scipt to the torque custer using the qsub command. - + Basic usage:: qsub_nm(job_path, log_path, memory, duration) @@ -753,67 +773,73 @@ def qsub(job_path, memory, duration, logdir=None): # created qsub command qsub_call = ['echo ' + job_path + ' | qsub -N ' + job_path + ' -l ' + - 'mem=' + memory + ',walltime=' + duration + + 'mem=' + memory + ',walltime=' + duration + ' -e ' + logdir + ' -o ' + logdir] # submits job to cluster call(qsub_call, shell=True) - + + def extreme_value_prob_fit(NPM, perc): n = NPM.shape[0] t = NPM.shape[1] n_perc = int(round(t * perc)) m = np.zeros(n) for i in range(n): - temp = np.abs(NPM[i, :]) + temp = np.abs(NPM[i, :]) temp = np.sort(temp) temp = temp[t - n_perc:] temp = temp[0:int(np.floor(0.90*temp.shape[0]))] m[i] = np.mean(temp) params = genextreme.fit(m) return params - + + def extreme_value_prob(params, NPM, perc): n = NPM.shape[0] t = NPM.shape[1] n_perc = int(round(t * perc)) m = np.zeros(n) for i in range(n): - temp = np.abs(NPM[i, :]) + temp = np.abs(NPM[i, :]) temp = np.sort(temp) temp = temp[t - n_perc:] temp = temp[0:int(np.floor(0.90*temp.shape[0]))] m[i] = np.mean(temp) - probs = genextreme.cdf(m,*params) + probs = genextreme.cdf(m, *params) return probs + def ravel_2D(a): s = a.shape - return np.reshape(a,[s[0], np.prod(s[1:])]) + return np.reshape(a, [s[0], np.prod(s[1:])]) + def unravel_2D(a, s): - return np.reshape(a,s) + return np.reshape(a, s) + def threshold_NPM(NPMs, fdr_thr=0.05, npm_thr=0.1): """ Compute voxels with significant NPMs. """ p_values = stats.norm.cdf(-np.abs(NPMs)) - results = np.zeros(NPMs.shape) + results = np.zeros(NPMs.shape) masks = np.full(NPMs.shape, False, dtype=bool) - for i in range(p_values.shape[0]): - masks[i,:] = FDR(p_values[i,:], fdr_thr) - results[i,] = NPMs[i,:] * masks[i,:].astype(np.int) - m = np.sum(masks,axis=0)/masks.shape[0] > npm_thr - #m = np.any(masks,axis=0) + for i in range(p_values.shape[0]): + masks[i, :] = FDR(p_values[i, :], fdr_thr) + results[i,] = NPMs[i, :] * masks[i, :].astype(np.int) + m = np.sum(masks, axis=0)/masks.shape[0] > npm_thr + # m = np.any(masks,axis=0) return results, masks, m - + + def FDR(p_values, alpha): """ Compute the false discovery rate in all voxels for a subject. """ dim = np.shape(p_values) - p_values = np.reshape(p_values,[np.prod(dim),]) + p_values = np.reshape(p_values, [np.prod(dim),]) sorted_p_values = np.sort(p_values) - sorted_p_values_idx = np.argsort(p_values); + sorted_p_values_idx = np.argsort(p_values) testNum = len(p_values) - thresh = ((np.array(range(testNum)) + 1)/np.float(testNum)) * alpha + thresh = ((np.array(range(testNum)) + 1)/np.float(testNum)) * alpha h = sorted_p_values <= thresh unsort = np.argsort(sorted_p_values_idx) h = h[unsort] @@ -821,20 +847,21 @@ def FDR(p_values, alpha): return h -def calibration_error(Y,m,s,cal_levels): +def calibration_error(Y, m, s, cal_levels): ce = 0 for cl in cal_levels: z = np.abs(norm.ppf((1-cl)/2)) ub = m + z * s lb = m - z * s - ce = ce + np.abs(cl - np.sum(np.logical_and(Y>=lb,Y<=ub))/Y.shape[0]) + ce = ce + \ + np.abs(cl - np.sum(np.logical_and(Y >= lb, Y <= ub))/Y.shape[0]) return ce -def simulate_data(method='linear', n_samples=100, n_features=1, n_grps=1, +def simulate_data(method='linear', n_samples=100, n_features=1, n_grps=1, working_dir=None, plot=False, random_state=None, noise=None): """ This function simulates linear synthetic data for testing pcntoolkit methods. - + :param method: simulate 'linear' or 'non-linear' function. :param n_samples: number of samples in each group of the training and test sets. If it is an int then the same sample number will be used for all groups. @@ -849,39 +876,39 @@ def simulate_data(method='linear', n_samples=100, n_features=1, n_grps=1, :param random_state: random state for generating random numbers (Default=None). :param noise: Type of added noise to the data. The options are 'gaussian', 'exponential', and 'hetero_gaussian' (The defauls is None.). - + :returns: X_train, Y_train, grp_id_train, X_test, Y_test, grp_id_test, coef - + """ - + if isinstance(n_samples, int): n_samples = [n_samples for i in range(n_grps)] - + X_train, Y_train, X_test, Y_test = [], [], [], [] grp_id_train, grp_id_test = [], [] coef = [] for i in range(n_grps): bias = np.random.randint(-10, high=10) - + if method == 'linear': - X_temp, Y_temp, coef_temp = make_regression(n_samples=n_samples[i]*2, - n_features=n_features, n_targets=1, - noise=10 * np.random.rand(), bias=bias, - n_informative=1, coef=True, - random_state=random_state) + X_temp, Y_temp, coef_temp = make_regression(n_samples=n_samples[i]*2, + n_features=n_features, n_targets=1, + noise=10 * np.random.rand(), bias=bias, + n_informative=1, coef=True, + random_state=random_state) elif method == 'non-linear': - X_temp = np.random.randint(-2,6,[2*n_samples[i], n_features]) \ - + np.random.randn(2*n_samples[i], n_features) - Y_temp = X_temp[:,0] * 20 * np.random.rand() + np.random.randint(10,100) \ - * np.sin(2 * np.random.rand() + 2 * np.pi /5 * X_temp[:,0]) + X_temp = np.random.randint(-2, 6, [2*n_samples[i], n_features]) \ + + np.random.randn(2*n_samples[i], n_features) + Y_temp = X_temp[:, 0] * 20 * np.random.rand() + np.random.randint(10, 100) \ + * np.sin(2 * np.random.rand() + 2 * np.pi / 5 * X_temp[:, 0]) coef_temp = 0 elif method == 'combined': - X_temp = np.random.randint(-2,6,[2*n_samples[i], n_features]) \ - + np.random.randn(2*n_samples[i], n_features) - Y_temp = (X_temp[:,0]**3) * np.random.uniform(0, 0.5) \ - + X_temp[:,0] * 20 * np.random.rand() \ - + np.random.randint(10, 100) + X_temp = np.random.randint(-2, 6, [2*n_samples[i], n_features]) \ + + np.random.randn(2*n_samples[i], n_features) + Y_temp = (X_temp[:, 0]**3) * np.random.uniform(0, 0.5) \ + + X_temp[:, 0] * 20 * np.random.rand() \ + + np.random.randint(10, 100) coef_temp = 0 else: raise ValueError("Unknow method. Please specify valid method among \ @@ -894,74 +921,78 @@ def simulate_data(method='linear', n_samples=100, n_features=1, n_grps=1, grp_id = np.repeat(i, X_temp.shape[0]) grp_id_train.append(grp_id[:X_temp.shape[0]//2]) grp_id_test.append(grp_id[X_temp.shape[0]//2:]) - + if noise == 'hetero_gaussian': - t = np.random.randint(5,10) + t = np.random.randint(5, 10) Y_train[i] = Y_train[i] + np.random.randn(Y_train[i].shape[0]) / t \ - * np.log(1 + np.exp(X_train[i][:,0])) + * np.log(1 + np.exp(X_train[i][:, 0])) Y_test[i] = Y_test[i] + np.random.randn(Y_test[i].shape[0]) / t \ - * np.log(1 + np.exp(X_test[i][:,0])) + * np.log(1 + np.exp(X_test[i][:, 0])) elif noise == 'gaussian': - t = np.random.randint(3,10) + t = np.random.randint(3, 10) Y_train[i] = Y_train[i] + np.random.randn(Y_train[i].shape[0])/t Y_test[i] = Y_test[i] + np.random.randn(Y_test[i].shape[0])/t elif noise == 'exponential': - t = np.random.randint(1,3) - Y_train[i] = Y_train[i] + np.random.exponential(1, Y_train[i].shape[0]) / t - Y_test[i] = Y_test[i] + np.random.exponential(1, Y_test[i].shape[0]) / t + t = np.random.randint(1, 3) + Y_train[i] = Y_train[i] + \ + np.random.exponential(1, Y_train[i].shape[0]) / t + Y_test[i] = Y_test[i] + \ + np.random.exponential(1, Y_test[i].shape[0]) / t elif noise == 'hetero_gaussian_smaller': - t = np.random.randint(5,10) + t = np.random.randint(5, 10) Y_train[i] = Y_train[i] + np.random.randn(Y_train[i].shape[0]) / t \ - * np.log(1 + np.exp(0.3 * X_train[i][:,0])) + * np.log(1 + np.exp(0.3 * X_train[i][:, 0])) Y_test[i] = Y_test[i] + np.random.randn(Y_test[i].shape[0]) / t \ - * np.log(1 + np.exp(0.3 * X_test[i][:,0])) + * np.log(1 + np.exp(0.3 * X_test[i][:, 0])) X_train = np.vstack(X_train) X_test = np.vstack(X_test) Y_train = np.concatenate(Y_train) Y_test = np.concatenate(Y_test) grp_id_train = np.expand_dims(np.concatenate(grp_id_train), axis=1) grp_id_test = np.expand_dims(np.concatenate(grp_id_test), axis=1) - + for i in range(n_features): plt.figure() for j in range(n_grps): - plt.scatter(X_train[grp_id_train[:,0]==j,i], - Y_train[grp_id_train[:,0]==j,], label='Group ' + str(j)) + plt.scatter(X_train[grp_id_train[:, 0] == j, i], + Y_train[grp_id_train[:, 0] == j,], label='Group ' + str(j)) plt.xlabel('X' + str(i)) plt.ylabel('Y') plt.legend() - + if working_dir is not None: if not os.path.isdir(working_dir): os.mkdir(working_dir) - with open(os.path.join(working_dir ,'trbefile.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(grp_id_train),file, protocol=PICKLE_PROTOCOL) - with open(os.path.join(working_dir ,'tsbefile.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(grp_id_test),file, protocol=PICKLE_PROTOCOL) - with open(os.path.join(working_dir ,'X_train.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(X_train),file, protocol=PICKLE_PROTOCOL) - with open(os.path.join(working_dir ,'X_test.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(X_test),file, protocol=PICKLE_PROTOCOL) - with open(os.path.join(working_dir ,'Y_train.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(Y_train),file, protocol=PICKLE_PROTOCOL) - with open(os.path.join(working_dir ,'Y_test.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(Y_test),file, protocol=PICKLE_PROTOCOL) - + with open(os.path.join(working_dir, 'trbefile.pkl'), 'wb') as file: + pickle.dump(pd.DataFrame(grp_id_train), + file, protocol=PICKLE_PROTOCOL) + with open(os.path.join(working_dir, 'tsbefile.pkl'), 'wb') as file: + pickle.dump(pd.DataFrame(grp_id_test), + file, protocol=PICKLE_PROTOCOL) + with open(os.path.join(working_dir, 'X_train.pkl'), 'wb') as file: + pickle.dump(pd.DataFrame(X_train), file, protocol=PICKLE_PROTOCOL) + with open(os.path.join(working_dir, 'X_test.pkl'), 'wb') as file: + pickle.dump(pd.DataFrame(X_test), file, protocol=PICKLE_PROTOCOL) + with open(os.path.join(working_dir, 'Y_train.pkl'), 'wb') as file: + pickle.dump(pd.DataFrame(Y_train), file, protocol=PICKLE_PROTOCOL) + with open(os.path.join(working_dir, 'Y_test.pkl'), 'wb') as file: + pickle.dump(pd.DataFrame(Y_test), file, protocol=PICKLE_PROTOCOL) + return X_train, Y_train, grp_id_train, X_test, Y_test, grp_id_test, coef def divergence_plot(nm, ylim=None): - + if nm.hbr.configs['n_chains'] > 1 and nm.hbr.model_type != 'nn': a = pm.summary(nm.hbr.trace).round(2) plt.figure() - plt.hist(a['r_hat'],10) + plt.hist(a['r_hat'], 10) plt.title('Gelman-Rubin diagnostic for divergence') divergent = nm.hbr.trace['diverging'] - + tracedf = pm.trace_to_dataframe(nm.hbr.trace) - + _, ax = plt.subplots(2, 1, figsize=(15, 4), sharex=True, sharey=True) ax[0].plot(tracedf.values[divergent == 0].T, color='k', alpha=.05) ax[0].set_title('No Divergences', fontsize=10) @@ -972,12 +1003,11 @@ def divergence_plot(nm, ylim=None): plt.xticks(rotation=90, fontsize=7) plt.tight_layout() plt.show() - - + + def load_freesurfer_measure(measure, data_path, subjects_list): - """This is a utility function to load different Freesurfer measures in a pandas Dataframe. - + Inputs :param measure: a string that defines the type of Freesurfer measure we want to load. \ @@ -994,51 +1024,51 @@ def load_freesurfer_measure(measure, data_path, subjects_list): * 'CurvInd': Intrinsic Curvature Index in each cortical area based on Destrieux atlas. * 'brain': Brain Segmentation Statistics from aseg.stats file. * 'subcortical_volumes': Subcortical areas volume. - + :param data_path: a string that specifies the path to the main Freesurfer folder. :param subjects_list: A Pythin list containing the list of subject names to load the data for. \ The subject names should match the folder name for each subject's Freesurfer data folder. - + Outputs: - df: A pandas datafrmae containing the subject names as Index and target Freesurfer measures. - missing_subs: A Python list of subject names that miss the target Freesurefr measures. - + """ - + df = pd.DataFrame() missing_subs = [] - - if measure in ['NumVert', 'SurfArea', 'GrayVol', 'ThickAvg', + + if measure in ['NumVert', 'SurfArea', 'GrayVol', 'ThickAvg', 'ThickStd', 'MeanCurv', 'GausCurv', 'FoldInd', 'CurvInd']: - l = ['NumVert', 'SurfArea', 'GrayVol', 'ThickAvg', - 'ThickStd', 'MeanCurv', 'GausCurv', 'FoldInd', 'CurvInd'] + l = ['NumVert', 'SurfArea', 'GrayVol', 'ThickAvg', + 'ThickStd', 'MeanCurv', 'GausCurv', 'FoldInd', 'CurvInd'] col = l.index(measure) + 1 for i, sub in enumerate(subjects_list): try: data = dict() - - a = pd.read_csv(data_path + sub + '/stats/lh.aparc.a2009s.stats', + + a = pd.read_csv(data_path + sub + '/stats/lh.aparc.a2009s.stats', delimiter='\s+', comment='#', header=None) temp = dict(zip(a[0], a[col])) for key in list(temp.keys()): temp['L_'+key] = temp.pop(key) data.update(temp) - - a = pd.read_csv(data_path + sub + '/stats/rh.aparc.a2009s.stats', + + a = pd.read_csv(data_path + sub + '/stats/rh.aparc.a2009s.stats', delimiter='\s+', comment='#', header=None) temp = dict(zip(a[0], a[col])) for key in list(temp.keys()): temp['R_'+key] = temp.pop(key) data.update(temp) - - df_temp = pd.DataFrame(data,index=[sub]) + + df_temp = pd.DataFrame(data, index=[sub]) df = pd.concat([df, df_temp]) - print('%d / %d: %s is done!' %(i, len(subjects_list), sub)) + print('%d / %d: %s is done!' % (i, len(subjects_list), sub)) except: missing_subs.append(sub) - print('%d / %d: %s is missing!' %(i, len(subjects_list), sub)) + print('%d / %d: %s is missing!' % (i, len(subjects_list), sub)) continue - + elif measure == 'brain': for i, sub in enumerate(subjects_list): try: @@ -1048,18 +1078,18 @@ def load_freesurfer_measure(measure, data_path, subjects_list): for line in f: if line.startswith('# Measure'): s.write(line) - s.seek(0) # "rewind" to the beginning of the StringIO object - a = pd.read_csv(s, header=None) # with further parameters? + s.seek(0) # "rewind" to the beginning of the StringIO object + a = pd.read_csv(s, header=None) # with further parameters? data_brain = dict(zip(a[1], a[3])) data.update(data_brain) - df_temp = pd.DataFrame(data,index=[sub]) + df_temp = pd.DataFrame(data, index=[sub]) df = pd.concat([df, df_temp]) - print('%d / %d: %s is done!' %(i, len(subjects_list), sub)) + print('%d / %d: %s is done!' % (i, len(subjects_list), sub)) except: missing_subs.append(sub) - print('%d / %d: %s is missing!' %(i, len(subjects_list), sub)) + print('%d / %d: %s is missing!' % (i, len(subjects_list), sub)) continue - + elif measure == 'subcortical_volumes': for i, sub in enumerate(subjects_list): try: @@ -1069,38 +1099,38 @@ def load_freesurfer_measure(measure, data_path, subjects_list): for line in f: if line.startswith('# Measure'): s.write(line) - s.seek(0) # "rewind" to the beginning of the StringIO object - a = pd.read_csv(s, header=None) # with further parameters? + s.seek(0) # "rewind" to the beginning of the StringIO object + a = pd.read_csv(s, header=None) # with further parameters? a = dict(zip(a[1], a[3])) if ' eTIV' in a.keys(): tiv = a[' eTIV'] else: tiv = a[' ICV'] - a = pd.read_csv(data_path + sub + '/stats/aseg.stats', delimiter='\s+', comment='#', header=None) + a = pd.read_csv(data_path + sub + '/stats/aseg.stats', + delimiter='\s+', comment='#', header=None) data_vol = dict(zip(a[4]+'_mm3', a[3])) for key in data_vol.keys(): data_vol[key] = data_vol[key]/tiv data.update(data_vol) - data = pd.DataFrame(data,index=[sub]) + data = pd.DataFrame(data, index=[sub]) df = pd.concat([df, data]) - print('%d / %d: %s is done!' %(i, len(subjects_list), sub)) + print('%d / %d: %s is done!' % (i, len(subjects_list), sub)) except: missing_subs.append(sub) - print('%d / %d: %s is missing!' %(i, len(subjects_list), sub)) + print('%d / %d: %s is missing!' % (i, len(subjects_list), sub)) continue - + return df, missing_subs class scaler: - - def __init__(self, scaler_type='standardize', tail=0.05, + + def __init__(self, scaler_type='standardize', tail=0.05, adjust_outliers=True): - """ A class for rescaling data using either standardization or minmax normalization. - + :param scaler_type: String that decides the type of scaler including 1) 'standardize' for standardizing data, 2) 'minmax' for minmax normalization in range of [0,1], and 3) 'robminmax' for robust (to outliers) minmax @@ -1111,107 +1141,107 @@ def __init__(self, scaler_type='standardize', tail=0.05, :param adjust_outliers: Boolean that decides whether to adjust the outliers in 'robminmax' normalization or not. If True the outliers values are truncated to 0 or 1. The defauls is True. - + """ - + self.scaler_type = scaler_type self.tail = tail self.adjust_outliers = adjust_outliers - + if self.scaler_type not in ['standardize', 'minmax', 'robminmax']: - raise ValueError("Undifined scaler type!") - - + raise ValueError("Undifined scaler type!") + def fit(self, X): - + if self.scaler_type == 'standardize': - + self.m = np.mean(X, axis=0) self.s = np.std(X, axis=0) - + elif self.scaler_type == 'minmax': self.min = np.min(X, axis=0) self.max = np.max(X, axis=0) - + elif self.scaler_type == 'robminmax': self.min = np.zeros([X.shape[1],]) self.max = np.zeros([X.shape[1],]) for i in range(X.shape[1]): - self.min[i] = np.median(np.sort(X[:,i])[0:int(np.round(X.shape[0] * self.tail))]) - self.max[i] = np.median(np.sort(X[:,i])[-int(np.round(X.shape[0] * self.tail)):]) - - + self.min[i] = np.median( + np.sort(X[:, i])[0:int(np.round(X.shape[0] * self.tail))]) + self.max[i] = np.median( + np.sort(X[:, i])[-int(np.round(X.shape[0] * self.tail)):]) + def transform(self, X, index=None): - + if self.scaler_type == 'standardize': if index is None: - X = (X - self.m) / self.s + X = (X - self.m) / self.s else: X = (X - self.m[index]) / self.s[index] - + elif self.scaler_type in ['minmax', 'robminmax']: if index is None: X = (X - self.min) / (self.max - self.min) else: X = (X - self.min[index]) / (self.max[index] - self.min[index]) - + if self.adjust_outliers: - + X[X < 0] = 0 X[X > 1] = 1 - + return X - + def inverse_transform(self, X, index=None): - + if self.scaler_type == 'standardize': if index is None: X = X * self.s + self.m else: X = X * self.s[index] + self.m[index] - + elif self.scaler_type in ['minmax', 'robminmax']: if index is None: - X = X * (self.max - self.min) + self.min + X = X * (self.max - self.min) + self.min else: X = X * (self.max[index] - self.min[index]) + self.min[index] return X - + def fit_transform(self, X): - + if self.scaler_type == 'standardize': - + self.m = np.mean(X, axis=0) self.s = np.std(X, axis=0) - X = (X - self.m) / self.s - + X = (X - self.m) / self.s + elif self.scaler_type == 'minmax': - + self.min = np.min(X, axis=0) self.max = np.max(X, axis=0) X = (X - self.min) / (self.max - self.min) - + elif self.scaler_type == 'robminmax': - + self.min = np.zeros([X.shape[1],]) self.max = np.zeros([X.shape[1],]) - + for i in range(X.shape[1]): - self.min[i] = np.median(np.sort(X[:,i])[0:int(np.round(X.shape[0] * self.tail))]) - self.max[i] = np.median(np.sort(X[:,i])[-int(np.round(X.shape[0] * self.tail)):]) - + self.min[i] = np.median( + np.sort(X[:, i])[0:int(np.round(X.shape[0] * self.tail))]) + self.max[i] = np.median( + np.sort(X[:, i])[-int(np.round(X.shape[0] * self.tail)):]) + X = (X - self.min) / (self.max - self.min) - - if self.adjust_outliers: + + if self.adjust_outliers: X[X < 0] = 0 X[X > 1] = 1 - + return X - - - + + def retrieve_freesurfer_eulernum(freesurfer_dir, subjects=None, save_path=None): - """ This function receives the freesurfer directory (including processed data for several subjects) and retrieves the Euler number from the log files. If @@ -1237,134 +1267,141 @@ def retrieve_freesurfer_eulernum(freesurfer_dir, subjects=None, save_path=None): :outputs: * ENs - A dataframe of retrieved ENs. * missing_subjects - The list of missing subjects. - + Developed by S.M. Kia - + """ - + if subjects is None: - subjects = [temp for temp in os.listdir(freesurfer_dir) - if os.path.isdir(os.path.join(freesurfer_dir ,temp))] - - df = pd.DataFrame(index=subjects, columns=['lh_en','rh_en','avg_en']) + subjects = [temp for temp in os.listdir(freesurfer_dir) + if os.path.isdir(os.path.join(freesurfer_dir, temp))] + + df = pd.DataFrame(index=subjects, columns=['lh_en', 'rh_en', 'avg_en']) missing_subjects = [] - + for s, sub in enumerate(subjects): sub_dir = os.path.join(freesurfer_dir, sub) log_file = os.path.join(sub_dir, 'scripts', 'recon-all.log') - + if os.path.exists(sub_dir): - if os.path.exists(log_file): + if os.path.exists(log_file): with open(log_file) as f: for line in f: # find the part that refers to the EC if re.search('orig.nofix lheno', line): eno_line = line f.close() - eno_l = eno_line.split()[3][0:-1] # remove the trailing comma + eno_l = eno_line.split()[3][0:-1] # remove the trailing comma eno_r = eno_line.split()[6] euler = (float(eno_l) + float(eno_r)) / 2 - + df.at[sub, 'lh_en'] = eno_l df.at[sub, 'rh_en'] = eno_r df.at[sub, 'avg_en'] = euler - - print('%d: Subject %s is successfully processed. EN = %f' - %(s, sub, df.at[sub, 'avg_en'])) + + print('%d: Subject %s is successfully processed. EN = %f' + % (s, sub, df.at[sub, 'avg_en'])) else: - print('%d: Subject %s is missing log file, running QC ...' %(s, sub)) + print('%d: Subject %s is missing log file, running QC ...' % (s, sub)) try: - bashCommand = 'mris_euler_number '+ freesurfer_dir + sub +'/surf/lh.orig.nofix>' + 'temp_l.txt 2>&1' - res = subprocess.run(bashCommand, stdout=subprocess.PIPE, shell=True) - file = open('temp_l.txt', mode = 'r', encoding = 'utf-8-sig') + bashCommand = 'mris_euler_number ' + freesurfer_dir + \ + sub + '/surf/lh.orig.nofix>' + 'temp_l.txt 2>&1' + res = subprocess.run( + bashCommand, stdout=subprocess.PIPE, shell=True) + file = open('temp_l.txt', mode='r', encoding='utf-8-sig') lines = file.readlines() file.close() words = [] for line in lines: line = line.strip() - words.append([item.strip() for item in line.split(' ')]) + words.append([item.strip() + for item in line.split(' ')]) eno_l = np.float32(words[0][12]) - - bashCommand = 'mris_euler_number '+ freesurfer_dir + sub +'/surf/rh.orig.nofix>' + 'temp_r.txt 2>&1' - res = subprocess.run(bashCommand, stdout=subprocess.PIPE, shell=True) - file = open('temp_r.txt', mode = 'r', encoding = 'utf-8-sig') + + bashCommand = 'mris_euler_number ' + freesurfer_dir + \ + sub + '/surf/rh.orig.nofix>' + 'temp_r.txt 2>&1' + res = subprocess.run( + bashCommand, stdout=subprocess.PIPE, shell=True) + file = open('temp_r.txt', mode='r', encoding='utf-8-sig') lines = file.readlines() file.close() words = [] for line in lines: line = line.strip() - words.append([item.strip() for item in line.split(' ')]) + words.append([item.strip() + for item in line.split(' ')]) eno_r = np.float32(words[0][12]) - + df.at[sub, 'lh_en'] = eno_l df.at[sub, 'rh_en'] = eno_r df.at[sub, 'avg_en'] = (eno_r + eno_l) / 2 - - print('%d: Subject %s is successfully processed. EN = %f' - %(s, sub, df.at[sub, 'avg_en'])) - + + print('%d: Subject %s is successfully processed. EN = %f' + % (s, sub, df.at[sub, 'avg_en'])) + except: e = sys.exc_info()[0] missing_subjects.append(sub) - print('%d: QC is failed for subject %s: %s.' %(s, sub, e)) - + print('%d: QC is failed for subject %s: %s.' % (s, sub, e)) + else: missing_subjects.append(sub) - print('%d: Subject %s is missing.' %(s, sub)) + print('%d: Subject %s is missing.' % (s, sub)) df = df.dropna() - + if save_path is not None: with open(save_path, 'wb') as file: - pickle.dump({'ENs':df}, file) - + pickle.dump({'ENs': df}, file) + return df, missing_subjects + def get_package_versions(): - + import platform versions = dict() versions['Python'] = platform.python_version() - - try: + + try: import pytensor versions['pytensor'] = pytensor.__version__ except: versions['pytensor'] = '' - - try: + + try: import pymc versions['PyMC'] = pymc.__version__ except: versions['PyMC'] = '' - - try: + + try: import pcntoolkit versions['PCNtoolkit'] = pcntoolkit.__version__ except: versions['PCNtoolkit'] = '' - + return versions - - + + def z_to_abnormal_p(Z): """ - + This function receives a matrix of z-scores (deviations) and transfer them to corresponding abnormal probabilities. For more information see Sec. 2.5 in https://www.biorxiv.org/content/10.1101/2021.05.28.446120v1.full.pdf. - + :param Z: n by p matrix of z-scores (deviations in normative modeling) where n is the number of subjects and p is the number of features. :type Z: numpy.array - + :return: a matrix of same size as Z, with probability of each sample being an abnormal sample. :rtype: numpy.array """ - - abn_p = 1- norm.sf(np.abs(Z))*2 - + + abn_p = 1 - norm.sf(np.abs(Z))*2 + return abn_p @@ -1373,7 +1410,7 @@ def anomaly_detection_auc(abn_p, labels, n_permutation=None): This is a utility function for computing region-wise AUC scores for anomaly detection using normative model. If n_permutations is not None (e.g. 1000), it also computes permuation p_values for each region. - + :param abn_p: n by p matrix of with probability of each sample being an abnormal sample. This matrix can be computed using 'z_to_abnormal_p' function. @@ -1389,62 +1426,62 @@ def anomaly_detection_auc(abn_p, labels, n_permutation=None): :rtype: numpy.array """ - + n, p = abn_p.shape aucs = np.zeros([p]) p_values = np.zeros([p]) - + for i in range(p): - aucs[i] = roc_auc_score(labels, abn_p[:,i]) - + aucs[i] = roc_auc_score(labels, abn_p[:, i]) + if n_permutation is not None: - + auc_perm = np.zeros([n_permutation]) for j in range(n_permutation): rand_idx = np.random.permutation(len(labels)) rand_labels = labels[rand_idx] - auc_perm[j] = roc_auc_score(rand_labels, abn_p[:,i]) - - p_values[i] = (np.sum(auc_perm > aucs[i]) + 1) / (n_permutation + 1) - print('Feature %d of %d is done: p_value=%f' %(i,n_permutation,p_values[i])) - + auc_perm[j] = roc_auc_score(rand_labels, abn_p[:, i]) + + p_values[i] = (np.sum(auc_perm > aucs[i]) + 1) / \ + (n_permutation + 1) + print('Feature %d of %d is done: p_value=%f' % + (i, n_permutation, p_values[i])) + return aucs, p_values def cartesian_product(arrays): - """ This is a utility function for creating dummy data (covariates). It computes the cartesian product of N 1D arrays. - + Example: a = cartesian_product(np.arange(0,5), np.arange(6,10)) - + :param arrays: a list of N input 1D numpy arrays with size d1,d2,dN. :return: A d1...dN by N matrix of cartesian product of N arrays. """ - + la = len(arrays) dtype = np.result_type(arrays[0]) arr = np.empty([len(a) for a in arrays] + [la], dtype=dtype) for i, a in enumerate(np.ix_(arrays)): - arr[...,i] = a - + arr[..., i] = a + return arr.reshape(-1, la) def yes_or_no(question): - """ Utility function for getting yes/no action from the user. - + :param question: String for user query. - + :return: Boolean of True for 'yes' and False for 'no'. - + """ - + while "the answer is invalid": reply = str(input(question+' (y/n): ')).lower().strip() if reply[:1] == 'y': @@ -1453,12 +1490,12 @@ def yes_or_no(question): return False - -#====== This is stuff used for the SHASH distributions, but using numpy (not pymc or pytensor) === +# ====== This is stuff used for the SHASH distributions, but using numpy (not pymc or pytensor) === def K(p, x): return np.array(spp.kv(p, x)) + def P(q): """ The P function as given in Jones et al. @@ -1472,6 +1509,7 @@ def P(q): a = (K1 + K2) * frac return a + def m(epsilon, delta, r): """ The r'th uncentered moment. Given by Jones et al. @@ -1486,12 +1524,12 @@ def m(epsilon, delta, r): acc += combs * flip * ex * p return frac1 * acc -#====== end stufff for SHASH +# ====== end stufff for SHASH # Design matrix function -def z_score(y, mean, std, skew=None, kurtosis=None, likelihood = "Normal"): +def z_score(y, mean, std, skew=None, kurtosis=None, likelihood="Normal"): """ Computes Z-score of some data given parameters and a likelihood type string. if likelihood == "Normal", parameters 'skew' and 'kurtosis' are ignored diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..9b914123 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[autopep8] +max-line-length = 120 +ignore = E226,E302,E41 diff --git a/setup.py b/setup.py index 59bff139..24d23e60 100644 --- a/setup.py +++ b/setup.py @@ -12,13 +12,13 @@ 'argparse', 'nibabel>=2.5.1', 'six', - 'scikit-learn', + 'scikit-learn', 'bspline', 'matplotlib', 'numpy', 'scipy>=1.3.2', 'pandas>=0.25.3', - 'torch>=1.1.0', + 'torch>=1.1.0', 'sphinx-tabs', 'pymc>=5.1.0', 'arviz==0.13.0' diff --git a/tests/profile_trendsurf.py b/tests/profile_trendsurf.py index e33a88db..3877122f 100644 --- a/tests/profile_trendsurf.py +++ b/tests/profile_trendsurf.py @@ -1,13 +1,13 @@ # NOTE: must be run with kernprof (otherwise the inmports get screwed up) -#import pcntoolkit +# import pcntoolkit +from bayesreg import BLR +from line_profiler import LineProfiler +from trendsurf import estimate import os import sys sys.path.append('/home/preclineu/andmar/sfw/PCNtoolkit/pcntoolkit') -from trendsurf import estimate -from line_profiler import LineProfiler -from bayesreg import BLR # with test covariates wdir = '/home/preclineu/andmar/py.sandbox/unittests' @@ -32,4 +32,4 @@ # lp.enable() # hyp[i, :] = breg.estimate(hyp0, Phi, Yz[:, i]) # lp.disable() -# lp.print_stats() \ No newline at end of file +# lp.print_stats() diff --git a/tests/testHBR.py b/tests/testHBR.py index b7893fdb..fee7065e 100644 --- a/tests/testHBR.py +++ b/tests/testHBR.py @@ -27,13 +27,13 @@ working_dir = '/home/stijn/temp/' # Specifyexit() a working directory - # to save data and results. +# to save data and results. simulation_method = 'linear' n_features = 1 # The number of input features of X -n_grps = 2 # Number of batches in data +n_grps = 2 # Number of batches in data n_samples = 500 # Number of samples in each group (use a list for different - # sample numbers across different batches) +# sample numbers across different batches) model_types = ['linear', 'polynomial', 'bspline'] # models to try @@ -41,52 +41,53 @@ X_train, Y_train, grp_id_train, X_test, Y_test, grp_id_test, coef = \ - simulate_data(simulation_method, n_samples, n_features, n_grps, + simulate_data(simulation_method, n_samples, n_features, n_grps, working_dir=working_dir, plot=True) ################################# Methods Tests ############################### - - + + for model_type in model_types: - - nm = norm_init(X_train, Y_train, alg='hbr',likelihood='SHASHb', model_type=model_type,n_samples=100,n_tuning=10) + + nm = norm_init(X_train, Y_train, alg='hbr', likelihood='SHASHb', + model_type=model_type, n_samples=100, n_tuning=10) nm.estimate(X_train, Y_train, trbefile=working_dir+'trbefile.pkl') yhat, ys2 = nm.predict(X_test, tsbefile=working_dir+'tsbefile.pkl') for i in range(n_features): - sorted_idx = X_test[:,i].argsort(axis=0).squeeze() - temp_X = X_test[sorted_idx,i] + sorted_idx = X_test[:, i].argsort(axis=0).squeeze() + temp_X = X_test[sorted_idx, i] temp_Y = Y_test[sorted_idx,] - temp_be = grp_id_test[sorted_idx,:].squeeze() + temp_be = grp_id_test[sorted_idx, :].squeeze() temp_yhat = yhat[sorted_idx,] temp_s2 = ys2[sorted_idx,] - + plt.figure() for j in range(n_grps): - scat1 = plt.scatter(temp_X[temp_be==j,], temp_Y[temp_be==j,], - label='Group' + str(j)) - plt.plot(temp_X[temp_be==j,], temp_yhat[temp_be==j,]) - plt.fill_between(temp_X[temp_be==j,], temp_yhat[temp_be==j,] - - 1.96 * np.sqrt(temp_s2[temp_be==j,]), - temp_yhat[temp_be==j,] + - 1.96 * np.sqrt(temp_s2[temp_be==j,]), + scat1 = plt.scatter(temp_X[temp_be == j,], temp_Y[temp_be == j,], + label='Group' + str(j)) + plt.plot(temp_X[temp_be == j,], temp_yhat[temp_be == j,]) + plt.fill_between(temp_X[temp_be == j,], temp_yhat[temp_be == j,] - + 1.96 * np.sqrt(temp_s2[temp_be == j,]), + temp_yhat[temp_be == j,] + + 1.96 * np.sqrt(temp_s2[temp_be == j,]), color='gray', alpha=0.2) # Showing the quantiles resolution = 200 synth_X = np.linspace(-3, 3, resolution) - q = nm.get_mcmc_quantiles(synth_X, batch_effects=j*np.ones(resolution)) + q = nm.get_mcmc_quantiles( + synth_X, batch_effects=j*np.ones(resolution)) col = scat1.get_facecolors()[0] - plt.plot(synth_X, q.T, linewidth=1, color=col, zorder = 0) + plt.plot(synth_X, q.T, linewidth=1, color=col, zorder=0) - plt.title('Model %s, Feature %d' %(model_type, i)) + plt.title('Model %s, Feature %d' % (model_type, i)) plt.legend() plt.show() - ############################## Normative Modelling Test ####################### - + model_type = model_types[0] @@ -99,11 +100,10 @@ os.chdir(working_dir) -estimate(covfile, respfile, testcov=testcov, testresp=testresp, trbefile=trbefile, - tsbefile=tsbefile, alg='hbr', outputsuffix='_' + model_type, - inscaler='None', outscaler='None', model_type=model_type, +estimate(covfile, respfile, testcov=testcov, testresp=testresp, trbefile=trbefile, + tsbefile=tsbefile, alg='hbr', outputsuffix='_' + model_type, + inscaler='None', outscaler='None', model_type=model_type, savemodel='True', saveoutput='True') ############################################################################### - diff --git a/tests/testHBR_transfer.py b/tests/testHBR_transfer.py index 93c3d7f2..029ad6b1 100644 --- a/tests/testHBR_transfer.py +++ b/tests/testHBR_transfer.py @@ -27,14 +27,14 @@ working_dir = '/home/stijn/temp/' # Specifyexit() a working directory - # to save data and results. +# to save data and results. simulation_method = 'linear' n_features = 1 # The number of input features of X -n_grps = 5 # Number of batches in data -n_transfer_groups = 2 # number of batches in transfer data +n_grps = 5 # Number of batches in data +n_transfer_groups = 2 # number of batches in transfer data n_samples = 500 # Number of samples in each group (use a list for different - # sample numbers across different batches) +# sample numbers across different batches) n_transfer_samples = 100 model_types = ['linear'] # models to try @@ -43,16 +43,18 @@ X_train, Y_train, grp_id_train, X_test, Y_test, grp_id_test, coef = \ - simulate_data(simulation_method, n_samples, n_features, n_grps, + simulate_data(simulation_method, n_samples, n_features, n_grps, working_dir=working_dir, plot=True) -X_train_transfer, Y_train_transfer, grp_id_train_transfer, X_test_transfer, Y_test_transfer, grp_id_test_transfer, coef= simulate_data(simulation_method, n_transfer_samples, n_features = n_features, n_grps=n_transfer_groups, plot=True) +X_train_transfer, Y_train_transfer, grp_id_train_transfer, X_test_transfer, Y_test_transfer, grp_id_test_transfer, coef = simulate_data( + simulation_method, n_transfer_samples, n_features=n_features, n_grps=n_transfer_groups, plot=True) ################################# Methods Tests ############################### - - + + for model_type in model_types: - nm = norm_init(X_train, Y_train, alg='hbr',likelihood='Normal', model_type=model_type,n_chains = 4,cores=4,n_samples=100,n_tuning=50, freedom = 5, nknots=8, target_accept="0.99") + nm = norm_init(X_train, Y_train, alg='hbr', likelihood='Normal', model_type=model_type, + n_chains=4, cores=4, n_samples=100, n_tuning=50, freedom=5, nknots=8, target_accept="0.99") print("Now Estimating on original train data ==============================================") nm.estimate(X_train, Y_train, trbefile=working_dir+'trbefile.pkl') @@ -60,56 +62,57 @@ yhat, ys2 = nm.predict(X_test, tsbefile=working_dir+'tsbefile.pkl') for i in range(n_features): - sorted_idx = X_test[:,i].argsort(axis=0).squeeze() - temp_X = X_test[sorted_idx,i] + sorted_idx = X_test[:, i].argsort(axis=0).squeeze() + temp_X = X_test[sorted_idx, i] temp_Y = Y_test[sorted_idx,] - temp_be = grp_id_test[sorted_idx,:].squeeze() + temp_be = grp_id_test[sorted_idx, :].squeeze() temp_yhat = yhat[sorted_idx,] temp_s2 = ys2[sorted_idx,] - + plt.figure() for j in range(n_grps): - plt.scatter(temp_X[temp_be==j,], temp_Y[temp_be==j,], + plt.scatter(temp_X[temp_be == j,], temp_Y[temp_be == j,], label='Group' + str(j)) - plt.plot(temp_X[temp_be==j,], temp_yhat[temp_be==j,]) - plt.fill_between(temp_X[temp_be==j,], temp_yhat[temp_be==j,] - - 1.96 * np.sqrt(temp_s2[temp_be==j,]), - temp_yhat[temp_be==j,] + - 1.96 * np.sqrt(temp_s2[temp_be==j,]), + plt.plot(temp_X[temp_be == j,], temp_yhat[temp_be == j,]) + plt.fill_between(temp_X[temp_be == j,], temp_yhat[temp_be == j,] - + 1.96 * np.sqrt(temp_s2[temp_be == j,]), + temp_yhat[temp_be == j,] + + 1.96 * np.sqrt(temp_s2[temp_be == j,]), color='gray', alpha=0.2) - plt.title('Model %s, Feature %d' %(model_type, i)) + plt.title('Model %s, Feature %d' % (model_type, i)) plt.legend() plt.show() print("Now Estimating on transfer train data ==============================================") - nm.estimate_on_new_sites(X_train_transfer, Y_train_transfer, grp_id_train_transfer) + nm.estimate_on_new_sites( + X_train_transfer, Y_train_transfer, grp_id_train_transfer) print("Now Estimating on transfer test data ==============================================") yhat, s2 = nm.predict_on_new_sites(X_test_transfer, grp_id_test_transfer) for i in range(n_features): - sorted_idx = X_test_transfer[:,i].argsort(axis=0).squeeze() - temp_X = X_test_transfer[sorted_idx,i] + sorted_idx = X_test_transfer[:, i].argsort(axis=0).squeeze() + temp_X = X_test_transfer[sorted_idx, i] temp_Y = Y_test_transfer[sorted_idx,] - temp_be = grp_id_test_transfer[sorted_idx,:].squeeze() + temp_be = grp_id_test_transfer[sorted_idx, :].squeeze() temp_yhat = yhat[sorted_idx,] temp_s2 = ys2[sorted_idx,] - + for j in range(n_transfer_groups): - plt.scatter(temp_X[temp_be==j,], temp_Y[temp_be==j,], + plt.scatter(temp_X[temp_be == j,], temp_Y[temp_be == j,], label='Group' + str(j)) - plt.plot(temp_X[temp_be==j,], temp_yhat[temp_be==j,]) - plt.fill_between(temp_X[temp_be==j,], temp_yhat[temp_be==j,] - - 1.96 * np.sqrt(temp_s2[temp_be==j,]), - temp_yhat[temp_be==j,] + - 1.96 * np.sqrt(temp_s2[temp_be==j,]), + plt.plot(temp_X[temp_be == j,], temp_yhat[temp_be == j,]) + plt.fill_between(temp_X[temp_be == j,], temp_yhat[temp_be == j,] - + 1.96 * np.sqrt(temp_s2[temp_be == j,]), + temp_yhat[temp_be == j,] + + 1.96 * np.sqrt(temp_s2[temp_be == j,]), color='gray', alpha=0.2) - plt.title('Transfer model %s, Feature %d' %(model_type, i)) + plt.title('Transfer model %s, Feature %d' % (model_type, i)) plt.legend() plt.show() ############################## Normative Modelling Test ####################### - + model_type = model_types[0] @@ -122,11 +125,10 @@ os.chdir(working_dir) -estimate(covfile, respfile, testcov=testcov, testresp=testresp, trbefile=trbefile, - tsbefile=tsbefile, alg='hbr', outputsuffix='_' + model_type, - inscaler='None', outscaler='None', model_type=model_type, +estimate(covfile, respfile, testcov=testcov, testresp=testresp, trbefile=trbefile, + tsbefile=tsbefile, alg='hbr', outputsuffix='_' + model_type, + inscaler='None', outscaler='None', model_type=model_type, savemodel='True', saveoutput='True') ############################################################################### - diff --git a/tests/test_NP.py b/tests/test_NP.py index 8b720d25..9f1b77a3 100644 --- a/tests/test_NP.py +++ b/tests/test_NP.py @@ -8,8 +8,11 @@ from pcntoolkit.normative_NP import estimate + class struct(object): pass + + args = struct() args.batchnum = 10 args.epochs = 100 diff --git a/tests/test_ST_NP.py b/tests/test_ST_NP.py index d6d86572..97588dbb 100644 --- a/tests/test_ST_NP.py +++ b/tests/test_ST_NP.py @@ -13,31 +13,34 @@ from pcntoolkit.normative_model.norm_utils import norm_init sample_num = 1114 -X_train = np.random.rand(sample_num,1) * 5 - 2 -Y_train = - 2 * X_train**2 + 2 * X_train + 1 + X_train * np.random.randn(sample_num,1) -X_test = np.random.rand(sample_num,1) * 5 - 2 -Y_test = - 2 * X_test**2 + 2 * X_test + 1 + X_test * np.random.randn(sample_num,1) +X_train = np.random.rand(sample_num, 1) * 5 - 2 +Y_train = - 2 * X_train**2 + 2 * X_train + 1 + \ + X_train * np.random.randn(sample_num, 1) +X_test = np.random.rand(sample_num, 1) * 5 - 2 +Y_test = - 2 * X_test**2 + 2 * X_test + 1 + \ + X_test * np.random.randn(sample_num, 1) configparam = dict() configparam['batch_size'] = 10 configparam['epochs'] = 100 configparam['m'] = 200 -configparam['hidden_neuron_num'] = 10 -configparam['r_dim'] = 5 -configparam['z_dim'] = 3 -configparam['nv'] = 0.01 +configparam['hidden_neuron_num'] = 10 +configparam['r_dim'] = 5 +configparam['z_dim'] = 3 +configparam['nv'] = 0.01 configparam['device'] = torch.device('cpu') with open('NP_configs.pkl', 'wb') as file: - pickle.dump(configparam,file) - + pickle.dump(configparam, file) + nm = norm_init(X_train, Y_train, alg='np', configparam='NP_configs.pkl') nm.estimate(X_train, Y_train) y_hat, ys2 = nm.predict(X_test) fig = plt.figure() ax1 = fig.add_subplot(111) -ax1.scatter(X_test,Y_test, label='Test Data') -ax1.errorbar(X_test, y_hat, yerr=1.96 * np.sqrt(ys2).squeeze(), fmt='.',c='y',alpha=0.2, label='95% Prediction Intervals') +ax1.scatter(X_test, Y_test, label='Test Data') +ax1.errorbar(X_test, y_hat, yerr=1.96 * np.sqrt(ys2).squeeze(), + fmt='.', c='y', alpha=0.2, label='95% Prediction Intervals') ax1.scatter(X_test, y_hat, c='r', label='Prediction') ax1.set_title('Estimated Function') ax1.set_xlabel('X') diff --git a/tests/test_blr.py b/tests/test_blr.py index ab8477d0..59be02c2 100644 --- a/tests/test_blr.py +++ b/tests/test_blr.py @@ -1,6 +1,6 @@ import sys -#sys.path.append('/home/preclineu/andmar/sfw/PCNtoolkit/pcntoolkit') -#sys.path.append('/home/preclineu/chafra/Desktop/PCNtoolkit/') +# sys.path.append('/home/preclineu/andmar/sfw/PCNtoolkit/pcntoolkit') +# sys.path.append('/home/preclineu/chafra/Desktop/PCNtoolkit/') import numpy as np import scipy as sp @@ -11,66 +11,72 @@ from pcntoolkit.model.gp import GPR from pcntoolkit.util.utils import WarpBoxCox, WarpAffine, WarpCompose, WarpSinArcsinh + def create_noise(type_noise, N, parameters=None): """Function to create different noise distributions""" if type_noise == 'exp': scale = parameters - n = 2*np.random.exponential(scale,N) + n = 2*np.random.exponential(scale, N) elif type_noise == 'gamma': shape = parameters - n = 2*np.random.gamma(shape,scale = 2, size = N) + n = 2*np.random.gamma(shape, scale=2, size=N) elif type_noise == 'skewed_right': - gaussian_rv = np.random.normal(0,1,N) - n = np.concatenate((2.5*gaussian_rv[gaussian_rv>0], np.random.normal(0,1,np.abs(N-len(gaussian_rv[gaussian_rv>0]))))) + gaussian_rv = np.random.normal(0, 1, N) + n = np.concatenate((2.5*gaussian_rv[gaussian_rv > 0], np.random.normal( + 0, 1, np.abs(N-len(gaussian_rv[gaussian_rv > 0]))))) elif type_noise == 'skewed_left': - gaussian_rv = np.random.normal(0,1,N) - n = np.concatenate((2.5*gaussian_rv[gaussian_rv<0], np.random.normal(0,1,np.abs(N-len(gaussian_rv[gaussian_rv<0]))))) + gaussian_rv = np.random.normal(0, 1, N) + n = np.concatenate((2.5*gaussian_rv[gaussian_rv < 0], np.random.normal( + 0, 1, np.abs(N-len(gaussian_rv[gaussian_rv < 0]))))) elif type_noise == 'heavy_tailed': - n = np.concatenate((np.random.normal(0,1,int(N/2))*2.5, np.random.normal(0,1,int(N/2)))) + n = np.concatenate((np.random.normal(0, 1, int(N/2)) + * 2.5, np.random.normal(0, 1, int(N/2)))) elif type_noise == 'light_tailed': - n = np.random.normal(0,0.6,N) + n = np.random.normal(0, 0.6, N) elif type_noise == 'gaussian': mu = 0 sigma = parameters - n = np.random.normal(mu,sigma,N) + n = np.random.normal(mu, sigma, N) elif type_noise == 'bimodal': N = int(N/2) - x1 = 2*np.random.normal(-1,0.25,N) - x2 = np.random.normal(1,0.25,N) + x1 = 2*np.random.normal(-1, 0.25, N) + x2 = np.random.normal(1, 0.25, N) n = np.concatenate([x1, x2]) elif type_noise == 'skewed_bimodal': N = int(N/2) - x1 = 2*np.random.normal(-1,0.25,N) - x2 = np.random.normal(1,0.25,N) - gaussian_rv = np.random.normal(0,1,N) - x2 = np.concatenate((10*gaussian_rv[gaussian_rv>0], np.random.normal(0,1,np.abs(N-len(gaussian_rv[gaussian_rv>0]))))) + x1 = 2*np.random.normal(-1, 0.25, N) + x2 = np.random.normal(1, 0.25, N) + gaussian_rv = np.random.normal(0, 1, N) + x2 = np.concatenate((10*gaussian_rv[gaussian_rv > 0], np.random.normal( + 0, 1, np.abs(N-len(gaussian_rv[gaussian_rv > 0]))))) n = np.concatenate([x1, x2]) elif type_noise == 'tdist': dof = parameters - n = np.random.standard_t(dof, N) + n = np.random.standard_t(dof, N) plt.figure() plt.hist(n, bins=50) plt.title('Noise distribution') return n + print('First do a simple evaluation of B-splines regression...') # generate some data -X = np.arange(0,10,0.05) -Xs = np.arange(0,10,0.01) +X = np.arange(0, 10, 0.05) +Xs = np.arange(0, 10, 0.01) N = len(X) dimpoly = 3 # Polynomial basis (used for data generation) -Phip = np.zeros((X.shape[0],dimpoly)) -colid = np.arange(0,1) -for d in range(1,dimpoly+1): - Phip[:,colid] = np.vstack(X ** d) +Phip = np.zeros((X.shape[0], dimpoly)) +colid = np.arange(0, 1) +for d in range(1, dimpoly+1): + Phip[:, colid] = np.vstack(X ** d) colid += 1 -Phips = np.zeros((Xs.shape[0],dimpoly)) -colid = np.arange(0,1) -for d in range(1,dimpoly+1): - Phips[:,colid] = np.vstack(Xs ** d) +Phips = np.zeros((Xs.shape[0], dimpoly)) +colid = np.arange(0, 1) +for d in range(1, dimpoly+1): + Phips[:, colid] = np.vstack(Xs ** d) colid += 1 # generative model @@ -89,91 +95,92 @@ def create_noise(type_noise, N, parameters=None): # cubic B-spline basis (used for regression) p = 3 # order of spline (3 = cubic) nknots = 5 # number of knots (endpoints only counted once) -knots = np.linspace(0,10,nknots) +knots = np.linspace(0, 10, nknots) k = splinelab.augknt(knots, p) # pad the knot vector -B = bspline.Bspline(k, p) +B = bspline.Bspline(k, p) Phi = np.array([B(i) for i in X]) Phis = np.array([B(i) for i in Xs]) hyp0 = np.zeros(2) -#hyp0 = np.zeros(4) # use ARD -#B = BLR(hyp0, Phi, y) +# hyp0 = np.zeros(4) # use ARD +# B = BLR(hyp0, Phi, y) B = BLR() hyp = B.estimate(hyp0, Phi, y, optimizer='powell') -yhat,s2 = B.predict(hyp, Phi, y, Phis) +yhat, s2 = B.predict(hyp, Phi, y, Phis) plt.figure() -plt.fill_between(Xs, yhat-1.96*np.sqrt(s2), yhat+1.96*np.sqrt(s2), alpha = 0.2) -plt.scatter(X,y) -plt.plot(Xs,yhat) +plt.fill_between(Xs, yhat-1.96*np.sqrt(s2), yhat+1.96*np.sqrt(s2), alpha=0.2) +plt.scatter(X, y) +plt.plot(Xs, yhat) plt.show() print(B.nlZ) print(1/hyp) print(B.m) -print('demonstrate likelihood warping ...' ) +print('demonstrate likelihood warping ...') # generative model -#b = [0.4, -0.01, 0.] # true regression coefficients -#s2 = 0.1 # noise variance +# b = [0.4, -0.01, 0.] # true regression coefficients +# s2 = 0.1 # noise variance # y = Phip.dot(b) + 2*np.random.exponential(1,N) -plt.scatter(X,y) +plt.scatter(X, y) W = WarpBoxCox() -#W = WarpSinArcsinh() +# W = WarpSinArcsinh() Phix = X[:, np.newaxis] Phixs = Xs[:, np.newaxis] Bw = BLR(warp=W) -#hyp0 = 0.1*np.ones(2+W.get_n_params()) -#hyp = Bw.estimate(hyp0, Phi, y, optimizer='powell') -#yhat, s2 = Bw.predict(hyp, Phi, y, Phis) +# hyp0 = 0.1*np.ones(2+W.get_n_params()) +# hyp = Bw.estimate(hyp0, Phi, y, optimizer='powell') +# yhat, s2 = Bw.predict(hyp, Phi, y, Phis) hyp0 = 0.1*np.ones(2+W.get_n_params()) hyp = Bw.estimate(hyp0, Phi, y, optimizer='powell', var_covariates=Phix) yhat, s2 = Bw.predict(hyp, Phi, y, Phis, var_covariates_test=Phixs) -warp_param = hyp[1:W.get_n_params()+1] +warp_param = hyp[1:W.get_n_params()+1] med, pr_int = W.warp_predictions(yhat, s2, warp_param) plt.plot(Xs, med, 'b') -plt.fill_between(Xs, pr_int[:,0], pr_int[:,1], alpha = 0.2,color='blue') +plt.fill_between(Xs, pr_int[:, 0], pr_int[:, 1], alpha=0.2, color='blue') # for the Box-Cox warp use closed form expression for the mode (+ve support) if len(warp_param == 1): lam = np.exp(warp_param[0]) mod = (0.5*(1+lam*yhat + np.sqrt((1+lam*yhat)**2 + 4*s2*lam*(lam-1))))**(1/lam) - plt.plot(Xs,mod,'b--') - plt.legend(('median','mode')) + plt.plot(Xs, mod, 'b--') + plt.legend(('median', 'mode')) plt.show() -xx=np.linspace(-5,5,100) -plt.plot(xx,W.invf(xx,warp_param)) +xx = np.linspace(-5, 5, 100) +plt.plot(xx, W.invf(xx, warp_param)) plt.title('estimated warping function') plt.show() # estimate a model with heteroskedastic noise -print('demonstrate heteroskedastic noise...' ) +print('demonstrate heteroskedastic noise...') # generative model b = [0.4, -0.01, 0.] # true regression coefficients s2 = 0.1 # noise variance -y = Phip.dot(b) + Phip[:,0]*np.random.normal(size=N) -plt.scatter(X,y) +y = Phip.dot(b) + Phip[:, 0]*np.random.normal(size=N) +plt.scatter(X, y) # new version Bh = BLR() hyp0 = np.zeros(8) -hyp = Bh.estimate(hyp0, Phi, y, optimizer='l-bfgs-b', var_covariates=Phi, verbose=True) -yhat,s2 = Bh.predict(hyp, Phi, y, Phis, var_covariates_test=Phis) +hyp = Bh.estimate(hyp0, Phi, y, optimizer='l-bfgs-b', + var_covariates=Phi, verbose=True) +yhat, s2 = Bh.predict(hyp, Phi, y, Phis, var_covariates_test=Phis) # old version -#Bh = BLR(hetero_noise=7) -#hyp0 = np.zeros(8) -#hyp = Bh.estimate(hyp0, Phi, y, optimizer='l-bfgs-b', hetero_noise=7, verbose=True) -#yhat,s2 = Bh.predict(hyp, Phi, y, Phis) +# Bh = BLR(hetero_noise=7) +# hyp0 = np.zeros(8) +# hyp = Bh.estimate(hyp0, Phi, y, optimizer='l-bfgs-b', hetero_noise=7, verbose=True) +# yhat,s2 = Bh.predict(hyp, Phi, y, Phis) print(hyp) -plt.fill_between(Xs, yhat-1.96*np.sqrt(s2), yhat+1.96*np.sqrt(s2), alpha = 0.2) +plt.fill_between(Xs, yhat-1.96*np.sqrt(s2), yhat+1.96*np.sqrt(s2), alpha=0.2) plt.show() print("Estimate a model with site-specific noise ...") @@ -194,42 +201,42 @@ def create_noise(type_noise, N, parameters=None): for i in range(n_site): sids[idx[i]] = i+1 sids_te[idx_te[i]] = i+1 -cols = ['blue','red','green'] +cols = ['blue', 'red', 'green'] # generative model -b0 = [-0.5, 0.25, -0.3] # intercepts -bh = [-0.025, 0.001, 0] # slopes -s2h = [0.01, 0.2, 0.1] # noise +b0 = [-0.5, 0.25, -0.3] # intercepts +bh = [-0.025, 0.001, 0] # slopes +s2h = [0.01, 0.2, 0.1] # noise y = Phip.dot(bh) # add intercepts and heteroskedastic noise for s in range(n_site): sidx = np.where(sids == s+1)[0] - y[sidx] += b0[s] + np.random.normal(0,np.sqrt(s2h[s]),len(sidx)) + y[sidx] += b0[s] + np.random.normal(0, np.sqrt(s2h[s]), len(sidx)) # add the site specific intercepts to the design matrix for s in range(n_site): - site = np.zeros((N,1)) + site = np.zeros((N, 1)) site[idx[s]] = 1 Phi = np.concatenate((Phi, site), axis=1) - - site_te = np.zeros((Xs.shape[0],1)) + + site_te = np.zeros((Xs.shape[0], 1)) site_te[idx_te[s]] = 1 Phis = np.concatenate((Phis, site_te), axis=1) -hyp0=np.zeros(4) +hyp0 = np.zeros(4) Bh = BLR(var_groups=sids) Bh.loglik(hyp0, Phi, y) Bh.dloglik(hyp0, Phi, y) hyp = Bh.estimate(hyp0, Phi, y) -yhat,s2 = Bh.predict(hyp, Phi, y, Phis, var_groups_test=sids_te) +yhat, s2 = Bh.predict(hyp, Phi, y, Phis, var_groups_test=sids_te) for s in range(n_site): plt.scatter(X[idx[s]], y[idx[s]]) - plt.plot(Xs[idx_te[s]],yhat[idx_te[s]], color=cols[s]) - plt.fill_between(Xs[idx_te[s]], - yhat[idx_te[s]] - 1.96 * np.sqrt(s2[idx_te[s]]), - yhat[idx_te[s]] + 1.96 * np.sqrt(s2[idx_te[s]]), - alpha=0.2, color=cols[s]) + plt.plot(Xs[idx_te[s]], yhat[idx_te[s]], color=cols[s]) + plt.fill_between(Xs[idx_te[s]], + yhat[idx_te[s]] - 1.96 * np.sqrt(s2[idx_te[s]]), + yhat[idx_te[s]] + 1.96 * np.sqrt(s2[idx_te[s]]), + alpha=0.2, color=cols[s]) plt.show() diff --git a/tests/test_gpr.py b/tests/test_gpr.py index 179a7768..f6909132 100644 --- a/tests/test_gpr.py +++ b/tests/test_gpr.py @@ -4,166 +4,166 @@ @author: andmar """ +from model.gp import GPR, CovSqExp, CovSqExpARD, CovLin import sys import numpy as np from matplotlib import pyplot as plt # load as a module sys.path.append('/home/mrstats/andmar/sfw/PCNtoolkit/pcntoolkit') -from model.gp import GPR, CovSqExp, CovSqExpARD, CovLin # load from the installed package -#from pcntoolkit.gp import GPR, covSqExp +# from pcntoolkit.gp import GPR, covSqExp X = np.asarray([2.08397042775073, --0.821018066101379, --0.617870699182597, --1.18382260886069, -0.274087442277144, -0.599441729295593, -1.76889791920444, --0.465645549031928, -0.588852784375935, --0.832982214438054, --0.512106527960363, -0.277883144210116, --0.0658704269222113, --0.821412363806325, -0.185399443778088, --0.858296174995998, -0.370786630037059, --1.40986916241664, --0.144668412325022, --0.553299615220374]) + -0.821018066101379, + -0.617870699182597, + -1.18382260886069, + 0.274087442277144, + 0.599441729295593, + 1.76889791920444, + -0.465645549031928, + 0.588852784375935, + -0.832982214438054, + -0.512106527960363, + 0.277883144210116, + -0.0658704269222113, + -0.821412363806325, + 0.185399443778088, + -0.858296174995998, + 0.370786630037059, + -1.40986916241664, + -0.144668412325022, + -0.553299615220374]) y = np.asarray([4.54920374633170, -0.371985574437271, -0.711307965514790, --0.0132128936184299, -2.25547325533819, -1.00991574929573, -3.74467593796503, -0.424592771793202, -1.32283365229581, -0.278298293510020, -0.267229130945574, -2.20011228672383, -1.20060998330897, -0.439971697236094, -2.62858043351126, -0.503774817336353, -1.94252531382056, -0.579133950013327, -0.670874423968554, -0.377353755100965]) + 0.371985574437271, + 0.711307965514790, + -0.0132128936184299, + 2.25547325533819, + 1.00991574929573, + 3.74467593796503, + 0.424592771793202, + 1.32283365229581, + 0.278298293510020, + 0.267229130945574, + 2.20011228672383, + 1.20060998330897, + 0.439971697236094, + 2.62858043351126, + 0.503774817336353, + 1.94252531382056, + 0.579133950013327, + 0.670874423968554, + 0.377353755100965]) -#y = y-np.mean(y) +# y = y-np.mean(y) Xs = np.asarray([-1.90000000000000, --1.86200000000000, --1.82400000000000, --1.78600000000000, --1.74800000000000, --1.71000000000000, --1.67200000000000, --1.63400000000000, --1.59600000000000, --1.55800000000000, --1.52000000000000, --1.48200000000000, --1.44400000000000, --1.40600000000000, --1.36800000000000, --1.33000000000000, --1.29200000000000, --1.25400000000000, --1.21600000000000, --1.17800000000000, --1.14000000000000, --1.10200000000000, --1.06400000000000, --1.02600000000000, --0.988000000000000, --0.950000000000000, --0.912000000000000, --0.874000000000000, --0.836000000000000, --0.798000000000000, --0.760000000000000, --0.722000000000000, --0.684000000000000, --0.646000000000000, --0.608000000000000, --0.570000000000000, --0.532000000000000, --0.494000000000000, --0.456000000000000, --0.418000000000000, --0.380000000000000, --0.342000000000000, --0.304000000000000, --0.266000000000000, --0.228000000000000, --0.190000000000000, --0.152000000000000, --0.114000000000000, --0.0760000000000001, --0.0380000000000000, -0, -0.0379999999999998, -0.0760000000000001, -0.114000000000000, -0.152000000000000, -0.190000000000000, -0.228000000000000, -0.266000000000000, -0.304000000000000, -0.342000000000000, -0.380000000000000, -0.418000000000000, -0.456000000000000, -0.494000000000000, -0.532000000000000, -0.570000000000000, -0.608000000000000, -0.646000000000000, -0.684000000000000, -0.722000000000000, -0.760000000000000, -0.798000000000000, -0.836000000000000, -0.874000000000000, -0.912000000000000, -0.950000000000000, -0.988000000000000, -1.02600000000000, -1.06400000000000, -1.10200000000000, -1.14000000000000, -1.17800000000000, -1.21600000000000, -1.25400000000000, -1.29200000000000, -1.33000000000000, -1.36800000000000, -1.40600000000000, -1.44400000000000, -1.48200000000000, -1.52000000000000, -1.55800000000000, -1.59600000000000, -1.63400000000000, -1.67200000000000, -1.71000000000000, -1.74800000000000, -1.78600000000000, -1.82400000000000, -1.86200000000000, -1.90000000000000]) + -1.86200000000000, + -1.82400000000000, + -1.78600000000000, + -1.74800000000000, + -1.71000000000000, + -1.67200000000000, + -1.63400000000000, + -1.59600000000000, + -1.55800000000000, + -1.52000000000000, + -1.48200000000000, + -1.44400000000000, + -1.40600000000000, + -1.36800000000000, + -1.33000000000000, + -1.29200000000000, + -1.25400000000000, + -1.21600000000000, + -1.17800000000000, + -1.14000000000000, + -1.10200000000000, + -1.06400000000000, + -1.02600000000000, + -0.988000000000000, + -0.950000000000000, + -0.912000000000000, + -0.874000000000000, + -0.836000000000000, + -0.798000000000000, + -0.760000000000000, + -0.722000000000000, + -0.684000000000000, + -0.646000000000000, + -0.608000000000000, + -0.570000000000000, + -0.532000000000000, + -0.494000000000000, + -0.456000000000000, + -0.418000000000000, + -0.380000000000000, + -0.342000000000000, + -0.304000000000000, + -0.266000000000000, + -0.228000000000000, + -0.190000000000000, + -0.152000000000000, + -0.114000000000000, + -0.0760000000000001, + -0.0380000000000000, + 0, + 0.0379999999999998, + 0.0760000000000001, + 0.114000000000000, + 0.152000000000000, + 0.190000000000000, + 0.228000000000000, + 0.266000000000000, + 0.304000000000000, + 0.342000000000000, + 0.380000000000000, + 0.418000000000000, + 0.456000000000000, + 0.494000000000000, + 0.532000000000000, + 0.570000000000000, + 0.608000000000000, + 0.646000000000000, + 0.684000000000000, + 0.722000000000000, + 0.760000000000000, + 0.798000000000000, + 0.836000000000000, + 0.874000000000000, + 0.912000000000000, + 0.950000000000000, + 0.988000000000000, + 1.02600000000000, + 1.06400000000000, + 1.10200000000000, + 1.14000000000000, + 1.17800000000000, + 1.21600000000000, + 1.25400000000000, + 1.29200000000000, + 1.33000000000000, + 1.36800000000000, + 1.40600000000000, + 1.44400000000000, + 1.48200000000000, + 1.52000000000000, + 1.55800000000000, + 1.59600000000000, + 1.63400000000000, + 1.67200000000000, + 1.71000000000000, + 1.74800000000000, + 1.78600000000000, + 1.82400000000000, + 1.86200000000000, + 1.90000000000000]) N = len(X) -X = np.c_[X, np.ones(N), np.arange(1,N+1)] +X = np.c_[X, np.ones(N), np.arange(1, N+1)] cov = CovSqExp(X) hyp0 = np.zeros(3) @@ -171,11 +171,11 @@ cov = CovSqExpARD(X) hyp0 = np.zeros(X.shape[1]+2) -#cov = covLin -#hyp0 = np.asarray((0, None)) +# cov = covLin +# hyp0 = np.asarray((0, None)) G = GPR(hyp0, cov, X, y) G.loglik(hyp0, cov, X, y) G.dloglik(hyp0, cov, X, y) -hyp = G.estimate(hyp0,cov, X, y) -yhat,s2 = G.predict(hyp0,X,y,X) +hyp = G.estimate(hyp0, cov, X, y) +yhat, s2 = G.predict(hyp0, X, y, X) diff --git a/tests/test_hbr_pymc.py b/tests/test_hbr_pymc.py index 49935864..e79ab02c 100644 --- a/tests/test_hbr_pymc.py +++ b/tests/test_hbr_pymc.py @@ -14,18 +14,19 @@ def main(): - fcon_tr = pd.read_csv('https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/main/data/fcon1000_tr.csv') - fcon_te = pd.read_csv('https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/main/data/fcon1000_te.csv') + fcon_tr = pd.read_csv( + 'https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/main/data/fcon1000_tr.csv') + fcon_te = pd.read_csv( + 'https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/main/data/fcon1000_te.csv') idps = ['rh_MeanThickness_thickness'] covs = ['age'] - batch_effects = ['sitenum','sex'] + batch_effects = ['sitenum', 'sex'] X_train = fcon_tr[covs].to_numpy(dtype=float) Y_train = fcon_tr[idps].to_numpy(dtype=float) batch_effects_train = fcon_tr[batch_effects].to_numpy(dtype=int) - X_test = fcon_te[covs].to_numpy(dtype=float) Y_test = fcon_te[idps].to_numpy(dtype=float) batch_effects_test = fcon_te[batch_effects].to_numpy(dtype=int) @@ -35,67 +36,67 @@ def main(): with open('X_train.pkl', 'wb') as file: pickle.dump(pd.DataFrame(X_train), file) with open('Y_train.pkl', 'wb') as file: - pickle.dump(pd.DataFrame(Y_train), file) + pickle.dump(pd.DataFrame(Y_train), file) with open('trbefile.pkl', 'wb') as file: - pickle.dump(pd.DataFrame(batch_effects_train), file) + pickle.dump(pd.DataFrame(batch_effects_train), file) with open('X_test.pkl', 'wb') as file: pickle.dump(pd.DataFrame(X_test), file) with open('Y_test.pkl', 'wb') as file: - pickle.dump(pd.DataFrame(Y_test), file) + pickle.dump(pd.DataFrame(Y_test), file) with open('tsbefile.pkl', 'wb') as file: - pickle.dump(pd.DataFrame(batch_effects_test), file) + pickle.dump(pd.DataFrame(batch_effects_test), file) - # a simple function to quickly load pickle files - def ldpkl(filename: str): + # a simple function to quickly load pickle files + def ldpkl(filename: str): with open(filename, 'rb') as f: return pickle.load(f) - respfile = os.path.join(processing_dir, 'Y_train.pkl') - covfile = os.path.join(processing_dir, 'X_train.pkl') + respfile = os.path.join(processing_dir, 'Y_train.pkl') + covfile = os.path.join(processing_dir, 'X_train.pkl') testrespfile_path = os.path.join(processing_dir, 'Y_test.pkl') - testcovfile_path = os.path.join(processing_dir, 'X_test.pkl') + testcovfile_path = os.path.join(processing_dir, 'X_test.pkl') trbefile = os.path.join(processing_dir, 'trbefile.pkl') tsbefile = os.path.join(processing_dir, 'tsbefile.pkl') - output_path = os.path.join(processing_dir, 'Models/') - log_dir = os.path.join(processing_dir, 'log/') + output_path = os.path.join(processing_dir, 'Models/') + log_dir = os.path.join(processing_dir, 'log/') if not os.path.isdir(output_path): os.mkdir(output_path) if not os.path.isdir(log_dir): os.mkdir(log_dir) + outputsuffix = '_estimate' + nm = ptk.normative.estimate(covfile=covfile, + respfile=respfile, + trbefile=trbefile, + testcov=testcovfile_path, + testresp=testrespfile_path, + tsbefile=tsbefile, + alg='hbr', + likelihood='Normal', + # model_type='bspline', + linear_mu='False', + random_intercept_mu='False', + random_slope_mu='False', + random_intercept_sigma='False', + random_sigma='False', + random_mu='False', + log_path=log_dir, + binary='True', + n_samples=17, + n_tuning=13, + n_chains=1, + cores=1, + target_accept=0.99, + init='adapt_diag', + inscaler='standardize', + outscaler='standardize', + output_path=output_path, + outputsuffix=outputsuffix, + savemodel=True) + - outputsuffix = '_estimate' - nm = ptk.normative.estimate(covfile=covfile, - respfile=respfile, - trbefile=trbefile, - testcov=testcovfile_path, - testresp=testrespfile_path, - tsbefile=tsbefile, - alg='hbr', - likelihood='Normal', - # model_type='bspline', - linear_mu='False', - random_intercept_mu = 'False', - random_slope_mu = 'False', - random_intercept_sigma='False', - random_sigma='False', - random_mu='False', - log_path=log_dir, - binary='True', - n_samples=17, - n_tuning=13, - n_chains=1, - cores=1, - target_accept=0.99, - init='adapt_diag', - inscaler='standardize', - outscaler='standardize', - output_path=output_path, - outputsuffix=outputsuffix, - savemodel=True) - -if __name__=="__main__": - main() \ No newline at end of file +if __name__ == "__main__": + main() diff --git a/tests/test_normative.py b/tests/test_normative.py index 55999758..666bce49 100644 --- a/tests/test_normative.py +++ b/tests/test_normative.py @@ -4,45 +4,46 @@ @author: andmar """ -#import pcntoolkit +# import pcntoolkit +from normative import estimate import os import sys -#from pcntoolkit.normative import estimate +# from pcntoolkit.normative import estimate sys.path.append('/home/preclineu/andmar/sfw/PCNtoolkit/pcntoolkit') -from normative import estimate -#wdir = '/home/mrstats/andmar/py.sandbox/normative_nimg' -##wdir = '/Users/andre/data/normative_nimg' -#maskfile = os.path.join(wdir, 'mask_3mm_left_striatum.nii.gz') -#respfile = os.path.join(wdir, 'shoot_data_3mm_n50.nii.gz') -#covfile = os.path.join(wdir, 'covariates_basic_n50.txt') -#cvfolds = 2 +# wdir = '/home/mrstats/andmar/py.sandbox/normative_nimg' +# wdir = '/Users/andre/data/normative_nimg' +# maskfile = os.path.join(wdir, 'mask_3mm_left_striatum.nii.gz') +# respfile = os.path.join(wdir, 'shoot_data_3mm_n50.nii.gz') +# covfile = os.path.join(wdir, 'covariates_basic_n50.txt') +# cvfolds = 2 # with test covariates wdir = '/home/preclineu/andmar/py.sandbox/normative_nimg' -##wdir = '/Users/andre/data/normative_nimg' +# wdir = '/Users/andre/data/normative_nimg' maskfile = os.path.join(wdir, 'mask_3mm_left_striatum.nii.gz') respfile = os.path.join(wdir, 'shoot_data_3mm_n500.nii.gz') covfile = os.path.join(wdir, 'covariates_basic_n500.txt') testresp = os.path.join(wdir, 'shoot_data_3mm_last100.nii.gz') testcov = os.path.join(wdir, 'covariates_basic_last100.txt') -estimate(covfile, respfile, maskfile=maskfile, testresp=testresp, testcov=testcov,alg="blr")#, configparam=4) -#cvfolds = 2 +estimate(covfile, respfile, maskfile=maskfile, testresp=testresp, + testcov=testcov, alg="blr") # , configparam=4) +# cvfolds = 2 -#wdir = '/home/mrstats/andmar/py.sandbox/normative_hcp' -#filename = os.path.join(wdir, 'tfmri_gambling_cope2.dtseries.nii') -#covfile = os.path.join(wdir, 'ddscores.txt') -#Nfold = 2 +# wdir = '/home/mrstats/andmar/py.sandbox/normative_hcp' +# filename = os.path.join(wdir, 'tfmri_gambling_cope2.dtseries.nii') +# covfile = os.path.join(wdir, 'ddscores.txt') +# Nfold = 2 -#wdir = '/home/mrstats/andmar/py.sandbox/normative_oslo' -#respfile = os.path.join(wdir, 'ICA100_oslo15_v2.txt') -#covfile = os.path.join(wdir, 'cov_oslo15_v2.txt') -#cvfolds = 2 -#pcntoolkit.normative.estimate(covfile, respfile, cvfolds=cvfolds) +# wdir = '/home/mrstats/andmar/py.sandbox/normative_oslo' +# respfile = os.path.join(wdir, 'ICA100_oslo15_v2.txt') +# covfile = os.path.join(wdir, 'cov_oslo15_v2.txt') +# cvfolds = 2 +# pcntoolkit.normative.estimate(covfile, respfile, cvfolds=cvfolds) -#wdir = '/home/mrstats/andmar/data/enigma_mdd' -#maskfile = None -#filename = os.path.join(wdir, 'Enigma_1st_100sub_resp.txt') -#covfile = os.path.join(wdir, 'Enigma_1st_100sub_cov.txt') -#Nfold = 2 \ No newline at end of file +# wdir = '/home/mrstats/andmar/data/enigma_mdd' +# maskfile = None +# filename = os.path.join(wdir, 'Enigma_1st_100sub_resp.txt') +# covfile = os.path.join(wdir, 'Enigma_1st_100sub_cov.txt') +# Nfold = 2 diff --git a/tests/test_normative_parallel.py b/tests/test_normative_parallel.py index efd7d844..691eca40 100644 --- a/tests/test_normative_parallel.py +++ b/tests/test_normative_parallel.py @@ -4,7 +4,7 @@ @author: andmar """ -#import pcntoolkit +# import pcntoolkit import os import time from pcntoolkit.normative_parallel import execute_nm, collect_nm, delete_nm @@ -17,7 +17,7 @@ python_path = '/home/preclineu/andmar/sfw/anaconda3/envs/py36/bin/python' normative_path = '/home/preclineu/andmar/sfw/PCNtoolkit/pcntoolkit/normative.py' -processing_dir= '/home/preclineu/andmar/py.sandbox/demo/' +processing_dir = '/home/preclineu/andmar/py.sandbox/demo/' job_name = 'nmp_test' batch_size = 10 memory = '4gb' @@ -25,12 +25,11 @@ cluster = 'torque' execute_nm(processing_dir, python_path, normative_path, job_name, covfile, respfile, - batch_size, memory, duration, cluster_spec=cluster, - cv_folds=cvfolds, log_path=processing_dir)#, alg='rfa')#, configparam=4) + batch_size, memory, duration, cluster_spec=cluster, + cv_folds=cvfolds, log_path=processing_dir) # , alg='rfa')#, configparam=4) print("waiting for jobs to finish ...") time.sleep(60) collect_nm(processing_dir, job_name, collect=True) -#delete_nm(procedssing_dir) - +# delete_nm(procedssing_dir) diff --git a/tests/test_rand_feat.py b/tests/test_rand_feat.py index 601e5b49..06db4aac 100644 --- a/tests/test_rand_feat.py +++ b/tests/test_rand_feat.py @@ -1,3 +1,6 @@ +from model.rfa import GPRRFA +from model.bayesreg import BLR +from model.gp import GPR, CovSqExp, CovSqExpARD, CovLin, CovSum import sys import numpy as np import torch @@ -7,36 +10,36 @@ # load as a module sys.path.append('/home/mrstats/andmar/sfw/PCNtoolkit/pcntoolkit') -from model.gp import GPR, CovSqExp, CovSqExpARD, CovLin, CovSum -from model.bayesreg import BLR -from model.rfa import GPRRFA + def plot_dist(x, mean, lb, ub, color_mean=None, color_shading=None): # plot the shaded range of the confidence intervals plt.fill_between(x, ub, lb, color=color_shading, alpha=.5) # plot the mean on top - plt.plot(x,mean, color_mean) + plt.plot(x, mean, color_mean) + def f(X): - y = -0.1*X+0.02*X**2+np.sin(0.9*X) +np.cos(0.1*X) - + y = -0.1*X+0.02*X**2+np.sin(0.9*X) + np.cos(0.1*X) + y = -X + 0.1*X**2 - - #y = -0.1*X+0.02*X**2+0.3*np.sin(0.3*X) + + # y = -0.1*X+0.02*X**2+0.3*np.sin(0.3*X) return 10*y + N = 500 -sn2 = 5 #2 -X = np.random.uniform(-10,10,N) -Xs = np.linspace(-10,10,100) +sn2 = 5 # 2 +X = np.random.uniform(-10, 10, N) +Xs = np.linspace(-10, 10, 100) if len(X.shape) < 2: X = X[:, np.newaxis] if len(Xs.shape) < 2: - Xs = Xs[:, np.newaxis] - -y = f(X) + np.random.normal(0,sn2,X.shape) + Xs = Xs[:, np.newaxis] + +y = f(X) + np.random.normal(0, sn2, X.shape) ys = f(Xs) my = np.mean(y, axis=0) @@ -45,131 +48,131 @@ def f(X): ys = (ys - my) / sy # add a polynomial basis expansion to make sure it works with p > 1 -#X = np.c_[X, X **2] -#Xs = np.c_[Xs, Xs **2] -X = np.c_[X, np.ones((N,1)) , 0.01*np.random.randn(N,1)] -Xs = np.c_[Xs, np.ones((Xs.shape[0],1)), 0.01*np.random.randn(Xs.shape[0],1)] +# X = np.c_[X, X **2] +# Xs = np.c_[Xs, Xs **2] +X = np.c_[X, np.ones((N, 1)), 0.01*np.random.randn(N, 1)] +Xs = np.c_[Xs, np.ones((Xs.shape[0], 1)), 0.01*np.random.randn(Xs.shape[0], 1)] -#cov = CovSqExp(X) -#cov = CovSqExpARD(X) +# cov = CovSqExp(X) +# cov = CovSqExpARD(X) cov = CovSum(X, ('CovLin', 'CovSqExpARD')) hyp0 = np.zeros(cov.get_n_params() + 1) print('running GPR') G = GPR(hyp0, cov, X, y) -hyp = G.estimate(hyp0,cov, X, y) -yhat,s2 = G.predict(hyp,X,y,Xs) +hyp = G.estimate(hyp0, cov, X, y) +yhat, s2 = G.predict(hyp, X, y, Xs) s2 = np.diag(s2) # extract parameters sn2_est = np.exp(2*hyp[0]) ell2_est = np.exp(2*hyp[1:-1]) sf2_est = np.exp(2*hyp[-1]) -print('sn2 =',sn2_est,'ell =',np.sqrt(ell2_est),'sf2=',sf2_est) +print('sn2 =', sn2_est, 'ell =', np.sqrt(ell2_est), 'sf2=', sf2_est) -plt.plot(Xs[:,0],ys,'r') -plt.plot(X[:,0],y,'xr') -plt.plot(Xs[:,0],yhat,'b') -plot_dist(Xs[:,0].ravel(), yhat.ravel(), +plt.plot(Xs[:, 0], ys, 'r') +plt.plot(X[:, 0], y, 'xr') +plt.plot(Xs[:, 0], yhat, 'b') +plot_dist(Xs[:, 0].ravel(), yhat.ravel(), yhat.ravel()-2*np.sqrt(s2).ravel(), - yhat.ravel()+2*np.sqrt(s2).ravel(),'b','b') + yhat.ravel()+2*np.sqrt(s2).ravel(), 'b', 'b') print('running BLR ...') # Random feature approximation # ---------------------------- Nf = 250 D = X.shape[1] -Omega = np.zeros((D,Nf)) +Omega = np.zeros((D, Nf)) for f in range(Nf): - Omega[:,f] = np.sqrt(ell2_est) * np.random.randn(Omega.shape[0]) + Omega[:, f] = np.sqrt(ell2_est) * np.random.randn(Omega.shape[0]) XO = X.dot(Omega) -Phi = np.sqrt(sf2_est/Nf)*np.c_[np.cos(XO),np.sin(XO)] +Phi = np.sqrt(sf2_est/Nf)*np.c_[np.cos(XO), np.sin(XO)] XsO = Xs.dot(Omega) -Phis = np.sqrt(sf2_est/Nf)*np.c_[np.cos(XsO),np.sin(XsO)] +Phis = np.sqrt(sf2_est/Nf)*np.c_[np.cos(XsO), np.sin(XsO)] # add linear component Phi = np.c_[Phi, X] Phis = np.c_[Phis, Xs] hyp_blr = np.asarray([np.log(1/sn2_est), np.log(1)]) -B = BLR()#hyp_blr, Phi, y) +B = BLR() # hyp_blr, Phi, y) B.loglik(hyp_blr, Phi, y) yhat_blr, s2_blr = B.predict(hyp_blr, Phi, y, Phis) -#plt.plot(Xs[:,0],yhat_blr,'y') -#plot_dist(Xs[:,0].ravel(), yhat_blr.ravel(), +# plt.plot(Xs[:,0],yhat_blr,'y') +# plot_dist(Xs[:,0].ravel(), yhat_blr.ravel(), # yhat_blr.ravel() - 2*np.sqrt(s2_blr).ravel(), # yhat_blr.ravel() + 2*np.sqrt(s2_blr).ravel(),'y','y') print('running RFA ...') -R = GPRRFA(hyp, X, y, n_feat = Nf) +R = GPRRFA(hyp, X, y, n_feat=Nf) # find good starting hyperparameters lm = LinearRegression() -lm.fit(create_poly_basis(X,3), y) -yhat = lm.predict(create_poly_basis(X,3)) +lm.fit(create_poly_basis(X, 3), y) +yhat = lm.predict(create_poly_basis(X, 3)) hyp0 = np.zeros(D + 2) hyp0[0] = np.log(np.sqrt(np.var(y - yhat))) -hyp = R.estimate(hyp0,X,y) -yhat_rfa,s2_rfa = R.predict(hyp,X,y,Xs) +hyp = R.estimate(hyp0, X, y) +yhat_rfa, s2_rfa = R.predict(hyp, X, y, Xs) -plot_dist(Xs[:,0].ravel(), yhat_rfa.ravel(), +plot_dist(Xs[:, 0].ravel(), yhat_rfa.ravel(), yhat_rfa.ravel() - 2*np.sqrt(s2_rfa).ravel(), - yhat_rfa.ravel() + 2*np.sqrt(s2_rfa).ravel(),'k','k') + yhat_rfa.ravel() + 2*np.sqrt(s2_rfa).ravel(), 'k', 'k') sn2_est = np.exp(2*hyp[0]) ell2_est = np.exp(2*hyp[1:-1]) sf2_est = np.exp(2*hyp[-1]) -print('sn2 =',sn2_est,'ell =',np.sqrt(ell2_est),'sf2=',sf2_est) - -## Random features (torch) -## ----------------------- -## init -#hyp = torch.tensor(hyp, requires_grad=True) -## hyp = [log(sn), log(ell), log(sf)] -##sn2_est = np.exp(2*hyp[0]) -##ell2_est = np.exp(2*hyp[1:-1]) -##sf2_est = np.exp(2*hyp[-1]) +print('sn2 =', sn2_est, 'ell =', np.sqrt(ell2_est), 'sf2=', sf2_est) + +# Random features (torch) +# ----------------------- +# init +# hyp = torch.tensor(hyp, requires_grad=True) +# hyp = [log(sn), log(ell), log(sf)] +# sn2_est = np.exp(2*hyp[0]) +# ell2_est = np.exp(2*hyp[1:-1]) +# sf2_est = np.exp(2*hyp[-1]) # -#Omega = torch.zeros((D,Nf), dtype=torch.double) -#for f in range(Nf): +# Omega = torch.zeros((D,Nf), dtype=torch.double) +# for f in range(Nf): # Omega[:,f] = torch.exp(hyp[1:-1]) * \ # torch.randn((Omega.shape[0], 1), dtype=torch.double).squeeze() # -##Omega = torch.from_numpy(Omega) -#XO = torch.mm(torch.from_numpy(X), Omega) -#Phi = torch.exp(hyp[-1])/np.sqrt(Nf) * torch.cat((torch.cos(XO), torch.sin(XO)), 1) -## concatenate linear weights -#Phi = torch.cat((Phi, torch.from_numpy(X)), 1) -#N, D = Phi.shape -#y = torch.from_numpy(y) +# Omega = torch.from_numpy(Omega) +# XO = torch.mm(torch.from_numpy(X), Omega) +# Phi = torch.exp(hyp[-1])/np.sqrt(Nf) * torch.cat((torch.cos(XO), torch.sin(XO)), 1) +# concatenate linear weights +# Phi = torch.cat((Phi, torch.from_numpy(X)), 1) +# N, D = Phi.shape +# y = torch.from_numpy(y) # -## post -#iSigma = torch.eye(D, dtype=torch.double) -#A = torch.mm(torch.t(Phi), Phi) / torch.exp(2*hyp[0]) + iSigma -#m = torch.mm(torch.gesv(torch.t(Phi), A)[0], y) / torch.exp(2*hyp[0]) +# post +# iSigma = torch.eye(D, dtype=torch.double) +# A = torch.mm(torch.t(Phi), Phi) / torch.exp(2*hyp[0]) + iSigma +# m = torch.mm(torch.gesv(torch.t(Phi), A)[0], y) / torch.exp(2*hyp[0]) # -## predict -#Xs = torch.from_numpy(Xs) -#XsO = torch.mm(Xs, Omega) -#Phis = torch.exp(hyp[-1])/np.sqrt(Nf) * torch.cat((torch.cos(XsO), torch.sin(XsO)), 1) -#Phis = torch.cat((Phis, Xs), 1) -#ys = torch.mm(Phis, m) +# predict +# Xs = torch.from_numpy(Xs) +# XsO = torch.mm(Xs, Omega) +# Phis = torch.exp(hyp[-1])/np.sqrt(Nf) * torch.cat((torch.cos(XsO), torch.sin(XsO)), 1) +# Phis = torch.cat((Phis, Xs), 1) +# ys = torch.mm(Phis, m) # -##s2_blr = sn2_est + torch.diag(torch.mm(Phis, torch.gesv(torch.t(Phis), A)[0])) -## avoiding computing off-diagonal entries -#s2_blr = torch.exp(2*hyp[0]) + torch.sum(Phis * torch.t(torch.gesv(torch.t(Phis), A)[0]), 1) +# s2_blr = sn2_est + torch.diag(torch.mm(Phis, torch.gesv(torch.t(Phis), A)[0])) +# avoiding computing off-diagonal entries +# s2_blr = torch.exp(2*hyp[0]) + torch.sum(Phis * torch.t(torch.gesv(torch.t(Phis), A)[0]), 1) # # -#logdetA = 2*torch.sum(torch.log(torch.diag(torch.cholesky(A)))) +# logdetA = 2*torch.sum(torch.log(torch.diag(torch.cholesky(A)))) # -## compute negative marginal log likelihood -#nlZ = -0.5 * (N*torch.log(1/torch.exp(2*hyp[0])) - N*np.log(2*np.pi) - +# compute negative marginal log likelihood +# nlZ = -0.5 * (N*torch.log(1/torch.exp(2*hyp[0])) - N*np.log(2*np.pi) - # torch.mm(torch.t(y-torch.mm(Phi,m)), (y-torch.mm(Phi,m)))/torch.exp(2*hyp[0]) - # torch.mm(torch.t(m), m) - # logdetA # ) -#nlZ.backward() +# nlZ.backward() # -#plot_dist(Xs[:,0].detach().numpy().ravel(), ys.detach().numpy(), +# plot_dist(Xs[:,0].detach().numpy().ravel(), ys.detach().numpy(), # ys.detach().numpy().ravel() - 2*np.sqrt(s2_blr.detach().numpy()).ravel(), -# ys.detach().numpy().ravel() + 2*np.sqrt(s2_blr.detach().numpy()).ravel(),'k','k') \ No newline at end of file +# ys.detach().numpy().ravel() + 2*np.sqrt(s2_blr.detach().numpy()).ravel(),'k','k') diff --git a/tests/unit_tests.py b/tests/unit_tests.py index 4adae936..b164f7b2 100644 --- a/tests/unit_tests.py +++ b/tests/unit_tests.py @@ -3,8 +3,8 @@ import glob import shutil -# Unit testing script for testing the basic functionality of this package. -# This largely only tests input and output routines. More detailed unit testing +# Unit testing script for testing the basic functionality of this package. +# This largely only tests input and output routines. More detailed unit testing # of underlying algorithms etc. should be done separately. # To fully evaluate the user test cases, this should be run in two ways: @@ -12,17 +12,17 @@ from pcntoolkit.normative import estimate from pcntoolkit.normative_parallel import execute_nm, collect_nm, delete_nm -## 2. by appending to the path -#sys.path.clear() -#sys.path.append('/home/preclineu/andmar/sfw/PCNtoolkit/pcntoolkit') -#from normative import estimate -#from normative_parallel import execute_nm, collect_nm, delete_nm +# 2. by appending to the path +# sys.path.clear() +# sys.path.append('/home/preclineu/andmar/sfw/PCNtoolkit/pcntoolkit') +# from normative import estimate +# from normative_parallel import execute_nm, collect_nm, delete_nm # ---------------- Config parameters ----------------------------------------- # General config parameters normative_path = '/home/preclineu/andmar/sfw/PCNtoolkit/pcntoolkit/normative.py' -python_path='/home/preclineu/andmar/sfw/anaconda3/envs/py38/bin/python' +python_path = '/home/preclineu/andmar/sfw/anaconda3/envs/py38/bin/python' data_dir = '/home/preclineu/andmar/data/nispat_unit_test_data/' test_dir = '/home/preclineu/andmar/py.sandbox/unittests/unit_test_results' alt_alg = 'blr' # algorithm to test in addition to GPR @@ -36,35 +36,37 @@ # ---------------- Utility functions ----------------------------------------- -def update_test_counter(test_num, root_dir): + +def update_test_counter(test_num, root_dir): test_num += 1 - test_out_dir = os.path.join(test_dir,'test_'+str(test_num)) - os.makedirs(test_out_dir, exist_ok = True) + test_out_dir = os.path.join(test_dir, 'test_'+str(test_num)) + os.makedirs(test_out_dir, exist_ok=True) - return test_num, test_out_dir + def save_output(src_dir, dst_dir): - + files = [] - files.extend(glob.glob(os.path.join(src_dir,'Z*'))) - files.extend(glob.glob(os.path.join(src_dir,'yhat*'))) - files.extend(glob.glob(os.path.join(src_dir,'ys2*'))) - files.extend(glob.glob(os.path.join(src_dir,'Rho*'))) - files.extend(glob.glob(os.path.join(src_dir,'pRho*'))) - files.extend(glob.glob(os.path.join(src_dir,'RMSE*'))) - files.extend(glob.glob(os.path.join(src_dir,'SMSE*'))) - files.extend(glob.glob(os.path.join(src_dir,'MSLL*'))) - files.extend(glob.glob(os.path.join(src_dir,'EXPV*'))) - files.extend(glob.glob(os.path.join(src_dir,'Hyp*'))) - files.extend(glob.glob(os.path.join(src_dir,'Models'))) + files.extend(glob.glob(os.path.join(src_dir, 'Z*'))) + files.extend(glob.glob(os.path.join(src_dir, 'yhat*'))) + files.extend(glob.glob(os.path.join(src_dir, 'ys2*'))) + files.extend(glob.glob(os.path.join(src_dir, 'Rho*'))) + files.extend(glob.glob(os.path.join(src_dir, 'pRho*'))) + files.extend(glob.glob(os.path.join(src_dir, 'RMSE*'))) + files.extend(glob.glob(os.path.join(src_dir, 'SMSE*'))) + files.extend(glob.glob(os.path.join(src_dir, 'MSLL*'))) + files.extend(glob.glob(os.path.join(src_dir, 'EXPV*'))) + files.extend(glob.glob(os.path.join(src_dir, 'Hyp*'))) + files.extend(glob.glob(os.path.join(src_dir, 'Models'))) for f in files: fdir, fnam = os.path.split(f) - shutil.move(f, os.path.join(dst_dir,fnam)) + shutil.move(f, os.path.join(dst_dir, fnam)) return # ---------------- Unit tests ------------------------------------------------ + print('Starting unit testing ...') if os.path.exists(test_dir): print('Removing existing directory') @@ -82,8 +84,8 @@ def save_output(src_dir, dst_dir): resp_file_nii_te = os.path.join(data_dir, 'resp_n100.nii.gz') cov_file_nii_te = os.path.join(data_dir, 'cov_n100.txt') -estimate(cov_file_nii, resp_file_nii, maskfile=mask_file_nii, - testresp = resp_file_nii_te, testcov = cov_file_nii_te) +estimate(cov_file_nii, resp_file_nii, maskfile=mask_file_nii, + testresp=resp_file_nii_te, testcov=cov_file_nii_te) print(os.getcwd()) save_output(os.getcwd(), tdir) @@ -92,7 +94,7 @@ def save_output(src_dir, dst_dir): print(test_num, "Testing again using the same data under cross-validation") print("----------------------------------------------------------------------") -estimate(cov_file_nii, resp_file_nii, maskfile = mask_file_nii, cvfolds = 2) +estimate(cov_file_nii, resp_file_nii, maskfile=mask_file_nii, cvfolds=2) save_output(os.getcwd(), tdir) test_num, tdir = update_test_counter(test_num, test_dir) @@ -103,15 +105,15 @@ def save_output(src_dir, dst_dir): resp_file_txt = os.path.join(data_dir, 'resp.txt') cov_file_txt = os.path.join(data_dir, 'cov.txt') -estimate(cov_file_txt, resp_file_txt, testresp = resp_file_txt, - testcov = cov_file_txt ,alg=alt_alg, configparam=2) +estimate(cov_file_txt, resp_file_txt, testresp=resp_file_txt, + testcov=cov_file_txt, alg=alt_alg, configparam=2) save_output(os.getcwd(), tdir) test_num, tdir = update_test_counter(test_num, test_dir) print(test_num, "Testing again using the same data under cross-validation") print("----------------------------------------------------------------------") -estimate(cov_file_txt, resp_file_txt, cvfolds=2 ,alg=alt_alg, configparam=2) +estimate(cov_file_txt, resp_file_txt, cvfolds=2, alg=alt_alg, configparam=2) save_output(os.getcwd(), tdir) test_num, tdir = update_test_counter(test_num, test_dir) @@ -119,13 +121,13 @@ def save_output(src_dir, dst_dir): print(test_num, "Testing larger dataset (blr with pkl data)...") print("----------------------------------------------------------------------") -cov_file_tr = os.path.join(data_dir,'cov_big_tr.txt') -cov_file_te = os.path.join(data_dir,'cov_big_te.txt') -resp_file_tr = os.path.join(data_dir,'resp_big_tr.txt') -resp_file_te = os.path.join(data_dir,'resp_big_te.txt') +cov_file_tr = os.path.join(data_dir, 'cov_big_tr.txt') +cov_file_te = os.path.join(data_dir, 'cov_big_te.txt') +resp_file_tr = os.path.join(data_dir, 'resp_big_tr.txt') +resp_file_te = os.path.join(data_dir, 'resp_big_te.txt') estimate(cov_file_tr, resp_file_tr, testresp=resp_file_te, testcov=cov_file_te, - alg=alt_alg, configparam=1, savemodel='True') + alg=alt_alg, configparam=1, savemodel='True') save_output(os.getcwd(), tdir) test_num, tdir = update_test_counter(test_num, test_dir) @@ -138,10 +140,10 @@ def save_output(src_dir, dst_dir): bin_flag = True tdir += '/' -execute_nm(tdir, python_path, job_name, cov_file_par, - resp_file_par, batch_size, memory, duration, cluster_spec=cluster, +execute_nm(tdir, python_path, job_name, cov_file_par, + resp_file_par, batch_size, memory, duration, cluster_spec=cluster, cv_folds=2, log_path=tdir, binary=bin_flag) # to be run after qsub jobs complete -#collect_nm(tdir, job_name, collect=True, binary=bin_flag) -#delete_nm(tdir, binary=bin_flag) \ No newline at end of file +# collect_nm(tdir, job_name, collect=True, binary=bin_flag) +# delete_nm(tdir, binary=bin_flag) From e9cdf40da21e9cabbfe4ff594ac1a9dde06e5f17 Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Fri, 8 Dec 2023 11:53:09 +0100 Subject: [PATCH 09/34] Parse setup requirements from requirements.txt --- requirements.txt | 13 +++++++++++++ setup.py | 25 ++++++++++--------------- 2 files changed, 23 insertions(+), 15 deletions(-) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..6d900998 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +argparse +nibabel>=2.5.1 +six +scikit-learn +bspline +matplotlib +numpy +scipy>=1.3.2 +pandas>=0.25.3 +torch>=1.1.0 +sphinx-tabs +pymc>=5.1.0 +arviz==0.13.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 24d23e60..7a183aa6 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,14 @@ from setuptools import setup, find_packages + +def parse_requirements(filename): + """Load requirements from a pip requirements file.""" + with open(filename, 'r') as f: + lineiter = (line.strip() for line in f) + return [line for line in lineiter if line and not line.startswith("#")] + +requirements = parse_requirements('requirements.txt') + setup(name='pcntoolkit', version='0.29', description='Predictive Clinical Neuroscience toolkit', @@ -8,19 +17,5 @@ author_email='andre.marquand@donders.ru.nl', license='GNU GPLv3', packages=find_packages(), - install_requires=[ - 'argparse', - 'nibabel>=2.5.1', - 'six', - 'scikit-learn', - 'bspline', - 'matplotlib', - 'numpy', - 'scipy>=1.3.2', - 'pandas>=0.25.3', - 'torch>=1.1.0', - 'sphinx-tabs', - 'pymc>=5.1.0', - 'arviz==0.13.0' - ], + install_requires=requirements, zip_safe=False) From a713318a66dd99a96ba522939d92124cea144041 Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Fri, 8 Dec 2023 12:14:44 +0100 Subject: [PATCH 10/34] Basic config for testing --- .github/workflows/ci.yml | 36 ++++++++++++++++++++++++++++++++++++ .gitignore | 3 +++ pytest/test_sanity.py | 2 ++ 3 files changed, 41 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 pytest/test_sanity.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..63c688b7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +# This configuration does the following: + +# It triggers the CI pipeline when code is pushed to the master branch. +# It sets up a Python environment, installs project dependencies, and runs pytest. +# Test results are uploaded as artifacts for later examination. + +name: CI + +on: + push: + branches: + # - master + - test_sanity +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.10 + + - name: Install Dependencies + run: pip install -r requirements.txt + + - name: Run Tests with pytest + run: pytest + + - name: Upload Test Artifacts + uses: actions/upload-artifact@v2 + with: + name: test-results + path: test-reports diff --git a/.gitignore b/.gitignore index 389146ff..c0de6be7 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,6 @@ ssh_error_log.txt HBR_demo/* dist/pcntoolkit-0.27-py3.11.egg +# Overrule for github folder +!.github + diff --git a/pytest/test_sanity.py b/pytest/test_sanity.py new file mode 100644 index 00000000..e689428f --- /dev/null +++ b/pytest/test_sanity.py @@ -0,0 +1,2 @@ +def test_sanity(): + assert 1 + 1 == 2 From f95610619ef2c6f35a99ba6ff0c82d0a9d02f83d Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Fri, 8 Dec 2023 12:15:27 +0100 Subject: [PATCH 11/34] Added another sanity test --- pytest/test_sanity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest/test_sanity.py b/pytest/test_sanity.py index e689428f..5b752c98 100644 --- a/pytest/test_sanity.py +++ b/pytest/test_sanity.py @@ -1,2 +1,3 @@ def test_sanity(): + assert true assert 1 + 1 == 2 From 0e73a3f50dc5eec832c382b7720e63618b9e4b59 Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Fri, 8 Dec 2023 12:18:05 +0100 Subject: [PATCH 12/34] Change the CI config --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63c688b7..3ec4de59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: run: pip install -r requirements.txt - name: Run Tests with pytest - run: pytest + run: pytest pytest/test_sanity.py - name: Upload Test Artifacts uses: actions/upload-artifact@v2 From 0ff7bf0eb3f847d56e942cbf0c0143ecdd6cdfa6 Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Fri, 8 Dec 2023 12:20:10 +0100 Subject: [PATCH 13/34] Change the CI config --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ec4de59..1836e5b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.10 + python-version: 3.10.13 - name: Install Dependencies run: pip install -r requirements.txt From c27f03eed42d3c16b5e7fcc02140f1656dafab2d Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Fri, 8 Dec 2023 12:40:35 +0100 Subject: [PATCH 14/34] Add pytest to requirements.txt --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6d900998..e9865975 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ pandas>=0.25.3 torch>=1.1.0 sphinx-tabs pymc>=5.1.0 -arviz==0.13.0 \ No newline at end of file +arviz==0.13.0 +pytest \ No newline at end of file From 684b5eb2c3c340498c7b8ef72de17bfc960ba260 Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Fri, 8 Dec 2023 12:45:42 +0100 Subject: [PATCH 15/34] change 'true' to the correct, capitalized 'True' :| --- pytest/test_sanity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest/test_sanity.py b/pytest/test_sanity.py index 5b752c98..870715ac 100644 --- a/pytest/test_sanity.py +++ b/pytest/test_sanity.py @@ -1,3 +1,3 @@ def test_sanity(): - assert true + assert True assert 1 + 1 == 2 From b4f9ced250898490cd666e6fb18ad795fb50df4b Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Fri, 8 Dec 2023 12:53:31 +0100 Subject: [PATCH 16/34] Remove pytest from deps, modify ci.yml --- .github/workflows/ci.yml | 9 ++++++--- requirements.txt | 1 - 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1836e5b3..67f7c8bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,13 +24,16 @@ jobs: python-version: 3.10.13 - name: Install Dependencies - run: pip install -r requirements.txt + run: | + pip install -r requirements.txt + pip install pytest - name: Run Tests with pytest - run: pytest pytest/test_sanity.py + run: pytest pytest/test_sanity.py --junitxml=test-report.xml + continue-on-error: true # Continue to the next step even if tests fail - name: Upload Test Artifacts uses: actions/upload-artifact@v2 with: name: test-results - path: test-reports + path: test-report.xml diff --git a/requirements.txt b/requirements.txt index e9865975..9d6b3302 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,3 @@ torch>=1.1.0 sphinx-tabs pymc>=5.1.0 arviz==0.13.0 -pytest \ No newline at end of file From 9e8e11fa8d991d962607a54c2c5b9ec742eff7a9 Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Fri, 8 Dec 2023 12:59:36 +0100 Subject: [PATCH 17/34] Upload HTML report instead of XML --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67f7c8bc..9e4b6d28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,14 +26,14 @@ jobs: - name: Install Dependencies run: | pip install -r requirements.txt - pip install pytest + pip install pytest - name: Run Tests with pytest - run: pytest pytest/test_sanity.py --junitxml=test-report.xml + run: pytest pytest/ --html=report.html # Test everything in the 'pytest' directory, create HTML report continue-on-error: true # Continue to the next step even if tests fail - - name: Upload Test Artifacts + - name: Upload HTML Report as Artifact uses: actions/upload-artifact@v2 with: - name: test-results - path: test-report.xml + name: test-report + path: report.html From 6a074e3fa5ed0670057d48fd15c9e5d434b4e43a Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Fri, 8 Dec 2023 13:06:13 +0100 Subject: [PATCH 18/34] Add the pytest-html dependency --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e4b6d28..9fe8ca7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: - name: Install Dependencies run: | pip install -r requirements.txt - pip install pytest + pip install pytest pytest-html - name: Run Tests with pytest run: pytest pytest/ --html=report.html # Test everything in the 'pytest' directory, create HTML report From 27008b83a12671b0eb2fcfd7d3fa1285fc33ee3d Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Fri, 8 Dec 2023 13:30:44 +0100 Subject: [PATCH 19/34] Also produce coverage report --- .github/workflows/ci.yml | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fe8ca7d..1651689d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: push: branches: # - master - - test_sanity + - 147_CI-pipeline jobs: build: runs-on: ubuntu-latest @@ -26,14 +26,29 @@ jobs: - name: Install Dependencies run: | pip install -r requirements.txt - pip install pytest pytest-html + pip install pytest pytest-html pytest-cov - - name: Run Tests with pytest - run: pytest pytest/ --html=report.html # Test everything in the 'pytest' directory, create HTML report + - name: Run Tests with coverage and save report + run: | + coverage run -m pytest pytest/ + coverage xml -o coverage.xml + pytest pytest/ --junitxml=report.xml --html=report.html continue-on-error: true # Continue to the next step even if tests fail - - name: Upload HTML Report as Artifact + - name: Upload HTML Report uses: actions/upload-artifact@v2 with: - name: test-report + name: test-report-html path: report.html + + - name: Upload XML Report + uses: actions/upload-artifact@v2 + with: + name: test-report-xml + path: report.xml + + - name: Upload Coverage Report + uses: actions/upload-artifact@v2 + with: + name: coverage-report + path: coverage.xml \ No newline at end of file From 07891029d5b1681da4fa8045c779a5489c0df0b8 Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Fri, 8 Dec 2023 13:42:25 +0100 Subject: [PATCH 20/34] ADapt CI conf to print coverage to term --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1651689d..16777a63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,9 +30,7 @@ jobs: - name: Run Tests with coverage and save report run: | - coverage run -m pytest pytest/ - coverage xml -o coverage.xml - pytest pytest/ --junitxml=report.xml --html=report.html + coverage run -m pytest pytest/ --junitxml=report.xml --html=report.html continue-on-error: true # Continue to the next step even if tests fail - name: Upload HTML Report From e99bff581847027c4dd631fa3a2c89941ea5325d Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Fri, 8 Dec 2023 13:43:19 +0100 Subject: [PATCH 21/34] Remove XML coverage upload --- .github/workflows/ci.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16777a63..facfa4a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,9 +44,4 @@ jobs: with: name: test-report-xml path: report.xml - - - name: Upload Coverage Report - uses: actions/upload-artifact@v2 - with: - name: coverage-report - path: coverage.xml \ No newline at end of file + \ No newline at end of file From a0a15941f5c93c6b058e595625932c7b59f06fc0 Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Fri, 8 Dec 2023 13:46:54 +0100 Subject: [PATCH 22/34] Reinstall upload XML coverage report --- .github/workflows/ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index facfa4a3..66f16af3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: - name: Run Tests with coverage and save report run: | coverage run -m pytest pytest/ --junitxml=report.xml --html=report.html + coverage xml -o coverage.xml continue-on-error: true # Continue to the next step even if tests fail - name: Upload HTML Report @@ -44,4 +45,9 @@ jobs: with: name: test-report-xml path: report.xml - \ No newline at end of file + + - name: Upload Coverage Report + uses: actions/upload-artifact@v2 + with: + name: coverage-report + path: coverage.xml \ No newline at end of file From c394b21f35604b17a215ec74d447dbf30765fc7b Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Tue, 12 Dec 2023 08:47:23 +0100 Subject: [PATCH 23/34] Add warning and docs to the untested 'tune' function --- pcntoolkit/normative_model/norm_hbr.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pcntoolkit/normative_model/norm_hbr.py b/pcntoolkit/normative_model/norm_hbr.py index e71fe78b..8dacd001 100644 --- a/pcntoolkit/normative_model/norm_hbr.py +++ b/pcntoolkit/normative_model/norm_hbr.py @@ -419,8 +419,13 @@ def tune( samples=10, informative_prior=False, ): + """ + This function tunes the Hierarchical Bayesian Regression model using data sampled from the posterior predictive distribution. Its behavior is not tested, and it is unclear if the desired behavior is achieved. + """ #TODO need to check if this is correct + + print("The 'tune' function is being called, but it is currently in development and its behavior is not tested. It is unclear if the desired behavior is achieved. Any output following this should be treated as unreliable.") tune_ids = list(np.unique(batch_effects[:, merge_batch_dim])) From a641a91fc506a01460bcab151394b231d354934c Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Wed, 13 Dec 2023 10:29:55 +0100 Subject: [PATCH 24/34] Raise errors in the correct way --- pcntoolkit/dataio/fileio.py | 2 +- pcntoolkit/model/bayesreg.py | 4 +-- pcntoolkit/model/rfa.py | 6 ++-- pcntoolkit/normative.py | 7 ++-- pcntoolkit/normative_NP.py | 2 +- pcntoolkit/normative_model/norm_blr.py | 2 +- pcntoolkit/normative_model/norm_rfa.py | 4 +-- pcntoolkit/normative_model/norm_utils.py | 2 +- pcntoolkit/normative_parallel.py | 14 ++++---- pcntoolkit/trendsurf.py | 2 +- pcntoolkit/util/utils.py | 41 +++++++++++++++++------- 11 files changed, 52 insertions(+), 34 deletions(-) diff --git a/pcntoolkit/dataio/fileio.py b/pcntoolkit/dataio/fileio.py index ca962aad..41885890 100644 --- a/pcntoolkit/dataio/fileio.py +++ b/pcntoolkit/dataio/fileio.py @@ -357,7 +357,7 @@ def save_cifti(data, filename, example, mask=None, vol=True, volatlas=None): data = data.astype('float32') # force 32 bit output dtype = 'NIFTI_TYPE_FLOAT32' else: - raise (ValueError, 'Only float data types currently handled') + raise ValueError('Only float data types currently handled') if len(data.shape) == 1: Nimg = 1 diff --git a/pcntoolkit/model/bayesreg.py b/pcntoolkit/model/bayesreg.py index 0c0403a8..998c6226 100755 --- a/pcntoolkit/model/bayesreg.py +++ b/pcntoolkit/model/bayesreg.py @@ -469,7 +469,7 @@ def predict(self, hyp, X, y, Xs, # do we need to re-estimate the posterior? if (hyp != self.hyp).any() or not (hasattr(self, 'A')): - raise (ValueError, 'posterior not properly estimated') + raise ValueError('posterior not properly estimated') N_test = Xs.shape[0] @@ -477,7 +477,7 @@ def predict(self, hyp, X, y, Xs, if self.var_groups is not None: if len(var_groups_test) != N_test: - raise (ValueError, 'Invalid variance groups for test') + raise ValueError('Invalid variance groups for test') # separate variance groups s2n = np.ones(N_test) for v in range(len(self.var_ids)): diff --git a/pcntoolkit/model/rfa.py b/pcntoolkit/model/rfa.py index b8d1fdff..27550e9e 100644 --- a/pcntoolkit/model/rfa.py +++ b/pcntoolkit/model/rfa.py @@ -63,7 +63,7 @@ def _numpy2torch(self, X, y=None, hyp=None): elif type(X) is np.ndarray: X = torch.from_numpy(X) else: - raise (ValueError, 'Unknown data type (X)') + ValueError('Unknown data type (X)') X = X.double() if y is not None: @@ -72,7 +72,7 @@ def _numpy2torch(self, X, y=None, hyp=None): elif type(y) is np.ndarray: y = torch.from_numpy(y) else: - raise (ValueError, 'Unknown data type (y)') + raise ValueError('Unknown data type (y)') if len(y.shape) == 1: y.resize_(y.shape[0], 1) @@ -191,7 +191,7 @@ def estimate(self, hyp0, X, y, optimizer='lbfgs'): if optimizer.lower() == 'lbfgs': opt = torch.optim.LBFGS([hyp]) else: - raise (ValueError, "Optimizer " + " not implemented") + raise ValueError("Optimizer " + " not implemented") self._iterations = 0 def closure(): diff --git a/pcntoolkit/normative.py b/pcntoolkit/normative.py index 9a51c2dd..8db40074 100755 --- a/pcntoolkit/normative.py +++ b/pcntoolkit/normative.py @@ -120,7 +120,7 @@ def get_args(*args): wdir = os.path.realpath(os.path.curdir) respfile = os.path.join(wdir, args.responses) if args.covfile is None: - raise (ValueError, "No covariates specified") + raise ValueError("No covariates specified") else: covfile = args.covfile @@ -522,8 +522,7 @@ def estimate(covfile, respfile, **kwargs): if warp is not None: # TODO: Warping for scaled data if outscaler is not None and outscaler != 'None': - raise ( - ValueError, "outscaler not yet supported warping") + raise ValueError("outscaler not yet supported warping") warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] Ywarp[ts, nz[i]] = nm.blr.warp.f( Y[ts, nz[i]], warp_param) @@ -767,7 +766,7 @@ def predict(covfile, respfile, maskfile=None, **kwargs): return_y = kwargs.pop('return_y', False) if alg == 'gpr': - raise (ValueError, "gpr is not supported with predict()") + raise ValueError("gpr is not supported with predict()") if respfile is not None and not os.path.exists(respfile): print("Response file does not exist. Only returning predictions") diff --git a/pcntoolkit/normative_NP.py b/pcntoolkit/normative_NP.py index a2c1fac1..4f941184 100644 --- a/pcntoolkit/normative_NP.py +++ b/pcntoolkit/normative_NP.py @@ -100,7 +100,7 @@ def get_args(*args): args = parser.parse_args() if (args.respfile == None or args.covfile == None or args.testcovfile == None): - raise (ValueError, "Training response nifti file, Training covariates pickle file, and \ + raise ValueError("Training response nifti file, Training covariates pickle file, and \ Test covariates pickle file must be specified.") if (args.outdir == None): args.outdir = os.getcwd() diff --git a/pcntoolkit/normative_model/norm_blr.py b/pcntoolkit/normative_model/norm_blr.py index bb7e2949..ca123742 100644 --- a/pcntoolkit/normative_model/norm_blr.py +++ b/pcntoolkit/normative_model/norm_blr.py @@ -56,7 +56,7 @@ def __init__(self, **kwargs): self.optim_alg = kwargs.get('optimizer', 'powell') if X is None: - raise (ValueError, "Data matrix must be specified") + raise ValueError("Data matrix must be specified") if len(X.shape) == 1: self.D = 1 diff --git a/pcntoolkit/normative_model/norm_rfa.py b/pcntoolkit/normative_model/norm_rfa.py index e514ab4a..7b35368e 100644 --- a/pcntoolkit/normative_model/norm_rfa.py +++ b/pcntoolkit/normative_model/norm_rfa.py @@ -45,7 +45,7 @@ def __init__(self, X, y=None, theta=None, n_feat=None): self.gprrfa = GPRRFA(theta, X, n_feat=n_feat) self._n_params = self.gprrfa.get_n_params(X) else: - raise (ValueError, 'please specify covariates') + raise ValueError('Covariates not specified') return if theta is None: @@ -54,7 +54,7 @@ def __init__(self, X, y=None, theta=None, n_feat=None): if len(theta) == self._n_params: self.theta0 = theta else: - raise (ValueError, 'hyperparameter vector has incorrect size') + raise ValueError('hyperparameter vector has incorrect size') self.theta = self.theta0 diff --git a/pcntoolkit/normative_model/norm_utils.py b/pcntoolkit/normative_model/norm_utils.py index 4e748468..b0914786 100644 --- a/pcntoolkit/normative_model/norm_utils.py +++ b/pcntoolkit/normative_model/norm_utils.py @@ -24,6 +24,6 @@ def norm_init(X, y=None, theta=None, alg='gpr', **kwargs): elif alg == 'np': nm = NormNP(X=X, y=y, **kwargs) else: - raise (ValueError, "Algorithm " + alg + " not known.") + raise ValueError("Algorithm " + alg + " not known.") return nm diff --git a/pcntoolkit/normative_parallel.py b/pcntoolkit/normative_parallel.py index de3005ec..521a6251 100755 --- a/pcntoolkit/normative_parallel.py +++ b/pcntoolkit/normative_parallel.py @@ -133,7 +133,7 @@ def execute_nm(processing_dir, kwargs.update({'job_id': str(n)}) if testrespfile_path is not None: if cv_folds is not None: - raise (ValueError, """If the response file is specified + raise ValueError("""If the response file is specified cv_folds must be equal to None""") else: # specified train/test split @@ -349,10 +349,10 @@ def split_nm(processing_dir, dummy, respfile_extension = os.path.splitext(respfile_path) if (binary and respfile_extension != '.pkl'): - raise (ValueError, """If binary is True the file format for the + raise ValueError("""If binary is True the file format for the testrespfile file must be .pkl""") elif (binary == False and respfile_extension != '.txt'): - raise (ValueError, """If binary is False the file format for the + raise ValueError("""If binary is False the file format for the testrespfile file must be .txt""") # splits response into batches @@ -391,10 +391,10 @@ def split_nm(processing_dir, else: dummy, testrespfile_extension = os.path.splitext(testrespfile_path) if (binary and testrespfile_extension != '.pkl'): - raise (ValueError, """If binary is True the file format for the + raise ValueError("""If binary is True the file format for the testrespfile file must be .pkl""") elif (binary == False and testrespfile_extension != '.txt'): - raise (ValueError, """If binary is False the file format for the + raise ValueError("""If binary is False the file format for the testrespfile file must be .txt""") if (binary == False): @@ -931,7 +931,7 @@ def bashwrap_nm(processing_dir, job_call = [python_path + ' ' + normative_path + ' -c ' + covfile_path + ' -f ' + func] else: - raise (ValueError, """For 'estimate' function either testcov or cvfold + raise ValueError("""For 'estimate' function either testcov or cvfold must be specified.""") # add algorithm-specific parameters @@ -1134,7 +1134,7 @@ def sbatchwrap_nm(processing_dir, job_call = [python_path + ' ' + normative_path + ' -c ' + covfile_path + ' -f ' + func] else: - raise (ValueError, """For 'estimate' function either testcov or cvfold + raise ValueError("""For 'estimate' function either testcov or cvfold must be specified.""") # add algorithm-specific parameters diff --git a/pcntoolkit/trendsurf.py b/pcntoolkit/trendsurf.py index e7860e37..8fce98d0 100644 --- a/pcntoolkit/trendsurf.py +++ b/pcntoolkit/trendsurf.py @@ -183,7 +183,7 @@ def get_args(*args): maskfile = os.path.join(wdir, args.maskfile) basis = args.basis if args.covfile is not None: - raise (NotImplementedError, "Covariates not implemented yet.") + raise NotImplementedError("Covariates not implemented yet.") return filename, maskfile, basis, args.a, args.o diff --git a/pcntoolkit/util/utils.py b/pcntoolkit/util/utils.py index 5679e36d..9eb9208b 100644 --- a/pcntoolkit/util/utils.py +++ b/pcntoolkit/util/utils.py @@ -42,11 +42,31 @@ def create_poly_basis(X, dimpoly): - """ - Compute a polynomial basis expansion of the specified order - """ - + Creates a polynomial basis matrix for the given input matrix. + + This function takes an input matrix `X` and a degree `dimpoly`, and returns a new matrix where each column is `X` raised to the power of a degree. The degrees range from 1 to `dimpoly`. If `X` is a 1D array, it is reshaped into a 2D array with one column. + + Parameters + ---------- + X : numpy.ndarray + The input matrix, a 2D array where each row is a sample and each column is a feature. If `X` is a 1D array, it is reshaped into a 2D array with one column. + dimpoly : int + The degree of the polynomial basis. The output matrix will have `dimpoly` times as many columns as `X`. + + Returns + ------- + Phi : numpy.ndarray + The polynomial basis matrix, a 2D array where each row is a sample and each column is a feature raised to a degree. The degrees range from 1 to `dimpoly`. + + Examples + -------- + >>> X = np.array([[1, 2], [3, 4], [5, 6]]) + >>> create_poly_basis(X, 2) + array([[ 1., 2., 1., 4.], + [ 3., 4., 9., 16.], + [ 5., 6., 25., 36.]]) + """ if len(X.shape) == 1: X = X[:, np.newaxis] D = X.shape[1] @@ -79,8 +99,8 @@ def create_design_matrix(X, intercept=True, basis='bspline', **kwargs): """ Prepare a design matrix from a set of covariates sutiable for - running Bayesian linar regression. This design matrix consists of - a set of user defined covariates, optoinal site intercepts + running Bayesian linear regression. This design matrix consists of + a set of user defined covariates, optional site intercepts (fixed effects) and also optionally a nonlinear basis expansion over one of the columns @@ -94,7 +114,8 @@ def create_design_matrix(X, intercept=True, basis='bspline', if site_ids is specified, this must have the same number of entries as there are rows in X. If all_sites is specfied, these will be used to - create the site identifiers in place of site_ids. This accommocdates + create the site identifiers in place of site_ids. This accommo + dates the scenario where not all the sites used to create the model are present in the test set (i.e. there will be some empty site columns). @@ -448,8 +469,7 @@ def __init__(self): def _get_params(self, param): if len(param) != self.n_params: - raise (ValueError, - 'number of parameters must be ' + str(self.n_params)) + raise ValueError('number of parameters must be ' + str(self.n_params)) return param[0], np.exp(param[1]) def f(self, x, params): @@ -550,8 +570,7 @@ def __init__(self): def _get_params(self, param): if len(param) != self.n_params: - raise (ValueError, - 'number of parameters must be ' + str(self.n_params)) + raise ValueError('number of parameters must be ' + str(self.n_params)) epsilon = param[0] b = np.exp(param[1]) From 70e9a0e6f0c325237284cf53b1e3fbf4fef5bfad Mon Sep 17 00:00:00 2001 From: Stijn de Boer <19709783+AuguB@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:28:15 +0100 Subject: [PATCH 25/34] Added PCNtoolkit.drawio --- PCNtoolkit.drawio | 1 + 1 file changed, 1 insertion(+) create mode 100644 PCNtoolkit.drawio diff --git a/PCNtoolkit.drawio b/PCNtoolkit.drawio new file mode 100644 index 00000000..35262463 --- /dev/null +++ b/PCNtoolkit.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file From cfb0c7b28069f4a48e37cecba3fe147fa58d20f8 Mon Sep 17 00:00:00 2001 From: Stijn de Boer <19709783+AuguB@users.noreply.github.com> Date: Thu, 14 Dec 2023 10:45:48 +0100 Subject: [PATCH 26/34] Update PCNtoolkit.drawio --- PCNtoolkit.drawio | 721 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 720 insertions(+), 1 deletion(-) diff --git a/PCNtoolkit.drawio b/PCNtoolkit.drawio index 35262463..16ebd495 100644 --- a/PCNtoolkit.drawio +++ b/PCNtoolkit.drawio @@ -1 +1,720 @@ -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 \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 61743d2aea4d063d06745da9202d502180608b21 Mon Sep 17 00:00:00 2001 From: Stijn de Boer <19709783+AuguB@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:36:48 +0100 Subject: [PATCH 27/34] Update PCNtoolkit.drawio --- PCNtoolkit.drawio | 194 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 178 insertions(+), 16 deletions(-) diff --git a/PCNtoolkit.drawio b/PCNtoolkit.drawio index 16ebd495..08bcd647 100644 --- a/PCNtoolkit.drawio +++ b/PCNtoolkit.drawio @@ -1,6 +1,6 @@ - + - + @@ -679,38 +679,200 @@ + + + - + - - + + + + + + + + - - + + + + + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - + + From 03e35a51f6cada814bcd8f0d281fe53148126ac7 Mon Sep 17 00:00:00 2001 From: Stijn de Boer <19709783+AuguB@users.noreply.github.com> Date: Thu, 14 Dec 2023 16:54:58 +0100 Subject: [PATCH 28/34] Update PCNtoolkit.drawio --- PCNtoolkit.drawio | 78 +++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/PCNtoolkit.drawio b/PCNtoolkit.drawio index 08bcd647..0ccf263c 100644 --- a/PCNtoolkit.drawio +++ b/PCNtoolkit.drawio @@ -1,4 +1,4 @@ - + @@ -686,7 +686,7 @@ - + @@ -700,74 +700,74 @@ - + - + - - + + - + - - + + - - + + - - + + - + - - + + - + - - + + - + - + - + - + - - + + - - + + - - + + @@ -775,8 +775,8 @@ - - + + @@ -784,10 +784,10 @@ - - + + - + @@ -802,13 +802,13 @@ - - + + - - + + - + From 32e86c69c379c3cbb142f072d34cb325192b6ab7 Mon Sep 17 00:00:00 2001 From: Stijn de Boer <19709783+AuguB@users.noreply.github.com> Date: Thu, 14 Dec 2023 16:55:55 +0100 Subject: [PATCH 29/34] Update PCNtoolkit.drawio --- PCNtoolkit.drawio | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PCNtoolkit.drawio b/PCNtoolkit.drawio index 0ccf263c..ac74390b 100644 --- a/PCNtoolkit.drawio +++ b/PCNtoolkit.drawio @@ -1,4 +1,4 @@ - + @@ -761,7 +761,7 @@ - + From 92db39a62cccada5255d31d1f50dfd15d5596236 Mon Sep 17 00:00:00 2001 From: Stijn de Boer <19709783+AuguB@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:06:12 +0100 Subject: [PATCH 30/34] Update PCNtoolkit.drawio --- PCNtoolkit.drawio | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/PCNtoolkit.drawio b/PCNtoolkit.drawio index ac74390b..052a58c6 100644 --- a/PCNtoolkit.drawio +++ b/PCNtoolkit.drawio @@ -1,4 +1,4 @@ - + @@ -686,7 +686,7 @@ - + @@ -700,10 +700,10 @@ - + - - + + @@ -723,39 +723,42 @@ - - - - + - + + + + + - + - - + + + + - + - + - + - + - + @@ -876,6 +879,12 @@ + + + + + + From 5402da36c7f9bec64b5330295a067c1d209eabd1 Mon Sep 17 00:00:00 2001 From: Stijn de Boer <19709783+AuguB@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:19:48 +0100 Subject: [PATCH 31/34] Update PCNtoolkit.drawio --- PCNtoolkit.drawio | 48 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/PCNtoolkit.drawio b/PCNtoolkit.drawio index 052a58c6..5bf3ed1d 100644 --- a/PCNtoolkit.drawio +++ b/PCNtoolkit.drawio @@ -1,4 +1,4 @@ - + @@ -686,7 +686,7 @@ - + @@ -699,11 +699,11 @@ - - + + - + @@ -712,10 +712,10 @@ - + - + @@ -748,17 +748,17 @@ - - + + - - + + - - + + @@ -879,12 +879,30 @@ - - + + + + + + + + + + + + + + + + + + + + From 9a4ae2a360b789ead7df6be9f70ce7e72475bf59 Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Sat, 16 Dec 2023 09:40:27 +0100 Subject: [PATCH 32/34] updated documentation --- doc/build/doctrees/environment.pickle | Bin 315173 -> 165307 bytes doc/build/doctrees/index.doctree | Bin 4035 -> 4036 bytes .../pages/BLR_normativemodel_protocol.doctree | Bin 92001 -> 101416 bytes doc/build/doctrees/pages/FAQs.doctree | Bin 4539 -> 4846 bytes ...R_NormativeModel_FCONdata_Tutorial.doctree | Bin 38925 -> 40408 bytes .../doctrees/pages/acknowledgements.doctree | Bin 5034 -> 5082 bytes .../pages/apply_normative_models.doctree | Bin 72556 -> 77544 bytes doc/build/doctrees/pages/citing.doctree | Bin 16094 -> 20142 bytes doc/build/doctrees/pages/glossary.doctree | Bin 8951 -> 9137 bytes doc/build/doctrees/pages/installation.doctree | Bin 14410 -> 14815 bytes doc/build/doctrees/pages/modindex.doctree | Bin 341841 -> 233086 bytes .../normative_modelling_walkthrough.doctree | Bin 113935 -> 117597 bytes .../pages/other_predictive_models.doctree | Bin 93209 -> 93948 bytes .../pages/pcntoolkit_background.doctree | Bin 41909 -> 45774 bytes .../doctrees/pages/post_hoc_analysis.doctree | Bin 46109 -> 46502 bytes .../doctrees/pages/visualizations.doctree | Bin 19938838 -> 19939605 bytes doc/build/html/.buildinfo | 2 +- doc/build/html/_modules/bayesreg.html | 390 ++--- doc/build/html/_modules/fileio.html | 372 +++-- doc/build/html/_modules/gp.html | 210 +-- doc/build/html/_modules/index.html | 152 +- doc/build/html/_modules/rfa.html | 261 ++-- doc/build/html/_modules/trendsurf.html | 229 ++- doc/build/html/_static/basic.css | 56 +- doc/build/html/_static/css/badge_only.css | 2 +- doc/build/html/_static/css/theme.css | 6 +- doc/build/html/_static/doctools.js | 382 +++-- .../html/_static/documentation_options.js | 6 +- doc/build/html/_static/jquery.js | 4 +- doc/build/html/_static/js/theme.js | 4 +- doc/build/html/_static/language_data.js | 100 +- doc/build/html/_static/searchtools.js | 825 +++++----- doc/build/html/genindex.html | 284 +--- doc/build/html/index.html | 164 +- doc/build/html/objects.inv | Bin 3947 -> 3768 bytes .../pages/BLR_normativemodel_protocol.html | 199 +-- doc/build/html/pages/FAQs.html | 167 +-- .../HBR_NormativeModel_FCONdata_Tutorial.html | 190 +-- doc/build/html/pages/acknowledgements.html | 164 +- .../html/pages/apply_normative_models.html | 185 +-- doc/build/html/pages/citing.html | 167 +-- doc/build/html/pages/glossary.html | 173 +-- doc/build/html/pages/installation.html | 173 +-- doc/build/html/pages/modindex.html | 1334 +++++------------ .../normative_modelling_walkthrough.html | 861 ++++------- .../html/pages/other_predictive_models.html | 189 +-- .../html/pages/pcntoolkit_background.html | 189 +-- doc/build/html/pages/post_hoc_analysis.html | 179 +-- doc/build/html/pages/visualizations.html | 177 +-- doc/build/html/py-modindex.html | 175 +-- doc/build/html/search.html | 161 +- doc/build/html/searchindex.js | 2 +- 52 files changed, 2789 insertions(+), 5345 deletions(-) diff --git a/doc/build/doctrees/environment.pickle b/doc/build/doctrees/environment.pickle index 4b4534f8809349f402a18e590328e11cfdb75da5..c7a830a5ffc2b4996022774b1ebd0099f8ed94e5 100644 GIT binary patch literal 165307 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�isp&~J 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 diff --git a/doc/build/doctrees/index.doctree b/doc/build/doctrees/index.doctree index dce80dcc97a71b1dccc8ed2f5bd602c909f03a10..dcfdfb2cb89d0f12c23a0c4db0582ed40595f2e8 100644 GIT binary patch delta 118 zcmV-+0Ez#@AH*L7fCZJgkp%PvXa#5!k@$`qIxlE%ZDlWTa%E#|X>Mh8FJW$EZDDdR zb7pt5r2&NsARuUTbZ~PzFE4j@cP?;wbZBpGE^l&YFJxtQFK}gWbF)GWJOK|3Wo=?* YWM64*VP|D?l!=p+4blNlvu6%m0Y>^Of&c&j delta 116 zcmV-)0E_>`AHyF6fCZJfkp%RS{EifQWpZY3a%3+=WpitE zZ*a4v0fh<}7-)2KaC15@FK}gWb1rasbZBpGE^l&Yvq}s+0TvBuZDD6+UuMzjla&q90g1C{4qO3lO)P5w diff --git a/doc/build/doctrees/pages/BLR_normativemodel_protocol.doctree b/doc/build/doctrees/pages/BLR_normativemodel_protocol.doctree index 2f25c82380181b4c693d105a1031c991af11f44d..263b64dabd1bd587654af433bf297964627c84f8 100644 GIT binary patch delta 9438 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 Hwub%zmGN=D delta 292 zcmaE-x?7pGfpzKz!HKNbCqC$tch(OrPAw|dFHX!%Pc14f$w)0q%P&gNcS$YIF3B(0 z+{wtn$Sj?qKiQj0ZSy{+e8$a&EKe9GA7+~m6b%K6s!yK4&M6sQnxUN$oFSdzU7D03 z*2AbbrFBZ?9 zg4BXyy@JY;jQl*k{G#;DCHyvwV!WBTiRr2FIr)hxnR)3`dRTHY^RlN*-Y>9EW^%@a h;vUYt#N5=9%7WBlpd<cNS+T0_h2xX9|Wg0s!w8Xx;z- diff --git a/doc/build/doctrees/pages/HBR_NormativeModel_FCONdata_Tutorial.doctree b/doc/build/doctrees/pages/HBR_NormativeModel_FCONdata_Tutorial.doctree index bd61908efa51248da350d387368277af8195fe56..ee47aec42e4ba264c636054039eec2f5d08ef11a 100644 GIT binary patch delta 4505 zcmb7Hdr(x@8PDD2vb#K2c?7#W7FLqw;Vy3$tgQ)&X$Dw7U?jddEQCbey1T5TFbW0NLLhnbkz?>p!2W!Gt# z^`8Uhp6~bgzVqF4?oJ$2U4B``kMajDJQd)t#R$BR61YBT*kBdhE~mBE4v)3Jb3i)4eM_b0{Y~Txu89kh8|qIod4FoZ(`kIf(cR^|w?EG7^SV9Vj?F>A zAk>nJ8TXK;Z)?dS{wJJ`%<#_x_z?L}^)zwC9?j+b@pT@j!|UuYwhtN+(I#XHY9Y%m zWZQl1LJcXsJta4`_`HJC)9LnfV5mbO|G6fGt0%8(?%{1n@p!A5n6v>6FKmP9 zAcU2X_q7RJjr3owpNoH@-zO9adtqsuOvGIRTkY}BLhz5^$q@)HU#fDQj)YZE{N_hkMrbOvK6A=OEzKjdD0tO#%IHShouI6 zF$a!6fyuuyJw6W~lNS~}7srbnch_Aoy@d;D`M7{4pqeihj0J~-;GbAiMo2-*zBoA- zF_9y=klsgWSp~ll&I!NuYrA^~y@I=^oVp1mcyBnFl$CmnQy6j-hTMf1N~lju`h(!J ziux2Q$m=%SAuzOzNbaVBBQ~K5%RFFTc zG;&$wY-PTTUmk(4CZ?*TS-gL-(d#zCSz~lL9sNF!)7)PGCyrPh;`^?`_e50*@SUq# zA@j9G@U@Z^)n+*5(nEY6I7;0vqqDQqxzXE?oUI|w?Y423OA_p1sl!CgW5q@XjI z2rH>Sgf8VypuPP28kJVr;v=fiIkMm>FpVVi6W{Mi_a!$b!%Y1U&U+<;;+Ek zvJ5ze#WPC!^jw&snkVZ1tWz{EDw=rh>89SMN?7q zpOK<(NV%(RD%g_v=2f^QH&|596>F-GV=cIi%&Vwf;`6l%rG~)fT*iB$Bi33I`3doeZ~arwvo5 zj_fwLf!D0PV1PG*-oRH!pNzQR#Rjx&h8sQ$F5nV=0xw{(?(ubnz?uQq{M|YW_94=u z_t+1pce)GP)^%`i&|UaqojaZvx1h8^^de)4zE3ei_x)q(`1%mNR6(aqRW%a%VSy zbQTs@M3;00Y^IMP<3q+YE6hTrh1&tX+RX5Iff|>G?got^7QI#0_&ORJphO{KUWkCa z7--vYDS!wA1;Rm-6cjS%MPnw}SRZ5faw`MdFK$3RHfDg6 zIG3^J5N<~$X&U2InS6NLEP-WZclPdR284|vsuV;yMd;+7S;CXjp}eT~=E$SyqK$EW z6F6&Ck3=|Vns+b_k!}|0qLE^Ra#Qrm0e==bv?~Mn_A|Z-%trG7Zp7XQ`SE%AOtSK^ zL4X`#kRQxLz8~RZJbEAaqU;>U3Y=x6zow)QqK{W1e9VOVDBA4+$lDC^mwCwL2p=une`#GVnx=_!BGqWN^2h9hX+SJ!#o_0SrzW2I5z6$4EQU7aT8V+J2rH?99_=-;2xqnaC+n=@GG9e!4038;$|GWaOl9{O&tD!!!O`-NPG#0>o~lN zLl0bA;wBv4!{P5Zyns~C!{GDldK@mNcd!?<;AgOiN_MJZrwVptAAv_mZKt$$N@=Hb zc1mUU3AqSHhe86iCmd$p@Rup`hClY0H~ds)-ta?~dBd+u<_$kxs5cSsVV^&-(=DAo HXy^V1WP>VD delta 4011 zcmb_fdr(yO74|NBmt6q?3k$N~E`k|Q9s!CAh;67=jSnIjDHWx%z{1_@vb%B@TjGO< zp=QfM#IuG(6KW>Yc66Gky-hNmbgHe!&NP*2CJu=-X_B^6&9recb*y8WX?o7@?q%;f zmRV-{&u?eX{m%D&=lovxzW0W1^qh_#;E(H`9q0ct{*N(hdugNO?2$?(hpWrk)9V+U zJ)Pd3j?#5bX^-FQlaF&dbg)+c&p3-v4GsJZXNMxgS^QTDW#2A`^YQyqEkdJ^EyM{q z^}^En-Zr5MHX7Ry(SrCA_{{hf?orU29^&l+BB9Jt08OTGeC~&aU>VpJCUI4A$-*F) zd`#*URtm!i8xnGaA{gwj!p(&9D0w3B3?{q?d)tb2!X!MAw6cCruFdE2*?K)LkK}i_ z-L~RB+jha}@OX>2^=(~iv*p>^y`E0DtKBczzU%S};iopQ$0m8Zoi?XupR32~>2`Yj z(n7eJWNj4UTS9_4_vj2wl?q4b6It497j zBTa{h8bnm4)M^k^jNFp-H$6#jRHbw0lCGn4IBi{<#M%*_lN$M+H4i%|cF(#Hqh0Kj zyK)oB#eQTMFC7f7DuRpoCjJ~;FDErFkVmK69Rf9_NarEC)G!^Ph%RV>534OXp$Ovx@*7tdWBvf;Tkp@;6_#j| z$xg}(WG6f+Yysug>WtL%XHk`H%{dqLDOOEj-DNJo=G*di#ZAFcbNF$wTd=RZMEsr!*>9D=GX5-3jf$g`pOc%KZ*o+*EiRVsenedIk9PfuLx1I%qFpR__hs$AivDWQ-+DZ>C;3sj_i*9P zw64%o0;Jl%%702E@EynZxM|kDI}R@8IUWcuF5r)0fSWzI8E2`SPxwm&w&B=BjllG1bQ6 zVe`z^4*wYg@xBrhZ@$7S?q5#47gb(EC5ywg11GrK*tCQA@Oo6kro)=bNxroj?1%Os z=*BF=YGRm%8;6FIza*?h`~rV^+r$O1zkju9HU<~^k6>axY#XSI{~9Ib>fz{9%aW~n zmX(b^m}NT_$cF0!bS|xqK&u$ks6xSUIM0-&X^7dFwN}HhQ)O_T& z9LYlK2~ZcP;f&Dmbe=l5JRh0cdq)}&w2XnQ1TuwZmFK}bxGc7gQj5NU6qSZ`)XlGJ z#cDNTzY82jS z{}Vy}TdVrq)bEXD=8aX z$7o&~8QG(x`*@V@V;bFbs79ze0(C{{KE`y7>X|@WCt)Y&1(}VhUuUiUj z*5P}<__=am-i121mF`{R9VIXgQfQp3mign`InF9KKeL77elP!I;teh{Q}FwJQcY>8 z&*_s&l#hrKZ%>yz^js}xPI7fSx}3Y+UPp(^(-jKp-7e3b5L`WRq5sENhxif=UHBFx zcF?e!hFvsF(eM@xXYtM#e@ep)4e!#>jJCuk8s4Gd4>U{?)k$hj(ByG+JN}L>*wX0Y({MbOA;c=oNA)nH&m<%$_i*c%u&%#T$J9DBkEzPVq+X dU5Yn)8Bx5^{ms0Ibba*(lRCZf51y~*{tL%_(6ayl diff --git a/doc/build/doctrees/pages/acknowledgements.doctree b/doc/build/doctrees/pages/acknowledgements.doctree index 9deef8619b4db45d11a4194e8eb2ee1297e7e1d6..bb93689ef8ccb4ccb3f5b56c902860818f56d553 100644 GIT binary patch delta 266 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 Mm9Fl%L{`fF4_*ljkpKVy delta 4831 zcma)Adr(y875Cd0F08@=g1XBJVvzSkWq8DiLey#mZB!6#97WlBxhfzFnrIAeD~XHC z%d@bPhdRbiOdqCpZgn-RDAt;^8e@~xNz#cF}FDFbnGD^+KWa z2aSzIRNI<`3}FR6coIr9SY_WQM&OvwoM$$ZXp;rv?5;0 zAFJ0GO+p)C3JFUD%MwC{`0;B{#r}YbzVUJDMb0>g437(U5g$ovgMW@+f@W!Lm4sw8|lSrTfv}s>u0s8`u-gW^^PqeQ1~JwsWohfNj>xz7vPy-evY{mZh0k$)xkAC|4B5_tdT~F(AbA@cOOKDmPr6jpm?Cv()wlS_OL$x%XhA6=)7z?)`*p!0ZV!A_W4s)M&m zT4_tC$sq`PXGOh^CRK>(JFQ}?RJZi31|RR_ zkG03}@pjm{B6;i{3KngpS5_2}e-9aC=tUrBlh_Hy$_cnLoidi9F_l*S9PN*ieJbyd z5i6tFBCxDnoK4$89u$#N9iElw@Nv>nC9NH?}c zs1c@rB+TI*1?X_`0hoR%c4t2&vLA8sby`L^(!g^$%wiI+55o`&{k!R02DvTE;Oy?H z=z0?od0&AvJC)pH_EHlSFS+yLW!Z|-y^ci1hISd6N<Qvu&}@~wJ^uQYw1k&@05vWRoGqLQcU(Hm|2@|c3i2a=kj6q% zZIT7>o&KV?Bc4vnAl?kLoUC9wJ$od^8}qb;@o$cSb3pV^S1)h%nL}4uJlKE#2Fi(s zt*5fm=nWaERc>ne0fv(5{1q+ec#Tv*=ZPcnIA;oWF&FLqgQl2>7dVe5#|nF$x((L4 zCTk-f>u~fGBVqT++ekH!sQAmc1de*n;hkl8rcv|Lo zpVm-Fb;KyonC75qIX)Z%(gv0g#BL5TEGvKZS@W!19{3oQycm$$D}8qU3hmob5(jrL zXj!jx?}Cb&$O&1bq8HtNdnVeRLEFq||4>9`7)PA*Ltqy_OiQ2<|DZB2 zoKFMa^8_(I#|O{xErRxg5sGJ>j#cqaWz^9|VU`mWwI`?)_VOHB;l5#DW$qvA+16BF zRi%@w{1YdDE+1W+iqqWEvtMzV%+YAJ{SeKWTpz>yAPjcS;4ga5lt@<2Jd+~XDn3^I zXf(b(Kg3t&4mSqES{H=0(-+neBm0=e^dyp5d6yV0eT!PE404EvI&scAIX zFDdQi-SB#*$Fmyl^RtkN4=aI)*!{mrY%ZNOm!H?DsDfW}bI*>+Y@7 z%gCSo-UMcH|HsUx6V`F1p8Q}PQ5vqtkLOOXuxH)z7PglF&np3g_7cxi{=IK zLC7F|!$I6ViR^O{ax-L%b0aWrC$TERxF0gcA4g!En95cYMtE4L&GkPV&gW?1WDZW`;3N)C;Naj64(#Bd4i4yO7Lo{p6blBQT>4q*lS@Bkd~)gMhEFa{ h{64ufU;5M2sGU3kM{`#0!Q48jVro%&?mj%B9)u?Em-8H~qG2{WJFg z<739BEThcFNXWpH(dD4FmW5q*j&-`YYn|;5&dXXIt#+%6b=!K-Aow25Kr_@r7ELDS zpsV_L@v)*&0hp<6B|yI;@8absyj+Kui&SU4&Q2P)^+JFQkeGVQ7(+^Wv;NQQ?R-62K?H4M2~WX?8FdG1C!o)g|5uB;l( zXf9ucg88+QHbyleMDtUvroc~{%QoI4%}wt|-H?c9gvfs$LXgMtYmpuRCgGMK&nLbi z6!Nj9;+JDj3IDm!h_VZPbh}I8HKt8Ism3Q#qhKYZ26;TJ2ERZMEx#oC*azgT3843E zz(DG5hVk-?gF4MdWHVPtrGC&8`597blb%RIv7LxRD7$!wZdj!Vo4sJkn2P>~@F{o^ z&o8Uy)9{r%cO*i`IC%Ud>A$JeO^!EjKj{QAlwFiGv!o^RB_Ud#_`2+^0=UG1idP^r z6M8&OcO}dA^z@jVy&l-*Fm<`w*jBEab#hL(*jQB!lHxgg3)gz3-EM8;+=3rXRnH$i ji%q@`f4Bp&4y&E>^g8iczrd3PiyLO)%Q|tmrV0E9lK8D$ delta 1254 zcmaLWUr19?9Kdn!otwEdr_4V&bxAmphBEW{t1lP4$ z@&bQ`2TpSbZ+75KA%k>|5({z4FEtuI*FY|i5~3odE>h+i?I-QS!aN^kIMxwNztp$6tFGGVd; zYkwZB+8T9w9Q)O?gb_zfr@2ytzcPjRyGBbpw5tVro79`J%FR~Nexi=R6Lx&efyrftBW^{ zkUF?2vq=Y$@*tfwc?b32Ekq&+3RxmN7eZnu4-xjAnQq_j=Qs14$GaHV86@-M)#mjT z;`%t@p_RQ-_#Wl9#i)~G6Pd{@o1w{U%1Y*F{z*|VVVYGC*ArChI(*PR~ z7w^3p^ylb{|GQ5-@_m81upMQiq=fzv{YdqQe--@{{io`u{^e?78b(zW=u**kU=jFT6Nm`q;Gu>i<1gK(NSX8}235Kc5#-Q)*c4}b!_5P|>P zEkMo^2r=8FOxjFTS- zUI7Ybf(1Vd9h-bW_}b*TBF}(g+ks-1laGl`nVcwg2FQO0R+A$>Z?d4ow#nxtK>A!K z7fM-9ek$3tIZvvYRYD@8q@Z)R>{dTM-5 kequ^yUiy?CmYmGI>?xCD6rZzqX7w>MFm!JAQc7n809YZ9{r~^~ diff --git a/doc/build/doctrees/pages/installation.doctree b/doc/build/doctrees/pages/installation.doctree index 9f3651d2f9419a6f9cd3e8ce006e5aac7daa8694..515f7805b76b49c2008910607ce01fe250bea162 100644 GIT binary patch delta 1530 zcmajeUrbY190zdEy*)?~+bHr!%cUj$0Y~B1O2wi+=nF9j@}NdXf~oY%;ckVF7SSav zl4#V#81RQanV1-}kv(jgP23_HMYBElgwtpuTT~J!26brm;*y#D&OJS+%cAk+mfZXO z{QjI1ZYLk5=wW*1UUYt(7I z7zCq@BD$VOgPC+;g6Q<*HZT@$Bct$m;TQSNi1u#&y&48R1@2E#^s^d!7YuuztHOt5 zVYup@!n&tL-C<`0T8q=1#6%T20e^bdL3q)9CBId&dGQ#^TS862))ZAZ=h{?qwt(6zHRr^Z!{d^u=I38m zY2_Nsc_MJI^lRld^ro_ggxrmHnc;+ zekJTh-*IKn3%kAiOLRX8ciC~f(OnYmd*R|nZr;gU&z6ceXkQ9Di?)_}(au2H{|v7@ z3M2j~9mO|qp(C&}d+jbAuf4`?Yr1nhZ(%mjgy9H0^f%}{8mWwunM`kGJ5jS5x;$GN z=M6H8m*3Y&Ec1NjI0*z;I-N{4n&!a3Kwa`sn)UY7_4aj{aqEDYw34aJ)vAreTWBTr zS@FHyiCCAF8a)A?)z?=v;s|yFe}wUxZIeHq;x_g)e}wr56JD=gvXKW{olvV2z`yyM zepWd+TJ2O|g$`%6!bY5WyueIm^Cv%(?C!tt-RoRF`<4zQWWr}bf7t}4@ISDYl{1|g zEXXRF7xnM6Z^B%7E6fHvvUFO~N1TSpZ}7U#_dZ;j#ouJ%KN9}^5ooBc2~&10bMb-k zT8dAH)`*^D0$M{$iI{J!Mkd$FWJo1_Vl|r|#(%b}SABBZwMyOmF}>k`>vc=HUr875 z3qz&FTJ{tN>*bV3WO_)Yai5Ne+-D*pljmjfoJxLRpEk7*ZIJqk)ITbXeg0^O{|kxv B&FcUF delta 1388 zcmZ{iS!fec6oxa&q()npSR30;(wZRDrAZo1HE~4|rPc+MDk@YfndW9R3o{8=idsd{ zVo>x%AG9fo4=xWC83b$9R`5whi$aB>SWyUW5BgLP#B-)jEJR->aR2Z7&pk7Fr#wlR zJIv=-2F{xM&OSa}-RNynL|O4FEs?M&r;<#RTjO%byGB&@CF2Q*a<{={{AdzbBXpF^ zgTV?5tTbiS5z|7>%mj8!fg?UU1kJAzxtvEDik?&2r+{<92TWe(ljW8{O1@IE6t+x! zU=kz~_^kD?Y0`j&36g^|7_?B`R_x#~4Pfe4ZVw>1U7*a;M;ZS&i`|2ftOrEd8 z1e`^)&Q$~#?`W4Aaq#@@ad?l`zP9QrDKGMhulffnpHXp}r(%+W-LrCd zG72Z0P55hsN1obglEGv!U^_F9#^3Fc;-mvU}g>-LzgSa<e?r1$jjQc#PJRC;2p zB`PM{5~4zvmB{PVoKB7J)Y#4xb5cr$tVC;L?y;(RYNjeFN6V)tx5neinA$u49{m*` zl)ypzZub=8ai5l{%eFo8XuJq5Hdwu?mXVca@?eI2wn=ypSajT`Oq>$7kpq{BKLDxm)}Yqd7(K|SqtRJRWo r*f{IUB7N4T#WX9c(-#Y9p7w$cdzleG%fYFWmbl~*fS#&%fSGQRPFFt)+MVAiuUy*tyIomtP! zN?L=#1Y#gGKS;dhCJ7KQkOT-Je+Wsq93UYi909^%Cx$?PK){5{-2bY2_nYq5$IK`W zAO1dhd%E9I$9q-vs_MOW=Zbrl9JAyY{4do+=! zcQ1}UXYr23Vz|0hI^1r~x61xvcnp*%S8Mf3%Wo{+vKX$%;+Wi} zI5bxR2s(?^?KU`j~HT=c>^HZg8xYX&iYE$zazz{K7R;#oT`jzE+ zsojQ`@Vuf?nq^O`D)V#d$5g3&u;oue|vCsM4rAB48)Y{#iKD_(7OZRu0&HBMwXE%Vkn?bjGt~BGfch5E}HN<f-=pB)4)`~USO6hfHG;huE}tvaT8rVa&^FNZ zd~d3>;I~`;%wo8Kz1k(;REPF=7EANhGpoBaWS*7|nYnU9;05ja_1&V`@wtTwz_bk@ zHDHjaaO)fohZ*jb^80 z4gYZOQ!m)wfe}67?I4hM>>Kkg5Isz`=SpRN!jnb39sBkHV`;{03|CN%MK$z?fK^Xr z<4z(X!DWqUD1}QrHmNq^li7-|w^A}#5H2~qSklKB4w5qVJU`_dCG|q#>LauD3Bm`Y z1k8rE-#X+kV%?`e-6!$cGUx%un7fuS1mqe{S0T%)E5g-sgb2!LZ%hh3JDx(ta19KP zU4SIDAFjjhh*$L6*XdHdz4{C!!X1hT)n|%9xSHU!fm}wT7p39Gke*Gze$NHiDg=x{ zwF(=L2k0eH03A;Nfp`VFb^KPn2C_*b-zqWo8TG_mk>%sd;xsvq8V3L;n{$4v)M>W# z@=JH_6g-ag$6{w53o7^$6cyTB%b2k0U z%|=HzWrV&#TVyp3*_l>puBs2g&6kwG9+>bZK#NhA6Sp!LZ>2#}g!#CV7g6I=y#g41 zr4JVv@@wb(2B`U&$s<5Fy{qG6`T*Eg)@v~JN_DMxuog@KPG z=q${^h~#_=*T5V9$XvZvu5}h)NCiNHHsEnJP?|Hm`a=Ya(H%#cJ|+du2;wRPaU>kH zBB3GmE0@N@@%M}BNU#S1Vngt!dXbc4ofW$tTE7yZ8VZOLtRBA zlY_@#FNNSd{1eM(UGZ@6jt-%^5#M)tyHlGbbI1dW*aKx*snOy9&BA!9cw|tv1#x<9 zMiKCFjDS~UC17wl*4T2*GML`oXs!c!P%C@$ZLsMlCIr-L^|zX+Fy8|^Z_EqL3Jsdo z;W{odCa3Dn@|?&QN&zvX+Ks_z{twQ8lQ zy5II-!6=v$`(1(*%wso&pq-aHYc#-Chr`n~zh0TNj45qlOy4!GsZYGBZMqC!CoD8M zQS?_55u)vjvtSXth@t)h{KIrBFb1>|hh(Z*S?GckKChVq#q4+Dkz9@FPl9Hf?XIqP z=iwmTwaYsMhC#E1k^)}%Ezs5=O8i!PoR~Jk_kQe*CBi?0RrPL|GENYb&7z={8Yc<0q2L!?4N#aMt z?~B-9T8$NOa(|AXiz7FF)tiKJ$96#}p@{ef#I@TYQ_M-dC7uB>g7vKy0~HGq&@~J#gAH!AO$ZzN|b5Dk6ScL#fF_uHsDu_ z4Tb#qhg@Mxcra=b?1;9KKUULr2 z%37QG*~n35PLJi-oDfFrha?7Z=YT~@HzItJ-00*Eezmx>HwQKn&M;4H;H3>mGB2N) zm6y&YgFiySTnJv9Vv4?+6-7HEVUZQw#fmbi2BG#YTzS0@e%pqepz^n* zFO7qjT1<0~18x?*7%NG$D0q$5FEolSJEc9x7eKPebu7kHCn9P9~U0#xc6)gmRnuQv)?B&a%6M)}uhRQDM9hdD~ zBD|4`nka3KBgwFZSGXyzq?qvW1r){~MpO&IeR(lH5h}kwE5`BjCLCg>^J2JUCwkG? zB$8_!r0?rEs0lO%wiJRtNC6q2dpMXg&AoUe5bo!|&=u>R>Ijcd$T1miz7ZB=rZA^v znOZe(iYuY?YZzY!aQ`uYLG)tfWLDZf)19izum zeXd#>^+r8!J1tpifC_kcz+?c8)tH~1ThMG}Fx7f)M-E?-T|>*<6E1qWyh&lH#MA+_ z&yn!8*}1xpv0vr05%DbW$+SE51yINJdh>AGyQJ2xHs`$J-aUKH9UUL;W}Ank@_;BE zXt>d=wSAO>S>ZqCj^H+nN@N0jPy2p7)HcVyuVd3H-pGr1wupAc?O!U!$3Uf*K5^gv%=QP z)s4U5@~=0SrFaN7f@lCycq0BuyXw|O;VDrw`3OS*c+XkDpp@peXS8b6Y*Mzs|VM~x<{2tua`I;4|a?6-ZMhb zP%T#9EXyc2>H8zzLFxgtp)4<=$w7%%RFtv$k6mda5DpJ!ppwgS6@-I`cu=OY8j!-V zs6Z5N!2e~t8lRg7yl3|W9(TrR;KjS=UY!TLm-hnR$^!=P6_iU491@}nAI*{f7kS`( zJHc6fn;c3@E)+e$li9%s-5tEQYZyT~N5DH+9S+qRl-#G~r7XvpF1lks%pO1G>=6@q zrc3kn&g7v|ecm^-Iw0F(er~SW>P)u5!0OyK-^4`17vhTN6WFSNEPip+J;0O8Le_^S zMS>~pi6r3zg23G05StjN$Il%(9pM^HelBW(eL%1Z2^|^~c1}ldH9*DEI;!8TK4eP6 zA7}|2)gM*wuKqaq2^7axA!&mnghBS;@rWCkD&U0viG`e6J#M{XzQLl$ou&sCKG^YS zO@qk|=JO%$q=ki6VmUc3f|bjOdn@oyCQIpBVg|v!lvj4iH$?ei4Nr+ZzZEki7OSLz1$c^Im(|XvCE+B9}ksWlR=k-S5eu# z%D$EdJijOKvMBrKK&xsriR?-4brwI(xpuF+ zt7!9)b!7@lJ*M3~qLRy^c2$!%cpSnt811@Nmt9fnRe{l^Ue5xsGN{*oLY{E-$}l1o zbv7q@w|XrqFJQPu*RMj{ko%7Ev4j*&Dkk|Id zml;>du_it1h7W!Hl_UqLn(t(xg-FEt7FHVJL-ZPutr0<|#HbOs6_q)4D*Ck{<_do( zyM_l>S)lQY<5!Eq;5?TADzfTtfiiZzWLl2(mzL`(bq3OC=bDY^1dU(_LqVLBcACu7EWA^Z z@_>_+_|+mMtiJ?&ijvw(Af#O@nUGKFFCnjGVT9Wulmf!Qh$)onGfi-LR%gB9j`GZo zu^n^G!+yOE4-H6W^jo_inq7fk^*vM5GwoedJ0g>nR?7~A2GQO~vXFHA9gBpixbhq) zSMaOFl_$g@`IS3gQJ(qAov$2&e+*UrLqnIp5h%mYQRUePFN$kAHT?>K4RP{_cvjXC z;cQZ5aQvJknwteYii`@%UdZb98OT$S!SRh0t+FU`AoQ}Z8=O$n@>6)aradUihC$v@$Hre+-q=arTJ%1Es3m#a&(S>s!s9Ofg5>s!BHv_GOE?i`Ue4N5 z_6LhNW(vXYJBN;CXHXu){lquI^Ci5pORyk$&^s?^0_KC{yg6oE-g#y|yaoDW=EGa@ ziOdIjiZUMp252vy_#*5sE!Atc`>?qvA+SGU7!!8AXtRPFI1bPJ1P)}ov4|i%B+Xs8 zXZ)-&$OUW7Uby4hv&3qls>1Te+U$;Q%aeb4 zx5d;=HRd$X6ti;6C|9g{7wM|`qC`1^_3$~Uvh3<$-ex!I{o_*yHXX+InCeWV9akIeW@uJuom#>8`61 z=V)F*qKwZ(ZAjOJyjoIMP5}~7H#rfAF^F%-zzg5qK{PK|2PQ9;fshtx(6cFN+s=OS zaDoAmlM}0@HtFKVa{MGPVLwb%LMJLQJb6I|p1jK4K{QWT2PRLJqq>x+&<_)pH#>XE z!xaWb7Oo_hMeAB=+u&0u<_BY4pBQD7MA_L2oGv^2y#QVYJNpwjFu0w~FeCf(Z@4aV zhu+SQGPE&qUfK+}<-l!j_tb_QPQe2>lnTL@oFht?l!!E(fpPSWC!+7?Ih+uC8F=vS zp)52J!Mp#)N+W!*KVpM-YgRdJ8tt?>V7Ca~t%OSP!8`nFdER>n-r23iOB*&G3&C=N z&hk82i0~b0OQ`5zgLmgx?0%C96D2gpKp;Q6_uasq%wZBsC;@V*cAiPl?UF~eI@he&i?YI zP)F9lSx>ONtHpP#GkxA+t4%ZVAHteJM62!9!=GDpOT~&mbFu=zTCCU@1FIr_TnQkz z-Ci=e-`ZbtpMmB+QXErYaXXyE1|r+VYtO=Me%J1ej8G@86ux8;D;53!)k%N+YSDj7 z0_2JhNhN_FcKgZv_)J!QIGYqvMbk$iifo zR$GoJ^48VD4ea3)xq;~^$_>n&ta+ozr_RCF-1tQ7<4m_Nl+EkvJ(X5f*jcxuvOBc9 z9KjMIjs1`#n0z9S0m|r8z?@ZXr|mhiZde+u)0U919JK=q>Ru<2%HWp07AmD;n!a(58zc48e^Zl|OzRqt~4 zl*jGFz=+o@-J?0VEV`YPya=vDpau?Q@k!Y9TG#~QNNs{5&KH{8He$EA){m`l% z@k;ICwqKtfpQX?AcriUvQ>A*l>CMl<>3VPi2b@>j!t)!{sTW?+AD@Zqj{ z>XdlImri)7MDd2&%xq~A&LiOmt#Sa0*Oq3dD)>P%@z(?!HLM4cOwoDyX+oo{0leYN~|QOJ81kz&r=?a!;9zjrrb!21$HF9;ZSLh;qhdXGp*+Q zTzeAsiFN1{8}?SY%k?kuo8VVUC|QQR{4M7K`C@(r)YYeyIZNHXn4ihI13u|SqGGvY zuGHO8Jy4H@NHq6|N~2RrKI&a$YTd>)E<(m6l^hS1()w`LQ&|m2kw~XN6mKF@>&Ekd zx3eekviNYW$OGOby?`fAaaQD$uQoz}%s$xp>3JZ$xfcjej1mTXs4Ad0SfzyHwd=h( z?+@icr$y+bb4ESS-9fZ7igl3D8TI?lp7J=O7#Q)2PIpFiP0LNlkSJ;0#kvzBN(%Nx zeluD4Cr-R$B+k7VSol}&4x(AeI>^Yvk2-tG!$Jl|P8Q}oP2)i)7S(8a-Om`JCbT#{ zgU5u$Pm}lGad#Dcng;7C3r&*CqK{SC0l_+iYcM`mI;%=p0ug6bDJZ(!tUm%nC4-yw z#I?%J$`B$K;lteAal2X1)HC{KO9$!nTYmcy9BSWc&UT-yk*SUx2*GIpCy?L6KUog3 zI%?J$k!^&7cQg|9&dEXw;bGg0l}7l`eZ(f}U6%dh@W^t;Nd@ z;bFUqpfgt-wilFpg|48YZV%fw>y!;jiR!fAaymSe8Sk)R8rVpMa-O?!yG0;3sGR19 z2-G;A8-BHDeyTDSB0!eYJ#?1?L14F?%z-w+>P9OA2R!p^jtFc}shR0d&eJ;HU{T8r zDQCbRIvIdpEd~_iX&n&|EBZT&0r~Bglga;@{*r$SyUxZQPT)s~8~wD-;f6-{lIP?W4!>Hw8$~BoIus>%VTMY;UAxz0?tV8bcb!cNKkTI`TIGING$>N~ zVRxr!mHT0Z9VPs*+5v9k6sIoL50~JAm=?PM$GfsL4_A#sWSZN|5YGn}S#8{x0pYCG zBnyMH*TGPLd~*Dg#qtcs0bhu`u7=72pPY7~yxE7(3He(>N%P?HEFcPt@Di+)U=ix% zkGhuU=v?+QiI6e}Z_;HyRJ0J3vJiwe)pMS~y_Uf(P}$|L#H3DcFWgK;4T~-53Llwx z5gbxzrMcCIiWh<-c~M&nsCBZU7B4k|2jBp*;NS3u9iRu@i!~xfK2q_?9L@wv(n)kT zqyUSH9UQeO#m+@3T8$Aq@o1x2kEbb!b+Vj6Il@2h1OQq1=P&Sy!awvhfbh>wE+FCR z^vYzZ63n}jYPXt6wJeIG)r<}bh0Vd{sz!i8P8i4W4S^Pg(%?4(Qat{&(AEM`(cvln zwp1z2iKtb4Z0h)KiVMGI)F)G-{}T*|c%|cxSp9>n`@h8zbwJehM)0aHl;$4hp00WL zsw7c{T;vC#l1rND4)gn|Y~DE5&+~xya8KZ6iDR9(-ZeO5eDcRarQFn8d4Nq$&U@uY zc1E+t^1xXnIMaoHCfprF`@&fV8Dj^JbM}-ccEG@hS9H$NypjvcZhkDtTPc^qqbbx% zP9$Q+%`F-D(R6nZ%@5W=Mt?sdF7#KPEG0-UL%bd8zuxsPVx+!x#(M01XHkdbp= zclMNra}11}oXZ(JJ!XSbb4Jta=Zqmr>4|75UlWU;Chym~yNV8;vaYhwB)KdGPnAg; zdgCHzOXWIW6c;#FO zF2Q+F2p;R64(7=j=XW z9F>6`HWbMW{C&d8O@IsxJS`$Ptl=FOpLbp6@F9z`Zdf_9-sfZ%ezll2Vupupm=$9Z z3IY@C){~j=*ZpO}MOKKI=7?9sJF*W~YjCr*t}5Z8@A-NKuiR!D(T7`NaaNiEe0L1_ zTNXW2vFPhg7U5TmMURp5DKkQfeLXY+thJ#^X6;w|%i3{_Lcu90t$8w7=z0`&-MLo; zYbBABE*Hf~$C_kM#mVEKQoM@7uNEhZso__QOJ4{0WcQiOr}cz`8{G_Y@Ir0LO5t;0 z8oYu?Eo|pyZgjTumn_@%D9cH4C2@46dUH#v>%AlynNt^8)OTaZrRVuhhT>O?p*u6+ zCgBUS)B>j3{U$T@xBAP}^E9TCg21V0V~$eR)*cb^wA*#T`PuzJ5{wvHv#6hnp}v!$ z_|;G#kT* zAqNTC@IYU{fCPg8isFK{HeK`OrS^mFMh=kRO*q5~!5>>A%{h4WRp>gXENEs>>H}9z zBaS+DV~jBHsx&uO2Xx`Mixw>9>#)vm(o!GI&@pZc)!M_Qc6)vniZSI`W4?o018yJ& zl9llM1zL7AnjK(3YYr@=aqlWD1znU&ZGV{g8Zf4et)Olucf%Z9(c1wifQu|&2fp&w z+i)KrYh3s;|L-HBxcx51?RVzo_OWr?e*MLDIza<(gsv~Z{X^ZH2L}(bLpO1e0kfqF z%%?_$`+fY1$wH$Joc$riUj*}p*&yJfLS$f#GImN=(fa|5GwwBlJV!C^YUkt!5^oNr1T!) zzLieh9O*q@17P{?ZhT_9yYVS1y@&hmy1KEp3LtRR>zJ`peJ1(QO=S2TE-T7)7)<5` z>+A}b?@OqDAfckCE~wVZB+S*}P^Z+I@jHumFN#Z%xbJG~9gD^4wp$j%)o_jOeBIwQ zGq*_Ia~<$-MXe2=7hiy>Vh=;NaG>_JIg^EE=Emn1CMwM`p5TPKe%gaBA>DEbP-ZPs z2J~aT!v-h9VTQ!{*ktf41axSBXR(CdzFneufc?gP!aja!^Ux)rMG1%8uwNaX#M|+T z#Cz1VGjpSh;V@84b`gaRi-1%eX;d3{{o)bI;1Q=G-YmvKxc8|SZ14C-IuqUw>U_t( zG4Fzk-!4zKYcrE%L`-|kJH01e@1No1D+^8~u|Y$AMxgeNMGsaXBp z>Wh4RA_`pYFmMSj$7U!dr*11+DaQ=Uyp_LA2yRS(;5F6PMndqCI0#hk*bp0S41JaE zrN1s}7ql-qA_xUfCXj&>$e1|zG*kj!yiM}LansxpcL&jNUDiRyIQU)8p7O-O85pJ( zi5fHa0>`F^Cf2mlVrOD}Va!*}6>WSyAgF1nkLq?12wFoSoFO#Lbxycul8NQ^ZV=1i z3iwj1ECx>oSR!`Fs&2a?yB^etkL)f*HS!~X>&MhIiU*A8vbjNFz|%PGVO0FY#6!Pm zelrU5_JlV9YKlbL#I4P#0M=i(()dJ?w~`l8!&JQj%Dsp8BlK^uTD!PCJU*u3V1rgF zxDVT4Gk1t}uR0?M`(djSlc~kv6lg76I=8SGu9{oe#bmTGlTnwi$NRhs!3Uu&S8kui zCSq)*ISU&W7lThgy>KOR8cwwa&az&@HSoqi0!!C&t+V(-DgaB4#^MWuG3YON7t{)N z;S&ylU>pGzI3iH15U4>aY0e^-(!kLJ*MzHABa{Ga>60l^M)O4%o)))yr`xX6|c{or5qrFbIpEb(yq>VdPS_KETpN zm;vazgJu5hF{b!A$`mh6!5XPl?qspXSBXk$LJ$jg0t*cvc`Ac^uT{$uJ<$d^cIA?- z!pJ~_{0!N_z*(eB<7G5dm~y3ObF^P6VTv}&T_&-@C%6Kep~0a{;$v<8bCW_@$1A7= zo7yU4P=5<6E>^~b`W7eDF*fp3ppr}R3s#}+_#`T;Z;9Xry8_c;!q4Xc@3vmR8)7qz zQ&#&!XS~JPQ=WAO10&r!gN>EsvbfHO zoHG4eXM}4s`5To=s`~F6!kXigI1JdDW1ympY_RQ|yWJ}pEeN;odfH^o ztgd;}J}W{{tqiv?wReFm6mk99w0Ap3qA1OjIf-Dg(`z$QOa=drP{T#m9Qf4sY;YRZ zAP-^iMTxnki?qcjvH|ya3MzpeFNLu=CX;W>{sHvIoWMWCCvpPQlkIM0*2V~j6}>?D zoS0XKr!ey_+Tz)uSv*)S-sE}8KujLiXOhjcS?MJc^nx9f30_RfYA^4L2kTeYk) z7H#ifS845?HL`oU>Z9?3`8}@EV!HGi9yrs1SA~>i@{` z(?|WkR+mLl27m&f%K%sbIHxrLUV-%C2Eah)Hdz3xI18N?zyZwysKCuBhal%1k^>;* z29q+%U1M(@q0ErlF4#^2`@sE{y!Jy_(1Yth%BSF7#Iz8+kt3YmYFLRinAPxlNi9pF zM_3JlvS2kxd1j8}WUJwCpg(3c{4G9_)j&^iRs(~z7pvhk)2S3~J)Epr4|@4{DgjN(s}q*Z`)N!ot`z|u$L zX*oh95;1WA3%1390Bp?9PC<$xh(jsZ@MNmn<{{lq<`|~0AjeiPd?Jag zRrn{<$y1*o_7FBJcx9KtA{IIVa}ZgQ!C6JtR_Kq3EDxVZWYJTc$YQX<)HQq3)x3aD z1l|BX7LMzxm6-OjOM*8p!J{V|Jjfv#VWTVe0u0CVwUB;cC1R{*!V|uj;*w>p$X*?S zY&~oE=%ry?3AEBjU^SK3_X$=lOJ_!}g0L&sSP%jUy}p;q4#FGyy-Fqakc3N}g;9~< zM6Z5aSr`hHpp4B>&q^f?F^&smBz{U!L29U#(7(&!9NE9p6MC|RwwTdhr_8~4ffLA> z71#5i5{TnezR{PtJBap;vJNu(M*rB^Qy$+a10!A#iouV5^gD?r)|}GpP*E1wMv>g@ zpXLra0E-sfM|pj}(QTKW-Ul_3^fV0WA5+spNwb+jYVEO{_T5Ta&%qe!&cRTYYwA3V z&+)#5Xg2dOHfnhocH?F)#+R`j<~-4p@ytp5A6923gR|vh{5!Pe()$|AjC_p$hI-la zF+L5oSUv_02bPba;|W|8IT-_z)-*DZTP_8Hg9%eIeg)QM3uh?i>E7>+%Mu49?b3MubVAV+gM z9b`17{-3j_JjN6QBb_nD#!7NoG^W%%3?73(_uCDEd0e1-NIGd6S{PNT`!1tu8-SB! zRFOdicGX`(Sz%W(G)UT?&e_myS3Qy=!uKK6kbla77lxW?JJ}uuGy=9`4d&Z@pTyi< z4I{#WO3;EF0_NaJ_U(Qi`eTlgFW?h7O6bYE z5n&*ZNz(tM1l1+YB!}N)6Q3Dz(O{*96C;)1=iH7SDsDymU0HYF#88@eF!9gfjZRZD z`gFJa%$f(N%WkHw8abu3SWL@@L=#S~L=iw9SGJ1;BWenC{blEmNNEfrX z+^80pdTG<`uIsi(LX;BsZ{j@DA?`wdh~UJckWH*{l1(q?)N#tD^zlC~YtmOxvg7X%l0xty4p6jlqbm@^2JPUNVGSlb z-Y>C9wu1%c&_V>UF_xSxJH7z@G1>7&d?MLFPjRw?!D_J8j#>-ZCcMM|AM+h|)k+K) zeqDn14GA7S+2HjdV2B9{H{5plkvThLxP}Qb`esV6^g+X8MdL>=Y`ElP7*(1MOBXg= zZPl`dVRYCKyV57RJtBmqgki&903#V#;gkC9R^%)q<12OGaElXT_x5dtOfZ7YP|s>1 zv`TPYgbvjUDK=Q@W&#YZ;9(>TYU!d!3AL4g{yJsw@DeAGaXv$+1fn^Wzw0`82hskn zebhllf7jEUJ>~ItF));_i<<)E8zqpfnH3W}jO1<~gNNe)OT>CYb=$R`co-&dk`kxj zAvP@(Hk%!!uz$)0K)1r~fAH|77N7G54_}S#Bn1y&YjtKaI9u@Wl}NiOvjyFB&{$>! z5ATI~*@K56)MCLy<;M7QM zd=5?t6GIhu%$?LDw4uPe&?|cN=1kGA*GI?bsXaaF$!KmuDN~1jlEzN_l;K8**x&pK zHiVV~(G-x{5V{l&FN7o805aT71;=>#jhRjr&+dYA-AXMuIu8zk!^7OX_VivXg2{+* z&`tvenAnUZ)P7&ulGp|hBIt$S-?H*L;5ZMNIOh#4pHkKX%gl6U#{s~mZob8d-bMtM z1<4_}-1jtltxvMY96s$W!a(a!fO+XACX1qHpL>=6OUcc_Q$$&Z0}L^5b`m4TTs#RX zx%8e`NuU{Z9F^5<+Dswlv+{tqs~7NcgqW|&1LWn5E~!lJo7^2lo7}8}j3&45>?x1Q z&A>=!ajpFT6ad}EH%N&j<2yhj4uZ@f zjsxz@XNZtW{uviE-S)Sp{d(Om%#Gz z!D{H37gPf0l^g`-@JV*Ay&d{v&b4>o6FJxD$@X3`3uGiM>Kb8D(bC+O4+Dq${1as2}+}iQsXlCJd&;7 zDr%iE0i$+-DG0iB)CT|}O-J2x%rS7H6#Qo(^MjBb^(U0oc}OX)tQbn9tnTB3!l|qV z7Z^T@k>|^Ce8FY`_b}7EghIi6hz0;r>#@ADYsd)&CnyUFP7Wb+@Fgp_o1s6Z;GT+4 zq~Pc&PQfu)dr@%P&B&}sYqk{KCSB3#HO%;pT=~N?^pOyKK3PsX9p-+-pXZ&!%B=)% zlXZ_Ck(4gXv`JQh8ax-FM!Bd4b#M^gX_Cn$kmw|5W~J-AkP&FsJk7cWr;k%cr4K0g z5P^O*LQ)7`$pMOXL+Drw88VACn34Q4iH)VQ^P@nFaD(V_et>HFTtZH8@xV5pz**R8gdt3f0-SG z<{6^mvnj{Y$DuK+LbP%8LeZZG(&@HSx={3kRxN8hMu(!YD}Bz}Lqk|m7>fRk?4bXt zq=Lx&V1(4s=pRa$qKtHRG+N;k`~aIFt58LwF^5or#iCipJGt7XnDnw!Os1LScdLv@ zNR&+1gU{M>L4TbxCcW7SWSo-`DuJ+0DImm()084~hOm*9}v$vdpDagc8o!@7ma2oIDr+A9F@MRZ%t}J={`0J;^O6;6sgV8 zfofx#?}|HSSQ-w=f}@==EX||RlV^>2@PUE1oz)lA^{tM8wL10^!<4BJxaa<*rHGMg2PG@Nui@ z*UGv_hb&4@4<@$#9*Iu|75y%#DltypMb)69+VH7OvvwARZ3sI*#Z{i#Z8F=R+~zlzhl+ufvT ztzV0%_NhPs;OQy&Cq8+?3N7KD$X62UiCQP~%C4y@G@YO*qMIqZ@(-C58d({y|B zi8LKO#c4VQYcHDa3_ZrE_EQUOp%$hEOXKPF;x*qRA+{}Zx^UX*VD}@ueXVnRxmDrg zWZg%6cw2!Q+>B78XjG#+xCGv%luRL><|2ld!44VWW$HbrbSLU=H#mg5p{Pd5HOrqAvS>KJ>@r$qVp_v z!CO0wDc}=<3xJP>GrDSVc^4nyzDI)hG6^0%+2Hjd!i|SA-!MOG2e-A zJKT#4$3^QU|y{F2RYi)ZGDYg-Y;`*bMcoR!n#>GeN|nV&bRtK2uoxiwXU^ zwpvH_ua-8O*SdeGL6m@~y3 z^LadI3=E|U<0b@#y1&6_qONN^u1i{QeyOu6*Xl*Ywm$QvIm{mAL@|b2+n`cfZk_G! zAevjOgN)p|*x6GaZZR;ja4WeiE*&H1XCD(2?uU^Ru~<~yb}bg)1T~UOGD=LqrctS5 zVgi$GWO=k`=6N5w)mVkG2vO*PEumRomOY22*{S0@JwjSwK&vRW^9)KU8t8m zF`)^ySYiV5%$bb&?erV0s>B4At_GU zZjXDgD;};!haL2==ega6ee`gnMhUk=mvLissnseiU?_RGMmy_kSfVuFft%$!HP|U# zUl{kUfJ-X>IhdPeQHf&96X4;gEJKJvT%)wciu!4^KgEYiZTaLp)7V|Hn{dI@|sGGmE^Lx{!;TWI2M5>0n)$qSBQu63+bd8^=RtSxd~DXbgjc4 zhEGZBumJ6OJU0PH0bhzSG{}7V8E@U`1b@V(n!F160Ouw+$B=GjiFLzI5nX<60u`P;WJ(c>tyiFk zBN9wBP2B%HblGK+DyDD_FRkg~JZU8#a}E`^BL0Z1JAfQ+lNd@94<@7J%Mz6+KXr@} zegG=DW{IwZzd&X4D&e2x0q=)BftMwtmoY(K)H>pi(yKnUX~pV}jDCmei;W zK96MUw_2i2bYgTy=q1&EmtOh+z>`)lormP%dWqpddg(qs5}bPJ*CLtZavW4}M1p&4 zMFf98L|i9oJ(gE?jWeOk1ZBaCCdZ9A;*xdQ&CnmyWlzN?(q;4%r^^_uz38%-!}$%J z#t-L5V@l7(FvG9XvGNEx9sv*Mw;}FF_Q!iM&JNNdl_^EBJAK>#|Zule@GX|mi>r<(jw!V+b|yHia*BkKk~ zz(?v?b7m4aEXcVGpQUG+ot@yB%rVLA)+muZtJTg;0~)ejDJrW(M_tJf%ixR1fuLs- z`OQw`V^&TlL8Y{*4?El)M7!Qt2N_*&=Qw-H<9cIYD770mm&ofq_>8lsJj`HV=3^S)e3;n@HW{L@I_uH)P5zVdL00g{tLtL0L!i#yBllfZ@jFi{DesKoH)1sVAA zDt8Cbd|@4!d|AF9?#z*>&<_)pH#>XE!xaWb7Oo_h#TY>3s;YmX>0kxuj|kVl>bA@E z{|Q)DCi&agnPsTPxZj_xMaj4JDZWDO_O0ou!J98Bwf**lH!-&`QLaM{=ftheslYFH zZsh}aW4MwRQ377Q0?K{S;<1?)e0)s9!GK1fjB<8xTv?iLw`-+_H`i*G{dU`H z`7k|9`&T@`|1{1YDo?2gk6j!2OhUvzo9Q^QQAbTXP>tK%8%HDjtG^3?quhrkG z85x@Iq0Jq60a{@IWM;bFa3$mvqZ4*?7#vFQe95@aIO3yU7Cf!s&=~U$jCl*A!wYTr z)80#^IHRzTRt8G8Wlr)y+LnAGk##Q6#3g39S5)LHa}HtZZzP9KW(T)H_Ak3WY^v_l z1$1JR#eSDi1y>-K3c+Q0nT0v63bx?4L2?NR4j6;@E(Xt!co%B#!r|-0-Dq-t=&vNO zM8X4EAPrsw{Q!hd!b-5+mAtao01JZGXaF!kgV%MvDh6-rdZ(o;Z^a?FYZrL*%N^LU z00IDB_$|2U4J3r$V#pMOw`*;L34YLaChXn@EdzEieh$XEZWq6NzoALG(QzeCF!0Yb_GF!Q@nbC4m`7b^NMR2+Exh1*`-l`ZLhlZYi18FC)m@NMxY(Ne7V9 z`@JLHWnQHOM_tkuo(M7r_ag(D$=2ny{HR3`H+Y=J_c>{dUo9FR+Xbc~aQ8Q$uH8m5 zbwAKw>TX{^>YjC(cQI{@0wK~VwIIKu4d;o{^^x9w9vL)-bTG73(b{%OK4cLl6|KKb zc*Tup{A$s9O9Ie}@<}Cu4|e;>eE258>qafZDBpIVO~bt7(?Dn2)Q}rsUwCC)5Jimm zaOnZ5vhY!&Ok!p8c9UjqSox4Y2`a@4bNp(tp|IZpvIHn}76S&@EhjVJc!Jc8R0al2 zp#6p{Xu|~P)EYCMu7c&RZA3zWU) zYhldLxJ@)j!xN*LWn*66YaQH!pfaT#U0#xd6@|mMvrvQ_1$?1$B8RJ z6<$h34MQ(HC6*?`7GmM%*rH$r1&|So+VB$4E8tOJcaXOmaI||7*CJy7JGMCvxb()BXTfr=;brRe+Cjfwt2UdJ zac_SU?4wS#)WITfC;?o%-e?Z{N9O9aa;-y>SaJ`+nP=Cs&&;*FR&yQ<5$02YHfJH9 zuL6)WUtS#m8@}CdHFu#f7amZ*JX1#dU8Yuzry)j2!4@w8W&>5b@=4#{b?8q zjj}&6eCfC~a^ZzFc$HuGI_Bnv5rG!z<4ff#3=i*Gztn2LXb@^=P|YjKtUH}3vj+cU z@s8*ggWIvtq9Qw!e31ZHuc6=Vel+2*{Eoo+&wcaQw$3S7nk zQh=X}2_9o}ZyvlD=S9q1U@7+@{?gyQc}AC%8tT|4zf~(Oeb%UBxNn;nqVJSnDY0Ru zp8PFYLki+TCtuu2rjL1WkmtCo6i4M#Rflh>cN>|{)uh9X5|^kn z-H4dQ|0FD+&tK5 z6~9$G1QzBY?rH-IIkB9a)xjsR-$L-uy!-Sv|2bHL1-buG!tP!w;HGtfCwKuQ7ft>r zZ}j^b^v5>q{3kxqW*vI6cf&BPG!igad)erBig2n$X~qp~cZ>|F#;eAs^n0I%)@PD# zJPXISL<8M?mVy}LivuvuFWga{*&%k;sI3^QM0H`G2j_+Lf`%=dlgVY#(oqsHSdT#W z+tOL9Yl0|KM*+}f>ih_#b6Qj9y*LiIslyNj(AgC40Kil7aOAKO@kUO6Ps!7JOfqq{sKTjM|3d;5w)hu6<)9&3 z=3(cEaoaLKkahcfTAo6b-+HbrnE@^RH=N-z>0+#wHBiYlS#$}xlFIhaYT1zo;O)Ht zENm2|X~mletL36R;GN$Scv-BLoAQ8nLuT+yiKvTTF*Hyis%aDqAzb}dh{M`+(o)t@ zl;dG{s{1Z=_#K$wY1QE!NFA;Y85*PxzsF^SQyre8t3$E!7k@#Dc|{S#8P%dVLQCeX zE#+YFBBVM5M)6N3$N58u98;cm@yafFAe5(|2`Ep=aC4|6E6+cI{+RN79X^rrq^CIL z$zbh8d2XC*wmVTCf;C)q;`eyP`J}#k2UveeYV%Ya{selr*SOmWu$(o@R3`f&2MQ_7 zzjlrix59k4tQ!uL8Ut6CpO%PZ@Ra{ERB}xZAro;y@i8jfKXv(Ec>w;;UH}%#F_pSp zy2B}YqEET|Wl!K`QJ1IW0dFHz%EoR}2F9sNHHm_MK(6*%U9J-HB5LiZI`2}Je-6N; zRhExM+HhsbkRWCGF2pCvO@8*3egmB1WtpFV$p=DNqV$4~5QnZna!IQ;1-S~%hR8k$ zbJdmdFW8Uc0K!=A;n2MwEBFgy6ybOkuk0EcLQx8OfTEPFHU~+vqAWvyOi@J^!4?2a?@BJt|7|8Zv7Ws=4`g=PTT#Ku;dxOGZxi!wsYvQ-{p z7Qf}FA+7l==a6x0&D&+&0fd+ZxJ-;F#kr1*1%Wr2n)QyMq%o*NAFimiq156F0FB^b zXbWPrH)dY6$_~cipoB`Zj8+&Vt70;$?3(!u*a?#`hV$jKO@zlWoX*!;4)y`Hszdub zizQ^nu4+M?W0~VrSqX4FD&ysr4;i?VQ?M%>_Edl`BsV@+ho|d}p`!tEgBgkn!_mcX zqk>DXV46|_Z6g##|bvcfC#%%?u_7XBa zsDugU-{yhyuQJ0KuXY6a(U~4OmU89M9Y4urPkXnV@{8e$mOovqECvezeuUYhdharO zCZR^W*|S>pQT-9fjvrGGD8lg`Pc|#aOek?;cbh)?VLiuRN@Ce7F=6JleQ?aO1zOZ6 z$+Y`zeDhxZbl;iM}~1QFuMM@L2x1-_*yAosH!cn@t#fn6MjVT*~!gqMyzrRHON zo#^bL0Z=OQXggj`2{mi&;qrV7babb_;7!9O5Vp&tqHnE=5?s;=!>n*fq^@tW5jm+i*GrREBThsX555!z|Zo zupel+;m^04aG(rqTbW^nVPgTD9s&Di6oif1bO%Z|>I8$ z)?$&%{)vP|9O3b+MfkBym?+j{s|U=p`%Y%we!|6#Hs9f$hIwb8or&i@(BFBy z4jQ5ypgTY}!vm^Q)OJzaO0-v>Z4uTDH0RYboV>!X7O%D>K(4rxR1)}Mx1Y?9BU$<3 zY*L)vur)=ieD634$OK=JIJ;pD746&E4ey5LbY{5u%g=6j7gkD;=UM{ihU=;|IHiG! z7-3yUoXuc}s4%jYYBl&20>RAmuA*|huu_HKU$US{xt?6Xu7?5j38?H^(Sl4Cvi9Q% zMI+B9(7O724rfAl0AV5ca6(@Q79U^8uuS92J;3?d1j6tT3m<1C%7hgQGC6qSe4VSN z`SvFBywPCWa&W({+bO)U>+y-avGf%0?#$^nygHOClXw@~@p3sT_tfNduiY%xLB;7B zoLvP6^)S~O{*u17QmZ1azJdMWu<@%@uZtJ$(jlL{yzs`>Jk;5_^B|mzJj2guELL+m z=XL<0=SmQ>JLD94!8zEB<&e9s z-<)J>T=SW&5Sp(U0--Jq{|l#FTI}VTRIWR91%>6+Xh<1+)19 zV3sdTm@9c;zO@&abuXmy_~~zBe3Q@21L~1pKwT|m1RpI5-evHK5gh{W_-vAOF%PUS zC0Nt#@_D_xgXmp8tOIkG4?nP8EHt8yuYZTLuRQyF7$EVA&N=8eSmL^iH>+g{rT@{M--4kV5$#dE zcSU=)K#eTX9_#`AU>NOTCKtszJa~;Zsyo`V<#K-7IfNx5;v0gWNyQ|egAVez5VYK^ z?#}ZZyfI8vX>z!LD$?C|;O~tHkXUp?Q)CPR92I{KGC_E>2Vx{XoZAF(A&PE{5A%qS zr{XO+B0|s#4>8#XnEfUqlqw?h8=n3eMfsF?c`g$#&&euYf;+JWix5T0fUw8QDPEEn zAiVED0c0Kxxm}2&c>f2>GJ-fjsa}HvS_aX< z6Exp}BN#j0RI^i+`xPE3;7FPWZ^JQC2>vwhP}yKmJ|eoA>;~7b!Qe|p{gs53#>WS; z04jo;e~Xo*Ap+y8FTu^n3a1XnbsoK|;xXZdZ}BY8PN|0HCDKU3li*V~_ z2sd|dgh%W?MMaHRMpw9Xk8o-scNu5#4P?<|h}|jAnBb;p)#B!gF_ZLeG&nbxK&5zY z;#Z5Cr>DY0aW7qY;FjH2GPi!&Uv8aqG=t;2Ey|{1)|pOb;a7`U*@E1PY1!%l^X$Hp znO7uS+-Mt9&Do>~#{P4PA;E)XD9}>|%if!!RURx8CXWb~X?Dal7eg8zbN#Wk&tZOq zEQoZG?}jEr8{$Dc4a_UCGnQ)&i2qPF9UK7-iF6ZU{o2@)wMFMJ~5Zs@a1bB-3=X)c;Ns)5J z#`Sv&*O&?lDJokCzMmJVQvs>(_J-6pDFx$%qK)9HT~b=5sH~8sdT=)@R*&7{WF^n) zf@=hj9fHa|2(B|SNOxtM1X~rxXJGF~emlo-`;zAkrU^E5GVVKytR*^~v}|`Bn5Zi2N$J zsBaPzlv3qaiNbmFt2T%RiaYy|6xDQ&Yj;xAL0NY|aUg*yKNL_)ic(*g-L*=-kTaLO zVb78vW>1Pz=mmFRGgea63L-y~uId&7?QKy{CXun+C3Yxx(FtNqDEFmM2|ODrl)D_q z0s{s%?a>dD60di66CK87-NesXJyj$mkm6TSqAZ4SBT3u;Fz%fIQbZV6_1+c6orW4& z!noLj2;;IrLIKMvyhbGnEV7Qj*Ej8*3O63ZY6Xt+nZ!p$i*#72RYRMhY-{4Yld&bLVK zMuT(nJVGOGR~CM?xS7uDFQr46t~>~DyRT$!J(^&5qsbl3X}EP%qXfQ1SvMA(StTd4 z@TB0%q9lCNtv&{bj~_a6Tg=AR6jjeEnr)nOjs( z$YTF1i!`aI{g+N^<5!E?D+PdxwDJ|u(XNq9$9MLZjy$FW+k=prENtT8FL3I^p$?Hd zrb8{`ocraqHxf}A)Bj%_ z8c36CFg(oT_L=sy595Y?rV1RIU>Hs<0U0Gk)bKDHq%_rHXIgU5?4llNWc#bnK^8GD zPtQ%uDHl+yU~y#`G~~_ZE4H;TJ9Np4VXeC?s4hN&Fx&Nvoq!d$BelesBH2x_Ge z?9WRG%q+aRH$pr*<C)}`J^a(bExY?I)2MS^to70Oh# z5Ii$4%9!PNq&Jk;8Pdqc?e`RJRzZaXm4WDZUL=kMBwo@R5?j;AP^D0UH!HLft13jP z7DUSPB6=Jk`lhUi##;)JF)2+>j&V2>WF?5#LNJ;FEN*I`?#^Ut#A5)?kAMfPgd0vc zgq)QqU?rTr(vHX#u=*-s!va=c!zT(@(bE6|R@>^$Qe_g)Q;CmQoj{IOJU0z)$l|5* zhOADLEfld+G!WF802l6WHvAsX-TWnxOgFhr+s_@v`l+lNK7Alt`z7*SVXW2KCuZSj zZ{&2Rb;`OuZVX>jeXXwt-$k_LB`~KHhQV5F#tLMe9K#b5!TK5Kt3*L2oK^72`T!f= zWl0l@?M^IW_K|LdN-ohMP{mF91uCm;(qAWxEE;AUB@J@%nV}TFRsZ}vke|n(QF}U< zTqyd1CzCW+xjTs70n0kDcEI9%65TAPA7)QCIeW^p1D1giFDbh@lw20WgpmWc|6#%n zK#+(qq3XRWO!z~P8Ck-F*n#^a4pDRek>#&Vvgywg1?d9B)fwb zA*s)WCdVzQq^D3_9a0}&Z9GjQy-n_xsT2qPChylfaNEi#UGvd z8yY!iq{jcN*!X`%*6|;_9BZ(+tQw<7Auju;Ss)d0*|%V&5w6?&7nc<}938KDoLwZN zET6YB47f{2y^vfZd*`E>2*HCx_d%ujxGa9vjl}=WxU3CJygn3h*$)tIreSMIZV{LL zTPkWen}DWhU`iny9^Ktr-?K>XMuT(nzX^@FxGa9PxS1}_D#uy+xUAh*GPnMVV0WX* zU4x8U&L)K){=F$$<%?sK2`LxHZ=#}o^TUsxWirN11n!4F11lwnFm3eW+7&$4q~1Yw zAA$p_n=RVwf%<)T_;9lYM`G{t=yYpHuxi3ikOmw;-DVDW_=0wS!dAs(SNDh$3}&Or zDHmLdII?A4LPa@5S)D>~c@~6FNWmSlmZN4apI1jwPmz;LQp_ zM(@gfDjwKWs#eyLQUg#`~$sV`6wC?sm6sGhrw+MuwG8(vM8@j{-$> zg>C8uw~y{VSvP#jKzwwJMAp%paIiEIj)`pcLrz~ZQa>lb%AQ@WPzpYa%~(!3EstFD zW@&FzW|x2431Uok`8S{v*wM;OmY7}sQ+GGf+2yR8_zA7MjlN1CnPWybe??hz8%2_~ z|8Aqt14t2WBh`DC+vt5zBa7Pzdq7o(hlH^~LSB;(z-ydI-EJd!{aJN8C`#+RF5Fxr zs>%j)iMdDb6=Lojd?GPNPXiEh+i)YfKiR0&;$@u{tB|Gh3cNFA3q>?9$0&h z@{w$R7ju~RL6e=!k)2GMYl2O^gl#kQ!$hI&L?I?((S%AaJ4pyDj946`vKpM$0XGU( z>qGoe$S=wR^YeRwd00&io5Ee6;5relv`OtZ1dK82(k>Z<*6w;=(T&7k-wN3&BsZ2J_l0 zN*x8S{f4vI2u(F!gx7v8RvO{Tt#4lYQ&5MY$DaPO^VD!ubbPjwvHbCxQjFGrJ2Roi zJ-25-rFcI)e%0+B@bDfV0j(SlkzZk!+U>+kOEyd4^k9O^Ih%zi%%h@)C#=hdeum0) zg=}6t*a+_W!*~1~I|toU>+%~dV!46m^uLaPkMo@4SBw6qIG|U2aFqgn*zG3s-VLSG0LA799@ zOykQvz`3tqSbD?^HKgliZc=Tn=( zV}Ul+q5Yl35*lHv9^XMsT?UX(z0q<0)4Ke4{jV8 zWsq>#l}kPdx7kym?6mA)a03a%f_B&3avT8yVfMqs`S}v0Xm985R$mmEqJ4*fPH-1C zV@0Bd#K>csM*KAUzbB!8SBkhIr`G@LC8w|Hd>*&lWGlqV;fff26^#biPs^cxsW6ibGfWa?tb zk^_Pr@MJRMK_><=%($O*oQfIWad!~S4AwzLX8hdQQyyk8F!C~ksfTW6EJ$W7Sj;%- z98+jp;x{@RdmL0sJ2SSsJBVfm>mVaD&T{sYhZzivoXl9G>F6$w46%=bE#jv+QLc6( z6f;q-$iSNe?hc}P!#c>wn`viHd3eLX$jO`G#-uQrqF969G}h2hlQqwF;uFJ~XJlZ_ zi`^YWvxaqGvSt~Wg#s6PHW7KXv!6VSVL;?$%xY=BcJXBye3FRZvx&&NoQTA5gL__PIcX|1y#Jq_eL)Tw#FZ?jV{&tOJWf?0Qx1t>IU%n#im;*QGb}aEJktg+s|@G3XmbEQ0TY?(#;ofEBV)zlP4u4E-J`xLJd4Z0YRE2@Yn|g`8V)vS zrGiVa4Za#<-K)-M1$>#+iD`lrgHxcjaOvE_Vz_EEtfA& z<5RdA_*ZFC=3=lP>V+%eMF;Lu0M@cz!Zq*)j}3?08#;?GqyliidSmg0!5H)xoDH>t zUHF7!AQ(qLhd7Q1)G7pOp#B@qBbP4#htDry_}q$QCdXCqXnbV8D-|5S;CeV(6*f&? z0ge?oF3P)jZm!il0@qD-DC`Xz2d^y6x7#&{eP7pVmVNN~T<_1oxl(QTE<|5%jYts_ z)DXf#Fv9^A9!qm**q#x%9v-?k^l21~&-&ynb9h1;#xWp58HHa$-IX_k!=&O)Qcyi2@gkmVkN0TfdzfJwU1rD1AC`vQxygWU_8-HJRmgv zR-5@itvr$hPu(Xop~vIqABRd{3V>hQ`^uen+a6~8YWSr$hMVI-mP5l;2$*iS8gGm- z1yl(BfuMAXa1oaK2o*Kri!guCh*0|Kqyw0`u-`l4UB*&)O08CjIeJWz2lt~IW)?`7 zGw!Draopf>djE*Pi=#Jwwdj3p7nq97-QR$^b{omm{XW6u2A^k$q@nKi1*Gm-mw6W> zRdH;0;J|esZm=psUiVC=>h0$t6(dO`7+R`mZTs4`pJx)r4J@biR;Uy|w(+Y)>n#aD zE6OL81U}g9C-dP{g4c~&20m;%(57Jy37a_3X^ay$K*M=uwC|ZMZ*U=8dO(^L_$W~( zC3LMtGdHZ94UcoO0l!*oDC~EDECC9g#ee~J%gGG5yuS>XKnns{(1r=n!62}(E#NXY zFp7o9XP2*e&ACo(w$>)2If^sOu{oii*$+t!;?5z9l&QGWa&ia1THM*20~-lvn5Q=I z(uO0Mm(8rabT%mxeeO#kDtRIX8V(7ItKcqFFNNUkRJ3o2K5w&_<{k$;(dSRFQi7eM zsnGES5H51P0yhof{eQ#5aTgoXu#LzAvhD&rF{)KI3_N;MeQ?sXaaF-Qs!MX}wYdl;gcZ8x62*8}k3Kq{}%gflGUZKiO{6`XU9r&lrCv|TPiYB^k#!*4`Ix6E*RT2+S( zpGK)J>*JNW&@lMB4%Hw72#R}jpp^7}u~Vk-tmsk;9AIU*n0LNGE*>>DUh_zXQX8L- zY&c;I?8!Sv@bXVJfXq3v=V0;3*zKb*O^)o?dAoO}*KX|H39jAZp7C>b7LV+@eRRjp zYfH0Jl@j|Uok7_vnseczTyp{MLR6?|bKy)j${47CO{G?vDHdV)PoDMky<^_^_&Cmm z$;+O;cT}Q~Q0FUmzDcVip%0#bd?*Bu&&v)k4u_4n0S&g=p*uQ}aT0nh9CH{LQPwkqW!38Mj-jq(?BKI@M+cG9iywFUNUNbFZ~zF*vSo z8Ay_+>X-85966VpnGaP|P_9wF%&)3D1y@RBm;on$OAe71c(#*0#i}dI?isS~(IJdV z;9CvKl=*~dJnE;qD%V}=#JiVCJfbokV`9F)50z4=&nhOQ{XZ(JZHg9&X9`5|W@Fgp z{d!v-@ZQ=JcsvzL11~-~>mzx<`%q@^;_XI}`iF63H6()Hj2gq6RIG~!`ph75(bjv!tvE@*|7H4U`&3U2z~-aWXHac&+r z6Z@^&A-b(zI-U~CNof)M6#IpJ8@zjAnkO5_hp`5WY5q{c?yiPsdk8#XsVrG(4uT>J zI1e#dzG#G-gKf(p*t_8|@W0dW=~#TCO%3#9Z)#vdek5S9Vy1LneX~V)p5heF37WzY zW#ctY_LFO!yCQhbzYM+c<1id5$F89T>&Cbv?RLoP-%+2Mo@wvs)82$bY+6QMUc`^%G;#(*)^gPwUJYy`B%*jP_4i}r*P zEWvLf(0#WjHgJ1FO34U=LIKfbPz{iwsUTD=c#E1#TMP55Wtd` zhs38~cv3Mr4uUUYuZ7_Ayz}(t#A#TAnG>ItkSCiH0!uI_Bqz-=kZewT7y4u7#6$Q* z<^(;(nG+1wUd)Np@Fw`iB)Q4jaVEvdnn|ITk2NdUICAao+sMbI-dyoOw|ZZz7Z9Vs zd?|TOs%-T{el@>B)(xiyvl2J4q1i4~y-HC~JB!4!P04lfCSoDCRf3;Eix;3$S}ndQ zWm%MLik8922v^@F+X)dPC~CQ+0O^u$#{gidrQ79*JYOy`j7YjIMdYHFOJc9Gy#*Ls z!4-(83L-<2D67c08Me{L8&x}9CeX@Y{4j1>{#(*`rcM7K2F z9P5rveE4vLq{F9M1PzHM+4@5KG{w$59|qBS#=W|@P1ZdsM3Dj~?5~?ElxSoSKQDkv zX~oa;sBGRv#2@DY?~i%{FUun0Z}Ncmp3LBx@+HcIMd7VRWbj;stM9^loiY2O#I^#V zOKcwmkW!27uONxI*k-tp*lr+N(PCTXCxqwdMyc>}iN7Efyy7h6!@*Jc^--^0JLuPI z)h3)e!BZM?)|PT5cnD!E1mEGHMvE4mg@`9+`OjhvX2O0;;+bs13YtI*lI!MpNj70u zoGxG1FGlsa-AychJz=@ptR6=5{V32=xnHzRtxQ>vU#=86Y_v}O;6xu(LytM zz$<44&y*^<2osJJ)Iw@J2IB}<-?h*hu7zk)ML0$j09`t0BLI|I2fY{x#B~tEgmlnm zL@H7T=_j?GeWl-kz)D$Wo9H1Igd9FX3MyQ42geFc&Eu9MdG1QCx>BG7uSNh1!7DkO z=@rl>ticq}%M_AIv3`(JZAAKvcen5w+Z& z;tjQ#+0v-heX+(~OA7xQV50c_7RK*;^YVKc3>L-j_(?7nz+wV`!67HOSu5G#cpm`E z7QpYvCo(wbDbCS-Uc zVV?FOe#=orX2=(uL&&`x{;aG!sI(6OGt(%_5?LoLFGQZ~_#RU~9+J3ZutvTOmC{-x z-=MO2t&vq1FbxzV*`p7>Sq_!*gO|k`@$!IoYA@i)Ju}MTDqn4+9W~}S|E)X_J~}gm zrcjKtm(;`wzK-Lr@Ai^NP7(7b!emk~beT+_0GLvnOixD|ag&K5LnhOw5wA#-NsA?& zsW01SO9#QEkh`&AzYbmsj4IyR{shbu(n z)VxH3_fiQSJ=x&F(VHV|kU0+uT?c%gulu|3r14ra-C3d858K2O*=x<+tdp+uLV4fJ zB<}gh(De+X^Xc$(Kq+`75GwlA(ClHXG>D9s>T}f+>>rps=z}~5L%)ZFrfqs-=*EE? zDj9+tKS-iIfzwqlYMNAxsA534B99yNMv8pHe^EQRawYFf`0?a#Iqk{BxetjBqDb^< z`q$hSw7p-}%@JtK!yzt z6QOri*Ijz|y-*`w?+yvs#LgPR=7rv6>>?fYH}E=I@9M6K({G$_!Gaxj$4R(Ty0?6D66Db7YEQ7B1lKv#~&V!ayOdN4^2?qpEZqWegJ{}}_zwGdJavztF zSNbax%4@PvOq>q#IIIMA84mOs52?CDqF6WP=B6mPKR&i?kw zEjXFWpTz5zV{E14x%r}%%xfx*X>HK;j+1os1iQ{L=4|z6$sMWEZsLPq{+45pWXhid z(e&|Zjv%)k^=GnfSjrr}OJzrC6yUk+%8XiLd}K4+cxNK|0SRn&BT69}ydRsfjHs=M zi-S2#ENNb%VO4?2Q)O&*eS#YZwbCX|pL60AV}5-aDy7gOw6*D@M$wIPqq3qziX3^L^33a^t5 z`Q4Y2ngLTD6%BKlm9zu4JP0`I*Ch&@EOR|6O@b%m;3xz)I>(4*Phhgk(GmPayC-tk z`f1&lv*H^}VGU;8D}YSv{tQhq#p`XB=?Law+TxN~q_8UuBXEF7XcDM(;Dpkmc$AcO{08ixY=HZ)A#NPSf*9pz_mei~ zqm~thQsz=OUQfR+6B+nA5|p$7UMu{I174VQnksO-jo zGw!2=K^)`otHro8v!J2)mZ=&r!R|SkZGT6YxRK^ZHl<-39C=CCLE-!pk1)Mp_i_|- z-o zFhwfHEpQTd3z&uTdImO`%@pw`UWzMrPh|E2+yG?KN4zlKTrm3#GmT6Z*jnvCDY+fwW7%OS1aV@;Y=>SU&>ktk- z-g3@mJ|s8J!kO}9W#A};Yn-_pz>rIc-LFmgj@~$2^gKRuyEOZA)|TTT_!9)O5WI!M zDrBrVbuA-{mTX6BmJ%u7q)4f3@g8F`@5zdC@NTTZR&5F(bJgaSsbI~;7mz$5$?{R? zE5&pZD>DunuFN#h2EH=;QWhGDmD%U9(g+uNF)K4|$@M6qmuSJoubfANoTdtfZK{6x zV3h5r6-+l zb2^KN4)P_X_4e5mt#a#43`ZeZ+#q8_N61Y%Msiti#m5TkZ4{r#dZVWSSZ`a%c7QDY z*%-s^SS(O$j>~d+Z8l1xnWUbut)9%aOMou=5H$yh+g#fx>xPp9G}jQ(E=z5-Mnz_% zX>a6AA}RVr31)WtOraQDhs_*4`)tMub&P#hhDs?E2H9tmDa)dL7DblE5dkoY_ZJ%ND!aifHB%|Vdc%5vYT^?bd;jrv7&34a~I@R4BmOsetG;h~FQd81i z>r-oTln1XuBnrVRI4(lUnsYhIeme@&mZ*G>qH^C%w|lc<8@vf?Fw;!|WSVZVq7_tk zqUIoR8*IB|-GLfx zj8tSQsT`aCzq%`dlB+7yEa`N*lQbkiSUPPgkfmX=5H?9dSOOtJLV|3Hh()F9byvMq zS2eX{!GnWGWSgg+!Gak9M~{xn(Lot;@Zg|0GU6x)!833i9dU%&j>p|WL7Bn%|Nn09 z-g@u8`(9TC%{hJZ>fP=C?|<)qzyH6>WAKbcg#jDA(Y5HglFamgO|j^ogT)*;z;>0D z>KMRw8Dw&pFbJ^ia7>GUEwUMU4A?dUJrRISRow;H#vz9@V8h}CU}K^rC~*?JP6ljC zBLExrGzHkiP6vmSvUmEoW96g%t(*p-9rZDIpn(=2ZqzzAtJdim(7HPtj%Z!?+PZixY%ED@0J=f!4!mT2271hcMF^*Is`RK)L zU=?K(eg7PGIoGS3u4uHgFKy2VmwOHoOa3v3JBT71x$Xn1rU9(ubGQh=t z=!$f$q$@qvQat%+4OTvSa@uED-rQp4D+Z072bmmZ6+&a1C~b)5 zA>2ts-Hx-Ps7~A*bZr)DuVxfD?GD~;uONDNFsne{9Xt!imB3Eyhu)#~TWiX)H<*zT zuNcF7gS(_MtTaHkSTIiVLO*m~?zHj}(}~`jLM$J)R}d{0R)H=ShfUj`vDTGEFpQ9l zf|(=KahF&Qg%}|fde(*Vbt@k+LU}BOP@c6{5G@o|fi9F;INJzL*bkkRf4A0@MI?-j zG$Kh(i}qxss_(Hqxei(*!k$!BciEFiLk?$q5{no1^cFQvCkW=R>ul{!#c}szb2pFZ4@;9>|DX$cgIZ_V{IK8TC)V9L$JR z1nEZPk_h0(Fdv~kx!{tRPRfOh%Tu9d2N)oXOHL>nmmxy?Zd`6n)1tz-yZ|%7C0(rl z1DF1N2di&S0{4Iv({G4hCXim-?hJ`S^%J$B&1ngnGj^q7mYbk&BRR&Ipcoq-dXVmz zsLdHfyX+vAG$4Po%y{LBng|WhYlueOk}w=dVmterDd```cJ{z%Jv#?nAh!_*aRR|u zLm;X4r7Ibw$_7F(Yfcv1yNMG!(|kL-i`d2yeB$7N??J-GAUJCfIx~`kBP19tCmLfA z%}Ro?4;c+>C!5RabHrhstT5J)RWj{LK*?!ADrOzYQu!>=Y-cZpR8|2HHH3o7Kpg~u z+beSg^eeQd7z8jRlYnG@Xf&doa4whc6OVCn!B|5s%R_z2LcS7?V+N3rSzEG%zDv~G z8Pz0YEmE9LxYD6iemVgsKu0g$c@Cv=FP@n9-Mx4McFM^(lNWE<1;UHB9G}RGM^An5 z;t`0-7n;RNCB~Px05uf%<*|fWJ$grCN9A=1Y?1jy!r9O?;W452KD*m)gUlOT*PktJ ziqoz=Zo}a?=E4iVk|UNt>m`!X^o~5mrN14EF&ue@H?Ehb5QQ*`q8xP`X9j@ z%4tB-egt3;9Vz8N+TwFihrX+>USyFNoSTF0+gJVl{6!mh74l*(b3=!-&? z*h$94Q=tfD2gwAxj@Tk#*OB-{u#2Ah2D^%QI^6|!QNpaS>kI-MtS$k&*t#iSLX}Hg zt8$I0*3|UO)>BtQ_nNjufU8qr?INaP#BL312gtYSG8wJO49=~#ihRy zi!p%NN$x_TSv!0Ox&i9cVxn0N@gT})}iORTtNL|u`yqo__W zwf!u#PO$o&7FhS$D~Mh+Vio9%Mh+`^w^-}SvTVc%iC3TDCDv}KNUhK%mBfYb4_R4> z=|lIXkjwq{3Zmu0D$wPUxbXe=)|#?Ng^`g_D#J=acZp?y;^;LZ4E9UcTF+Ryi4n}x zDFpL^y@F`LunG*pB(1Xk!dg=n!7wt?2qrl#8laKtzQ+dW+0Ysh2B@mK%K%*lIh+kp zEM6F(8?oWpD(eb-Of*23F#|M5(`b_Y>6TdbYJb^W>HU0QbilQ9P~KNUf``MDwvCyEP2Nu$W+SJ&iHR%D`{3v@Uj1!1Ccve85hk==I_ z^@=p@D@@ckVx}<;>0(UOKg3nodMakQCA*vC7-z|1Yh_1> zi4v=@9woJ}!uEUN=mSQh*$L-zxQhsnlLN*Ya&TM+lA|_o%m5?QtSwnWcM|n>M)|^# zi-Zo?wQr9Z?P}+Mi|SFUs4&(LRjLJVC9qW4Kx}5s$zpq?w_-bqCKh8&w&6|H_rS$D z-qduxQE2QZ;J`on<$pG`y+t5hzSkrY_x{vqTqk+`$SN<4HRQE4v?d!vzLJtdVvvnl zOR{YKE3Is-MT)i9T@I!4wOE`69i4(Z97^R*K{5NgI|T(el~Zyir{KD+!YOzPK9N(9 zp8Dn#q`MwnoPv}vt5a}2fjw3iZY4yYlXk{JI%Rrwk^DAjzwm(3$DrLWxK-xut6z|} zXry0|$A$fZ?3EnSgm2#>DNXMeR9yO3V=;zbaCIs zd7HNK7UMmrLM8{mPp-i-r473VXW)doEeo|bGYXuXgLm62i1u}`3JhOI(&>x8w$_xz z*TKk$SN&nvV7FAHZs_tsB{*!qWMw3#2R)oZCQsNah?WVfK$nSw!}hz@y0S=x5t30V zz4@ZSudGbPNa$xNBy`XlY|}tFT0*RX)Djx8)|Evm@xn)VH9T`4;RX2U>?5Qp)zwF6Pb}vye=~N2QU6?PZ!jDKecz(O z@dR_EZsMf%(nq*6ExY~>%)xwwiXh!bI35cdX+>FLMd>AU&miSRIO=Ucivw&BenL(< z`Uyj%_uWs}NYlo`PguuHV;ti3&QB;nTk6vqBBNM2ii3aeNyRL8dfZNOjPp-nY}h8~ zg`dzYGhP7;KjAGzqi&BF>OlAj-$hC7e!>Ihh5CZgdUg)DKprFx;sk=RhCoueljI-{ zQe}f-Yu21Bwg-q4JJZ~G=pwd$yLX>An$}J@m)3JuX<@7(Ek|#yl9OWwkdRqhvV^|Z zTM6~$^AK|{(%aBUEQ62<=4x1F&%JQd`*R)HoHTb~tRWWjBM>q})aN10{K*YHi|DbF zt_=lik;3VBlY{uU(+}q~N2g!ap;Yel6H~dn(@&sHIlpFd`t5|~WS@+@6rafHM^All z`Yk3zH63@83VozNoW7XOe3{AuQH$7HmigR8#pv96r?b29JefDRwvXp7u$c@W-XP~+ zU>x~d4n!Kc`y_$sy?2T^e=inec<+o2cp<->5J{I`PLGJ+viDvq)iE~v9gxWZV3S|& z7RR(`$woFqk1g37fSw3TR#n|)$zBIJoGn=_URbhBl(dm!7raKbZa;3Zayz?IpxE4_ z*|F1wS_uvo64-9QRrjJNI;WRC)&B=11lH|1JwiL`Bhdh`WY6|}&`~=*qS~ov=PkaK zmRJ8t%)yLVMUZaH#wbOY)+1tM^OK@@4oY_zJ;FTY#GrW^BBt-==`Yi?nlMj)hMC5= z4vR5QL*H=d|4ED47qfBa)X#kmoDYFa@dyoL4TLu1lNM%~R)|F>Eq$X(4o=>>!!&)!ESe3}2mtPlT`NsSo&Se7fP4D#cc1kC%sI zZ(d_hp&GNgdK^x;fVCoWL@%=jmHwcZNCF5?>=d8x1}y|jk>lkn#EW2KM6U7XDfx+d zV{f5RS_R)f+@pR>wX3blbhT0}wCat_VDM^V9O+Y#9SmM6^B(wSR2LW@5z%U5YL1N4 z=Yg_hB0XSK4d72=F$Q2<;U+sGHhctRSmiSjWmgn@J3N$HXYCzU)?!fQ4UoxUUL;hx zhth^td*_CFs7ZQv+@e5xrxSm77UJ)m3Gwr_5oMAe6}u=f-s&uTCJPInO3gyN&KGOI zi0UKzYL8L9541<1dR287s^1AYoKZa%FHk)jZ-iDagV)ihJ_K^kIKNOWwyTAfhgX9r zm5~!ON)EIv`^X#9wz;W#$&>fpWz8--g#SEJIqLr#Css_@y1tEo{Qes;2U}$NzU0)N z4d;2$DlpGW-K6)F@!*i@{T;aLsa=D~wO;FZ;p+XRvtVNX4b;fy`IqpC=6QOGn&&yG z04U09#H7Y%x$=@5VWwLOUu!-A{+7evO87hR+Dmax5e7c&_hUQblwlhh~H8T2~gqFha5lMos%&x@>^rgeK#^ z^tof!cH4{}BbtLC6JRJOJ+{zZL9}RC1-fVw^;phYQx?H6GSUbpIW3wxk+7o2rp{96 zrV*x&s=CY68H60prVbV_OdSS|$kZ9)gTuanvG{U8$YAA!E{DR4E7`ZIMR&C)lD*i! z0>uvIgjM?C*$2t~zw%h0Q7SdZPEHgwN6*$N3TYYjufrT{ok9_$uTz|CGdslLT)ZxH zRgQFu+^J$>X0ey;94$ca$a)emv`zpuHY1&+@a=a5Wjd8;vW|brTVcpZv zM4%0Lp&P}E^%{KWqgJU+l81njgl~dnGlPDnEzzv_P%5VRqR71@&$w+X7;7wwtO_fE zL_TEa3DV_*{$kc0ud11XfX|{5FIG>6*tB;SCADwb8xtlGKZe+Vqlfie6P)8-uEIVi zVO#@iSXc4O^WQMq)lM@P)z^sTI8kA&A*uy%_|PC)$s-~X8r>`t+K*cLsrR`iz@*orv<5)btFsW z+}=uM3xPG%eGC%dhb+Z1LcQIZZntukslud}8?Q{{N|lKT(C;<4Fdo#Px~!aglXZ>g zU|L>pw6K%VrmRB4SVL%M&4@E4(3zD3!ZvA37Pg;O*w!M&mfAZV^fBL3iwM|c|zvx*Fky3Z`UT<;cy&V^9jF_!s_t^EJq|gX9cx&; zaICSON1mjAg4a0n*&S;qTpsOM3*XEx{A*5W<$&_fK{>!q`8ds=q=ZJ2{O+Y~{%vn@%XgLz>~B4t8K?i#4iVJHa)8Yd4OXd&|Y?m(-j zX-DBet74`xu7Cds9cXu?Vwzhmw~##JEKH0w0B|}-nCv|19cX6V@hVw3(B4J77&C&d zB7_6&CQAC6InX|Dw5y$FE~*EJ<~UJdtRbohADNOzL?jGSvy|jUzn`eGGr$dd*67wE zg;jB*L#f=Vz+vlXRa6{GT^=pZ)4H5xFZWhegke?4?PgtW4{bO}SDRV_4Uw$*Im zPJMXj9oBAwjL5uwH9^E3ySQIaZRck829FDyAncVK&@?+Qm6WD8K@^vM4vR5Nkd3+t zl1gelvR7Vj2U3XStM&?_MZzjDL?YP(P!;vv^QWyfWf2J@Bcn*>gGkg=$d#&+ z3x27dqJ_z=Ug)#LkF0FPNa#OPNa!Vd16&Mmqx<_!}QroniMM8{>G!jZqi@u`B z>9xncq7$IqBYZ`w>Mmc=5ae+76=CthSHvI!`HJRpY1@58%eb$IU<*4(PU}9}wk*+e z-4e@Q?XSmf0AJ0r_J*iUS^e}D2|P!QbQ&kEm)@ca)3WPt!5qw6qzKZzMQ6GKb#Xl! zS2}WwR`vpE={SbN9tf zB+oea1;%Q&jX zifE1#6~-E(if|?=c|=4)qno89H~J@tDmw$*)8x|V2Xw2}zZh+1=Yosj8LJpD))0g1 z#w#To*Bl@)v%X}3eY>{;J78P24tb;AvQEN!)haBEHH4LF&y|u`s%#)Ov*u*6z0zB; z_4^jWRavAp4w(Ro#cZs@SVLNlmkKI5Ic5L}nYATLXgN`D@0KYv;YwjR;?6CyAaT2D zyQwyBL4r2F;K|sks%%WYS5>``Z9#DEDx*#9)N>7ZnN>^}YlvyoM!qB{uqFeMm?bBR zWM^8DSc??PAbTB#B3}kUOzF4`QgkSlFM|ju?Y;~mtU-xyGcAMM1I@{nLGHyTS_Yw~ zzAb~$?bR;JApDx^Y|9|bIAV3hIMi#EiZ}tI&e~<(di8l@jOfG8zV7jW%-h#>4{>jJ z)Vc?c3$J^yS8^ zwx7b=Z-;J`2I86TxnaGh6KyMEjXp1u6Z^S6XYz z;%8=L#G`=l;tU(E$!XEg9I4KF>}UQO4BH4lv#Pqw&-`h~;p}I|;)S1?sbq4Y-Vd)4 z?%Dm!N4=?0X?Zz*+yHQ_Hig8*N`>Ixj)ETbZ?($XaP{=_%Npd36*D9?!_AzuUV4)6 zPRp);7v^A|WJQqfNnW97H@1A_P`jB&dtH-DS<%e@F`&ai4+}3bCmX%QA+o!@#N+<0 zp${@bO>{^v2)zr2FD>A$w{ZU=`-USJli^o7nGdIFVBscy2s4dwRPsmYCVnOr)7<2H zngkkWnPaSB@*S`ZiDupL`dGM$pCn!kJQ2Mi@tc(NHFFccYP74JW-hAV5zTR;!dOF8 z5uRWuEP$Wdy6^S8^)pBWfFcR z+k>>*_~m*V+PUIlSq7QneOnl7h-Gy;rX-0$%8VdRv-V_hE+ua4jHeLiYkEZSGNbM7 zTyb&ku!<974ROxwXrYqs%*p{_o3tehds|vzTZI-0mFc1|GqFdNw-RX?iG1qjDHUn%{(rtijh5sl`!hx zmxeI}(Sn^}VDK*?vkln694oF8$F^`1npOeI4@XW?PVNa6aM9fpD(n>r;WBwb{}GsD zp3tx16L~`EsSlpe#e~=szudx8Wj$bew5Ub*tYvm_{s7t~tOE7%V|Q_WPv-5{_giFR zkzY~m>mHO5Y^cfKau^W;_^Bi?y=PM~=l=wYF+7|7IaGS+6*g(ac+-9l1Y z9eB+&Zuz(ReaPc#`U#d_&JLBDpO-<Q)3jE$F~ApPb;O%9_-_-{C2=)VaO z*6qJZhGk)2jj+Q)9vz*^ccf`S;l6o0W*XzFw`cB~^*C#yCE7JU5^pnOPOQ&{abn^f z0Ut=kIX7AEBB91P1~4`}UqiZe>6wuN%90tnk4Z(mau(j4JBceDT^OEVvFY!9l=L<8 z<~(M!u$^Wuv`2~NIH6&zA+!jGjS@;kBs98NN^+w=LR8rq;O0MTbZe2qA`Bc#wuN+BM*?y8xqMKYs8dnX4c zO}r;dV$&NIicx@T8Nr#Y$R?32`1|asU#v z{o!oKv_2 z&DXN=Nf&gWT!*nEKsYA@L3oIazJu^jrfDw$!as(Y#<-e_0pTIzb8!`+wb|Ff@CZRc%2N7mR|w)c*Fbv&X+x21A!6)NaU@o z(bx1Qy@tRBj;Uo=^gSeE)Gu;cgvQgySrjzt_mH9{x=uAw&rne#Evx?Zn1i7rMUak) z3{3~KX6TJPQR%KMIm?n={20t?-VnZ~$IO2$Hqnuvu|cjjF@ z3I$1(1}Ml>SD#8nDn~&dCn?0CAdEFo(1LC{rCfuENRXFViXBEE^7$xHWgUD1`CwZ+ zBA@p;l**Bh=wd<-*hxm@Bea;5dnV-bJdntc&kOiO$cLW#fP5CiVY5^Ac*Pj#Gs2+{ zPn8w>EETnYJ6#;N0R_IR-t;zISg1C=%$VpEXrXZD)Vqrv6TKw!_G>v>Hk$#824w_8 zMe?^CMWh{v-(+X*zVZjPucnHH&G$i9RLuE9Sd0#ff@9(&!4yu&nRFv(UYrwVBg*-QH%eAo{2Wt3W^M;qb}5s6V`+6f>vFxv@mFQ==QI|1%b%d7u4n1g|9MUW1z zN8_hbHi-g^v!;5#Uj&+rRv2~+5gQ*q33{C|@$<8VC2hM^LqJ7A|d%(~+hu2}#758_3KbB1~l>;KPF zQv3S<0lUuRs5k4aZ0CRrZUJP9KSY4BhTyVo6%JBmgE4N_oGi8x;>6B0-$CFawqrQ} z!VxDP*a+_PsD(*J7z%EkNXR^891ityb|7@cx?yO9BjNVAIf@7c@<3i zhu9|KV0N?KZWO&vc@^e8I2b9m8~8!5Jb%9bjPBbd|lS}j~Y?gR@;^JSz6dvyH-TiRk!O%E<~6)&+yPVd z_ITCKwnKv9a-k}J4g?FleHCb{7ObH+MWTUVpycgpPeOSkdkYQx#44m{c!eo=13t?_ z(L<&>w*@1W8hlSJ@70R+5~S(`L+#eY>N8h2EAYHnkIZ|GM!k_Q!wI!2IHN&uz+17^ z2@YeLD^$t=mC^~uj2NQ`|3T=J!3Zl?(I9d!)+3C}!Qse56B?=vO;iP*I>G#Ep*Gou zg>-1~68t{It961S8eXy9DCJAuH1rRsra~<}8$6K{z$UavrIep;coUU<;0Fpu3hh=s zuU-X*RG?GMF99yf%Z>*POD|w?&1=-a(*wmEMsJ{XYfTitdNtkj+NFAa-xPMK!@Yge z;2z3{b5buUw(@%_%?j(HhviF^W?@$q8R1k8iUCnx70d%>nlJ6^61uT zc3^le!Y8Pt3CjtP24sV!l|bZk8-=~{Cv?#TII5uM)Fz=LRx4BRRo6=nUm1){d((Mn z6#Teeks53szS&g(+Hr6MxpJ#D-Q2Kt?cTk6*G%tkmFu-N^~U7dlDB6qq-?J31V>>G zA_Q{eTiz6isRd%01MgQaKlwCh*OA6V(WnOBsA^V-YGBY7uJ3T;!F1IzXvCt zJ0NHVL-@J{A9Tb-6tSaOUV0;ZxfbK$I}m62zdih*gY<3vy_1h2YwS7E=`Yi*nKPn*JJP$2G7vbaq#pk27fUM!3QzeycmKvVDNDaK8?Z4 zV-UQG!B<`f!Q&Y0UIM}O7(9i+GZ>t@41)C-{04*n#o%YlA$Spkzg+>r7cgk8gy04Y z&K`$gJqCFUN*KKEBnVExU}7}{yD|9nS_poJ!I#%T@E8VLPJ!TJ49-0bf{hpyFqpug zeI^8N#~`;Jf)yAXv;l$v40dC1JqC+5La-Qvr!aUHgAbeqK?j2`V(=&ipWFn&{TN() z4g@uxtwiCt|R9D+F6Hxb7kdJPdYT48fHc z3~qy99tQYHHu_qej~8y!UEn@mi9)xv_;|L4j{W$!t&esb`nZBki|amm)5y`+4(5lo zB8}Gh#R}+F9PTiJ*&Cb$MLMPy!f#kQP!^y?r*_q=QqfYFmZv_ z!fke}LOD~1UyqlE0JQCI!ek|0D8C`tL@$@;XF7#Azd)QVdBBTEL5{Fr(Lw;!kviK7|8YqK()X` zhd_GU!KgHIRHVNY{xj5WH=SbNklWlB?SK*C!OV literal 341841 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

i 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%0D+@rpW-q>7(y$yUW(!_R}N+;W%bLU({Rp6eB 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 diff --git a/doc/build/doctrees/pages/normative_modelling_walkthrough.doctree b/doc/build/doctrees/pages/normative_modelling_walkthrough.doctree index d55dba04f1fcb2e19dc091e65b47c2a75f43ee94..a0d667b2da1a5ada49e310bc59aa2d4907818385 100644 GIT binary patch delta 5469 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 Xs`|B>zORRVlB(bPDkhyiMP~mGB*{mA delta 5123 zcmb_gdr(x@8Tag)ry#E=uLTT6-iTDRB!Y@0l2t(siV9iH%H;-jkzG(wP$ouQT~vBj zZ`(;mf{$n_vAC(Eqxi^JwRRG1$7oWuR-4TDNTW7QMl_m!-??|!3q`^o<&VSechB$l zy}xtrUfr(U@=&8*quyo?Xj6aDc4KpNS=wBysn(iiHC9%bYU^yAsdlNQ_Vu)>ChHqE zOAXyb)+@ugG+H!k3_Y*(qJ@fDdR|cjvI1J9O!0MiQAVNMB7< z3a-bM=;Nyd;ivAHsKWr*zEcoCh@_DZ{m7 z*f1`fSHeHADU>b>P4Ioo+2U++!d?&y_c}>B8AcC>rjqqEB`kJYow?F%wHd3b^eGMc zIh@I8wxrBzFig?w@p!bdOjwU zel;>0B9em<(Mb`>rn#fO4G!nC5!SbyQX*Pl_YBxQCfK1e8>>xDho%4=u3L;nP)u_p ziaqtjXVap{78Mdki0pUKR!(!i&nM{zs%fsK4>+R=)Z&Ks&`&`~x$%+seXpC5gYR6XElC+gRp3sgA zC;Kp*>cenlVxppt+Kfq~nc7QoQ1BOW5PpWzcO9KPNhvvdUiPvb~~#3Bf1Qf7E8FHvp>- zJvws^nMi|XjRUS1MXnr#K}}N9kfh#%yIihWn_yV*3mLEtMDyTbtQY^PxOFz|EG`W% z62J7*+Qw=)cJ?(5db$$!xLYrB$Q9&7Baod5N;c(qGwq}+ei-4@D5@FuG_(y#EWp=&oJ<8``!4BY&??P_=Dj?)%?4+xEt-q%O@V)aPLQPS(1lcv-IoL`R13gp!A z4fwec7Ka)pcCF1>s6ddEM7Lte@9$X>JDM)DBtEZ{C2`1u=m!>tyKDbKu0tB8ODWK= ziv$Ozfdk22yfj`X(}s0P5S7VFcESWq0)66`#X2^GxHRi4Fydu^%F=`pAf80;|2~+W zvM&R16%$8z#ICIy{V*GiOdjQy=k!g*Mw;x%gk26a#haI!U;_&$ZMXZ5j$uH_6o|)w z3Ayc9kE-^dDs3DhI{qgiJ1`-6&rS$G8k7`jWZCE!fH=a^Vc7B+I~R`2(=ox*2ZNsr zhWLK&#Jbvl`DP?G)nAd1HC4EW?CCTzj2D4oSGD|(Cm}TNVl?&X%4aFf0Qc`=3_a<* zLyvY%g>>GL(s@&gp;%i6r>p5;ni8ICRrGporq-|>72Je`<_z!CEMY2|;us4XN2+k< zc}Xb>fp7XKmZEnUQlfuWih2(f5g^HA+H{5;h)H7{ezLAfR?!cN$4usbDor|Bn)*4m zLrhcrAN;f!R)b2`HA7BQM?aThU0uhw1tVWM@;y6EJt9y{)2`3nCQe9`yX)F7;^14w z;^;m*R?yyyi6C8}61%Pr_+?%9h_1bK3?4hBEImg4I=bQWc(U7d;PZb^(Y5X$#{L9S zN20sWM3BQ%q7ODXxexgd(NqW&US;`R$jk0p2BpyP8mR`-kC-Kahh&cwxK$`Il0?4_ z6h|49D5~q;Y$VGufuAr#Mi|sgj9aABp?mHUaykG*bQz?c(`Lva_v%Z??G}zNCuPC| z8(EtO{y0dYlYfBBZjbs!3I^$7H%L`a)+Pane+U3xqkcAOrVJIhT#gDF1U1`(y75$v zeh@&32T&#?HOSSZ41!u9LuEJ0Q4Alz>xbA;km z8eFHA+my+)1j)WG{4~#yiby2tA7RB_-(LokGY4u4zH(bT-tGY2bUZ;+4%{$e1*#mTxxNy zT5wqawZ_lGr3058xLm^J0xqe@o`lO*Tv}nNb9mwVxvinbWOX*Jb|&y>f}MM8XWH#d zvz=+RGmUnp&CWF0nHD?KU}x-h#%!SJ`uF1$t7sB=e5w_sZf%! zg7HrzT_`w1qQmuC)@cLW@MDCQGvqzM<+*V$A})jcLb!64ghxoofo`J=`mzvomXra0 Vp&LJ6jt`UY{jW)a|DVas{{o4z>;?b; diff --git a/doc/build/doctrees/pages/other_predictive_models.doctree b/doc/build/doctrees/pages/other_predictive_models.doctree index 585c33a15b191f27b6a2dcb5f23f164c835afd83..9c1c18c912ad1608ce199d517a853d7832084e67 100644 GIT binary patch delta 7777 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 Mslj(#0`tND04o6-KL7v# delta 6851 zcma)BeNa@_75B2cutdP3fLK1Ih=Rz6Mgh@AW6&@P8We(H5h7b2Pd+v*OVE}8X_~qX z0qjLzjHD8+#Z;vex6PW?i8zwdwNsMxp7ZuSF1tJH{pZds zdw##)`JH?2xo;nnAD9lkYch424qOV@Z@TjX(wk70S!%Pi*fMQ3^>vmOd#hw=`DSxV zZRSH3+wRt8tMg^Dme4hZBZe3$k4}uG&=BK&^YZP|Vkt+8kuvR%Nl`r{W8k|3-2)v1 zi|BIWshAk4M2eLRQe3eVU(5n>Q}H{=)U$#D8bkB!@m}{+o$?+iz1|iDMss6r3H|t43~| z#r_@w@=Umo{xdio0*_;0Zt6?YQR!HZx!$^`RcdZpHUKgjFk%|8`@UKLx&(mHJn-0- znFN5pQGt;@73pxuAOwA2qc;t4R1TeGW^s_rpJ{jK9Mb1J5ZaD3p9ttL;`N>FvmP`7 z_m_SXh=|;3h>;`cuQ%U;H+@gUxo+=ckdV)#x^d~M1ObahP(|Fj35W~a78QEvepHa3CV*gh^8`idpo zMhE}5kv@>tminEJfgX}JAkPe7NcVKmt+~7K09w47X2lf4AQI+7uHO{(lc13=mgWgg6>YwkWyYN>((!)# z>onl$!%IIg#>jF2wa&|msAEX(+&hu>>1C@-kXE}+>$iG8XQ3Jo;`U=B7p*J) zYhxKIzAvVDhvNW6`q?8XVDaCYVB*vvDk6l7))g0Qu0q90U2%z@VN_(~NqEUHgS)D; zIms|Jn*KC;F1422U;}I+t=XC#naqNx`m1O*!Ez06U5^d29H{Vk`b{MQ$TcP;*txw5 z8-IhA3vWM}57+GnSH6y#rDB?kc@osjt2v9Bb(&^4=RY6AX~wQw%z77E?_hD9^>wOs zYwfGyuyMU&X%qmj#)N4+-&Txi42cnRdJ&YjCj~+VXEYyt?VjPo-L+E(a7U+^&g*Pi zrcbxG0IyA9Ek%Gx@kKA7S?!UJR(B22)lUS`ny%xZd`T?z!+b92y}esa{V%>!Kgx$s zOZ~-tW}3IJX|A67-D#=Q*Y{m#6VYUZi5MARoVIOe)oq(_9K@>6Vc!aL>@Oe}jR{k~ z^XwoJEYJy-`#XK-Jqfg0I?wl?3UI#Ge-_Vjj?U)y0F8OFImc-m5)_INyyHdi*1>Z0 zQwanxWBD$+#)s=Cu=)8|gw%^^{+%bm8h(Cg5fXe$jNoH00_E^hbp4zd!KYpX`9EBV z1V_XO{^Lb(V<;I9-z#DSlU@WJ!yjS|E&_pP-Dd`HZKv1$^vmxem#lY|)rnS8iT7PgC{}P*GE6X zypCAv^6W*I)B6w>`*plk#W? zu}|F*Z%#{C84n?!vt9nZy31GCm)tI2MFXy^F!B>D(seNh*B}**3LLmLg_99vxGl!; zkrzWl1c^fPra&J@NU4t}P0RI7&jLPe z(>0^+O%KACGg@DkS1S+1k`>D46k=9-6G*&rD3&eo(ZxP)!}OMlrDrxwFRPl~P!h3E zwc}<#na#z_Tx-5Y+OQuV5tEJfknM9VT0qoa>bt}k61*6q7LpX)O}iLFiYJ3IvV>fN zG59?ofct`xq04feguF43B#;*jKyzZ-p2` znfRV^c%~%Bz_Oja^_0tCIRVRYSZdiD8My|Q`Rs?O91qK0Se}68OIRjh zS;-y>IWVTRX^~&p!$Ie1pUZWt{*p&GV*_K+kz+n diff --git a/doc/build/doctrees/pages/pcntoolkit_background.doctree b/doc/build/doctrees/pages/pcntoolkit_background.doctree index 9274487d75a5f58508db816792eade0f6ed77be8..7b603c81861525a0262317c59f1ee0fcc88a3254 100644 GIT binary patch delta 6681 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 diff --git a/doc/build/doctrees/pages/post_hoc_analysis.doctree b/doc/build/doctrees/pages/post_hoc_analysis.doctree index 7b43d5f1a387bce413f9734551cfc724fb4a5aad..94eecb2b36fc3c06d1b23e19c20a7970c15b92e7 100644 GIT binary patch delta 3697 zcma)(6w$LP&T$zhB$~k+~zJjyV<(gwR?~a zSPEyA;atqo4!M*tRKSH&*ZBFG0AFXV?X-qr@@8zt3E1Mr*q4oow9+V@Fx*B8l1I== zGLc*df!~IaLm*qC!+m1!>>&hWAH!HbjOCMRy#e)+CjI3UNGv^^NG6V3`b;LWKh=>r zj4gOE&MwOWK_3kbaZicgr(V}d^Dn^gcjP6r*@DmF5$Zn-lP!_SR4fT@%}GMcU{p3R z=Ya&D3(an5=?vgz-?Ff*mkk$17*tQH)BH?6rbTXxmWwMN_9*suru7ar`zNCHk=H}4 z={7Pyz0fdSD#3te&C-&_8BHT!rUw`yTO;AA7{cL zn!svuC99lV%-YImn-!Ys3oebOiSN$ch!|p*Mnpw4f^3>s#0VW42~{zKsVKth8VS$G z5RT1vvkdQRBPh8<5zt_h@GT-YTuU>~(4Mr_NUOMB zT8sncI|i2W7sX?-nnwdbBQ&7wCdT}MtlsW$2<3-`>i~)DKEfIDbAB)EDOCj@K%V<@ zp1X3Msn|S6q&&ro^$)Q;dL3I*s(Fq=o(OF^&nhzMY?up~gqvU^JPsrlYKjC4k3y(S z*QlNoir^>EyUBHz!|bFTrAEKfOzi94;Wm>k?pzjcgT{EHfRBJFkc_*B7-YKwS*}*| z%W)8v>Y1V9g3%LB`-6|J&ly48I;>4%7$c2 zV2g8sS{A)T5&KFr^H_p~vNoIi=r=9|F&y?n4vfhcj0)onxkKg)*yV{e!wj(9$Bg(l*E_do)t6&Y(=TV@7#PBjwL{ig=;pijHAE zR4`Afz4x(rOk}l@gIzxIM%Qje{8S@x;(^4I-F7n2?PA1p3Q@J}lkr4Sx#v|zxu}sc zHG|^Ws~r9RXr$bWr3eee8+*T24x76&=0Qs)Q5XLKB@dLaCov>y`wYLCLHS}pUNTl7 zfI~I~!IDXjUNS$(Dq;-GC62*<_z&{iU|9mfV$O-rk=2R=ftJ=nyS=@=-6r4eZO#6A zdmZ0qZ{b^7#ak!c$Y|i3YWcbs8k%bA`PT4j#54TW-qm0x6wzh{>{!Aw+T5n6`G2&r z(QW~4oYEB9Y=t5T&(X#~yEbTop%hpulvpiBh2+~CT62GR}Lq*5#}>?f&5{Fpb${`J9R z%2%WU28`WI9%r(IjELz^O%QfD$}X)iZ%QL0uxOgov-opjnpg{$&w{crdwo`W(1Jt zsG>D8+MIa+X^|>gFQc2YK0@kHMW2^Z$I1z$PF1uio{AmWPJ*^em2Hi;kd@;=8c;>s zWOOD<-%&+7Wwg&+ja5&mqOZwlcCH=im_%1At=}%B@St`L%;oOZnT1OPrs_6iTx2-9 zrTJd!;oIVvWibsiUzIW!DP{JsNGT2D1MrEx4mK8gvfdhwgo!mG{Elg28(z2w=L_Fu z``KP}igj@>+Qovri<7(yYq4o5c45%O*1TdDqbyQN6YJv4O4fn!C(A9Y#T-%#3*fFA zWi9BdXSHUb5+>_PGs;;&+`GJ&E@kal~BBd-` zKdk3^RF$4j)G1rru=bQHeK$di-)^1OApcV0ijQa^fj@dgd$;$({BR2NZGQu^{U~Kq zyb%u*vVpU)5B0B-E>>7Bts0WZH#8kZeqR+gB=P)a{x(R}#&*jZrRpZm#jl$)3$W&@ z7|Mkg#?sd^$hL}LVLwKX*w$r9)hYuG2yId|#kpQh;&%e;k=Mt#d3oG9-8##|VOMaR zlmi*K!rl{|T6|lPb|8Z*QXg%ta3i>eGU4sPO+W|RF(Fme&revO9j6!jYJk!=$B^$+ z#cw3>)**^Ktcu@E$(85syw)ldPql zy{;bj?%sY^x2LZs64v#4`u0TNyYrWxt6_BnI}T;+1tHjQ_y>=+zj3hQdtjXe~S%1S|g5E;^^%$o~ubK^Oo4 diff --git a/doc/build/doctrees/pages/visualizations.doctree b/doc/build/doctrees/pages/visualizations.doctree index 98ffb79cc060c8279c66188281b65b842c1503a3..be923a4ab7d10a3ee499c86519bbdae185f23a30 100644 GIT binary patch delta 6713 zcmchcdsGzH9ml=EuqG$d-yYJ1w8Mq@cW#@fVKUon#Ah^?Bmwqn1(ft{IMgzWqy=X`$4 z%-s9Ezx%uQ{_Z>u{_m9Wz{{tN+*aqk>Ld6EFrLxNU{q<|h zt+ket^0EyjRhH`2_0A(KkNI=u5 z&RJ?pg;+Z?J9ap>Ikq^`1&$8HbN-#loIT{3Dr-rNwJfQ0W0C`fmkB1pUx+Uf5{gi5 zy0|BxbPQ*Y{6@*zHP)o1wKamZYIQ|b85Lb6w-w7R;HZ^l;$lO(Xy@KwoR}CmHVUWe zytI}CE)3!9{w0-_RQ)BY9%c_HFWF#q2n|BHJw+@DObK?l{jL-J46~sYXqXy6YSR0; zJj)jA4Pk7$bBAFg3pr9vWnaa`Lt;m8s+eVbmF0 z%H^>x@x$Om#)+Zf*)ey8cp+0rSQtm;sJjT-J#lIH&Cr4JwqQ15#6T6(lt3KV3}k{}>_KZQ zH`WLhNe^Jvnwmq*gqAD070MfEjJuh0_BQtJuSV#)74a18o`yd<*j>Rm;}sES%|59qDvC zk~Neg*VW3xJ@@E~%_~h}x5*&xU74&nc2eV5jv_bAj4!NYs<^FDyx5-LJhXmRgxH;F z5NCEq;b2~b60>t|SJM=2&zKd~YviyRXKYqS?Xqs)0{pbTIr}!BP@{Io?TUFQS3)$k zHDR08o+$phAV{3kK3)mi2tPGZ_!hQSjW`DMV*5L)Yf(^>XV#EWk~FmurQQ>Z6jxGo zyOQbe5v5}%yHrMa1`e?4=%3R&9obnW_yY!YKwp_Q^A3eo7KE9%e3A)A3 zdWzk<+{e*oTrBooE)K<(Xxl`QqFwjl-`kb>zMW2r9akny*+KvHZ|d%%vG#x@vst^f z_a6_4VNW)t+O(5XxGN+SexNu)Sw`Nn%vTpjz!{r_Hc z{PWdVxhF@+)&8F3ihHjKN^jnv3vzGXr2htbGf;e(-cX4ldUJKKL+;*mqZWE|jw+)! znS(=db5La^x1GAtBp$!frVRL{eihpMO};JA9z|S^@w=atD`K~Gi}iYn{qp8-6tNNh zdQ-<;PqFtuRri%BjhIE5I>V*!e=A~AL^jV7#LipA!JI7vLJ0muvF}!POe!77x0ZCj zRQOQ7QF6}hDNyelrfM3V=2OFu(6G&|Od)oD@mZQG_?&LR0NiML2>x{c=c?Wc-FilE zy_PF`T64r>y}@F9?_u%U?aRs-uG1|ZK1e*giNx=OiN*gs)Y8+`nmusPp|r9^cPla8 ztvFZzYpHsF)~s952)KLYhK@F${b)X*n5NrgSzzbkA*aBVzFM+LK z8+aLP2R{Nkz)sKrc7aB)8|(pg@Cs-G&0sIs2lfLIIKTmLu`yn=drnzHmTH??4H=@es#J^#Fg0xb4cbU#H&57T+JO8OFZt)l<2M&CD&mwD?;7czL(ttj^DR$Sq& z=rT>_8Ly^Z#;IZTEePN9mQ;c;Ba3J2q^Yp=ip$X&ecysm=B@8aG4uEmph!;8Eh%_O zN>aClXNO1O&AD*KHV1m{SLe1iX&cY8o)FHq1g9}2i#hSpEuJlMNqFchV~mrIP2pK# z3@)vr67oHV`$K{>I)`V!8_C&&$}4KDr4<$H=!M5>l1Psn_y}Mtr)uT)W+@Yen^WCi z0%SSdV|aeV5DKXw={7FBc|1G8l9Gg)n#$^Yi>1E4-rWD1-CR+% z#!_ajvs79ut6heCo)xf&Q0sorwmoE8Ftcn9G8b`6 zWBY(iGacH=ETAIu$m}GuU0Phgvr#LldXZeUNUl~SS1FQ<7s*A7YK3Hci;>$9Lgfa8 d^Zn}dK-;fQ_gDSuwES(`sfk6dQ)ps2`#+;imL&iH delta 6307 zcmchbdsI_L9>*O5;cZY6&_JxZBF}IYA1EUzD%B!B*tKU{4_Y{;UZRpTfm&--lzI`3 zuTC_vt>##}de&}tyCzk7dkGh0_53g33GDLkYmWUKmzEg^SU5ZO4OIBT|1V=!hJ%PJOV4AoV<#_+t( zFh6UO#<;Lbr?ChmD{SsUbeV?_RAH|VO_Etzi7&?8@G>DPVK@nzm90s;0bON+9uz_KDB**?-*ixeSgAu>B z!bn^BBtFA3u--a=g+|!OXtp;ZlWb=vBh2x8mYYo^&CGx7X>9LoY>Vtm{>t8uTnpC6 zVdlw}@%7fwAimjdj7lN!;d%Fm!Ols@ixObCW_Bw2bO^k;VPvzejAplc4(r>FuDq&9 ziQnTKVPQJhSVe0Xuha&U9zC#W&hQ@aPD@aNKulp3`lVI8t}S!!3qSFTH9hR?X$$CY4LvwPf; zo^Fr2B+hhtXltP>Td7joYw~u9VeAPgj7yysaAmG6orYoTcZZQ3NEpR>(rKctWsy5o z_5+K@D_F(5{oqZZ0xgetN0aQa^<9+WC0QSIQF3{4vOn>EuaV@ zy*fh#6LyGsxhpLKtCxwk)*iXDQH&re$a%wy7lyjKHL!Vg<~vfw3d@JES&fOTXa+UBEh5zO>o;9*)oD1xe(84TC(v~;o9Y($t$jL6653s&GXfU z3bS?veB|1Wv{fwTa%_ah3U|4F8Cvf&^XwuvPrOy=7C(*ug?XVHXhy!L`zsZD~u?q`s&zWw1p=kpUj zYZTuXA2RGSu2X`+rn|=(70@^jVH~a75m(VJZ%-DD#z`{@4)!zJy|+X3nIX+5+~3Ed z-e}EcnK$>cd*7YI8J0;i?AgVzY%>^s`F%92y!8g-Zk3BrpmX4dei0fU!U>K!I^U9*_@=2POaoz(imYFc~NWo&=@< zQ-P-%&Ydn=e6p_}*zkm7T8w!0?v<9# zdGLe4>vnp4>4cO%03NP)l#)#VN)U<+B^V`yI2@&NGJBaobpq83^aX($1Zos$l|ZWn z`l3LW2=pa^E*0p@0)=L;2z0qXR|s^aKx+iLN}#I+S}V|31-eF{flg|0$nH2 z^#U~sbb~-Q3Ure|Hw$!&KwlFm6R26BTLrpJpqBmA+E^4{7v-OGqlwK&kQTm|7qVz?HLy1R8KKZEYkaM{M$N&C=ze_zJ|j)OC3v{Fj5 z;{B|)+ZGS!$c|Vz_T@j-=Aaq=Xoo{$?Pi-Qn!Yj)bYpRy&HAXz$*P>Uw=u - - + - - - - + + bayesreg — Predictive Clinical Neuroscience Toolkit 0.20 documentation + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - +

- - - - - - - - - + \ No newline at end of file diff --git a/doc/build/html/_modules/fileio.html b/doc/build/html/_modules/fileio.html index a5ba487f..fbb9028f 100644 --- a/doc/build/html/_modules/fileio.html +++ b/doc/build/html/_modules/fileio.html @@ -1,75 +1,38 @@ - - - - + - - - - + + fileio — Predictive Clinical Neuroscience Toolkit 0.20 documentation + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - +
- -
- - -
- - - - - - - - - + \ No newline at end of file diff --git a/doc/build/html/_modules/gp.html b/doc/build/html/_modules/gp.html index 6c610975..af251eee 100644 --- a/doc/build/html/_modules/gp.html +++ b/doc/build/html/_modules/gp.html @@ -1,75 +1,38 @@ - - - - + - - - - + + gp — Predictive Clinical Neuroscience Toolkit 0.20 documentation + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - +
- -
- - -
- - - - - - - - - + \ No newline at end of file diff --git a/doc/build/html/_modules/index.html b/doc/build/html/_modules/index.html index 7122023a..cf9a339b 100644 --- a/doc/build/html/_modules/index.html +++ b/doc/build/html/_modules/index.html @@ -1,75 +1,38 @@ - - - - + - - - - + + Overview: module code — Predictive Clinical Neuroscience Toolkit 0.20 documentation + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - +
- -
- - -
- - - - - - - - - + \ No newline at end of file diff --git a/doc/build/html/_modules/rfa.html b/doc/build/html/_modules/rfa.html index 971533a2..d221d929 100644 --- a/doc/build/html/_modules/rfa.html +++ b/doc/build/html/_modules/rfa.html @@ -1,75 +1,38 @@ - - - - + - - - - + + rfa — Predictive Clinical Neuroscience Toolkit 0.20 documentation + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - +
- -
- - -
- - - - - - - - - + \ No newline at end of file diff --git a/doc/build/html/_modules/trendsurf.html b/doc/build/html/_modules/trendsurf.html index 8dc6c789..4bb733f2 100644 --- a/doc/build/html/_modules/trendsurf.html +++ b/doc/build/html/_modules/trendsurf.html @@ -1,75 +1,38 @@ - - - - + - - - - + + trendsurf — Predictive Clinical Neuroscience Toolkit 0.20 documentation + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - +
- -
- - -
- - - - - - - - - + \ No newline at end of file diff --git a/doc/build/html/_static/basic.css b/doc/build/html/_static/basic.css index 4e9a9f1f..bf18350b 100644 --- a/doc/build/html/_static/basic.css +++ b/doc/build/html/_static/basic.css @@ -222,7 +222,7 @@ table.modindextable td { /* -- general body styles --------------------------------------------------- */ div.body { - min-width: 360px; + min-width: 450px; max-width: 800px; } @@ -237,6 +237,16 @@ a.headerlink { visibility: hidden; } +a.brackets:before, +span.brackets > a:before{ + content: "["; +} + +a.brackets:after, +span.brackets > a:after { + content: "]"; +} + h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, @@ -324,15 +334,13 @@ aside.sidebar { p.sidebar-title { font-weight: bold; } -nav.contents, -aside.topic, + div.admonition, div.topic, blockquote { clear: left; } /* -- topics ---------------------------------------------------------------- */ -nav.contents, -aside.topic, + div.topic { border: 1px solid #ccc; padding: 7px; @@ -371,8 +379,6 @@ div.body p.centered { div.sidebar > :last-child, aside.sidebar > :last-child, -nav.contents > :last-child, -aside.topic > :last-child, div.topic > :last-child, div.admonition > :last-child { margin-bottom: 0; @@ -380,8 +386,6 @@ div.admonition > :last-child { div.sidebar::after, aside.sidebar::after, -nav.contents::after, -aside.topic::after, div.topic::after, div.admonition::after, blockquote::after { @@ -424,6 +428,10 @@ table.docutils td, table.docutils th { border-bottom: 1px solid #aaa; } +table.footnote td, table.footnote th { + border: 0 !important; +} + th { text-align: left; padding-right: 5px; @@ -606,26 +614,20 @@ ol.simple p, ul.simple p { margin-bottom: 0; } -aside.footnote > span, -div.citation > span { + +dl.footnote > dt, +dl.citation > dt { float: left; + margin-right: 0.5em; } -aside.footnote > span:last-of-type, -div.citation > span:last-of-type { - padding-right: 0.5em; -} -aside.footnote > p { - margin-left: 2em; -} -div.citation > p { - margin-left: 4em; -} -aside.footnote > p:last-of-type, -div.citation > p:last-of-type { + +dl.footnote > dd, +dl.citation > dd { margin-bottom: 0em; } -aside.footnote > p:last-of-type:after, -div.citation > p:last-of-type:after { + +dl.footnote > dd:after, +dl.citation > dd:after { content: ""; clear: both; } @@ -642,6 +644,10 @@ dl.field-list > dt { padding-right: 5px; } +dl.field-list > dt:after { + content: ":"; +} + dl.field-list > dd { padding-left: 0.5em; margin-top: 0em; diff --git a/doc/build/html/_static/css/badge_only.css b/doc/build/html/_static/css/badge_only.css index 3c33cef5..e380325b 100644 --- a/doc/build/html/_static/css/badge_only.css +++ b/doc/build/html/_static/css/badge_only.css @@ -1 +1 @@ -.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../fonts/fontawesome-webfont.eot");src:url("../fonts/fontawesome-webfont.eot?#iefix") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff") format("woff"),url("../fonts/fontawesome-webfont.ttf") format("truetype"),url("../fonts/fontawesome-webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:""}.icon-book:before{content:""}.fa-caret-down:before{content:""}.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.icon-caret-up:before{content:""}.fa-caret-left:before{content:""}.icon-caret-left:before{content:""}.fa-caret-right:before{content:""}.icon-caret-right:before{content:""}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} +.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} \ No newline at end of file diff --git a/doc/build/html/_static/css/theme.css b/doc/build/html/_static/css/theme.css index aed8cef0..0d9ae7e1 100644 --- a/doc/build/html/_static/css/theme.css +++ b/doc/build/html/_static/css/theme.css @@ -1,6 +1,4 @@ -/* sphinx_rtd_theme version 0.4.3 | MIT license */ -/* Built 20190212 16:02 */ -*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}[hidden]{display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:hover,a:active{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;color:#000;text-decoration:none}mark{background:#ff0;color:#000;font-style:italic;font-weight:bold}pre,code,.rst-content tt,.rst-content code,kbd,samp{font-family:monospace,serif;_font-family:"courier new",monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:before,q:after{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}ul,ol,dl{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:0;margin:0;padding:0}label{cursor:pointer}legend{border:0;*margin-left:-7px;padding:0;white-space:normal}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*width:13px;*height:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top;resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none !important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{html,body,section{background:none !important}*{box-shadow:none !important;text-shadow:none !important;filter:none !important;-ms-filter:none !important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:.5cm}p,h2,.rst-content .toctree-wrapper p.caption,h3{orphans:3;widows:3}h2,.rst-content .toctree-wrapper p.caption,h3{page-break-after:avoid}}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content .code-block-caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo,.rst-content .admonition,.btn,input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"],select,textarea,.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a,.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a,.wy-nav-top a{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}/*! +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,.wy-nav-top a,.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url("../fonts/fontawesome-webfont.eot?v=4.7.0");src:url("../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"),url("../fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"),url("../fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"),url("../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular") format("svg");font-weight:normal;font-style:normal}.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content .code-block-caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857em;text-align:center}.fa-ul{padding-left:0;margin-left:2.1428571429em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.1428571429em;width:2.1428571429em;top:.1428571429em;text-align:center}.fa-li.fa-lg{left:-1.8571428571em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.wy-menu-vertical li span.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-left.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-left.toctree-expand,.rst-content .fa-pull-left.admonition-title,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content dl dt .fa-pull-left.headerlink,.rst-content p.caption .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.rst-content code.download span.fa-pull-left:first-child,.fa-pull-left.icon{margin-right:.3em}.fa.fa-pull-right,.wy-menu-vertical li span.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-right.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-right.toctree-expand,.rst-content .fa-pull-right.admonition-title,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content dl dt .fa-pull-right.headerlink,.rst-content p.caption .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.rst-content code.download span.fa-pull-right:first-child,.fa-pull-right.icon{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.wy-menu-vertical li span.pull-left.toctree-expand,.wy-menu-vertical li.on a span.pull-left.toctree-expand,.wy-menu-vertical li.current>a span.pull-left.toctree-expand,.rst-content .pull-left.admonition-title,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content dl dt .pull-left.headerlink,.rst-content p.caption .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content .code-block-caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.rst-content code.download span.pull-left:first-child,.pull-left.icon{margin-right:.3em}.fa.pull-right,.wy-menu-vertical li span.pull-right.toctree-expand,.wy-menu-vertical li.on a span.pull-right.toctree-expand,.wy-menu-vertical li.current>a span.pull-right.toctree-expand,.rst-content .pull-right.admonition-title,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content dl dt .pull-right.headerlink,.rst-content p.caption .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content .code-block-caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.rst-content code.download span.pull-right:first-child,.pull-right.icon{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-remove:before,.fa-close:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-gear:before,.fa-cog:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-rotate-right:before,.fa-repeat:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.rst-content .admonition-title:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-warning:before,.fa-exclamation-triangle:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-gears:before,.fa-cogs:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-save:before,.fa-floppy-o:before{content:""}.fa-square:before{content:""}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.wy-dropdown .caret:before,.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-unsorted:before,.fa-sort:before{content:""}.fa-sort-down:before,.fa-sort-desc:before{content:""}.fa-sort-up:before,.fa-sort-asc:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-legal:before,.fa-gavel:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-flash:before,.fa-bolt:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-paste:before,.fa-clipboard:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-unlink:before,.fa-chain-broken:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:""}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:""}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:""}.fa-euro:before,.fa-eur:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-rupee:before,.fa-inr:before{content:""}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:""}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:""}.fa-won:before,.fa-krw:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-turkish-lira:before,.fa-try:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li span.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-institution:before,.fa-bank:before,.fa-university:before{content:""}.fa-mortar-board:before,.fa-graduation-cap:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:""}.fa-file-zip-o:before,.fa-file-archive-o:before{content:""}.fa-file-sound-o:before,.fa-file-audio-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:""}.fa-ge:before,.fa-empire:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-send:before,.fa-paper-plane:before{content:""}.fa-send-o:before,.fa-paper-plane-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-hotel:before,.fa-bed:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-yc:before,.fa-y-combinator:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-tv:before,.fa-television:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:""}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-signing:before,.fa-sign-language:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-vcard:before,.fa-address-card:before{content:""}.fa-vcard-o:before,.fa-address-card-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content .code-block-caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context{font-family:inherit}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content .code-block-caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before{font-family:"FontAwesome";display:inline-block;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa,a .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li a span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,a .rst-content .admonition-title,.rst-content a .admonition-title,a .rst-content h1 .headerlink,.rst-content h1 a .headerlink,a .rst-content h2 .headerlink,.rst-content h2 a .headerlink,a .rst-content h3 .headerlink,.rst-content h3 a .headerlink,a .rst-content h4 .headerlink,.rst-content h4 a .headerlink,a .rst-content h5 .headerlink,.rst-content h5 a .headerlink,a .rst-content h6 .headerlink,.rst-content h6 a .headerlink,a .rst-content dl dt .headerlink,.rst-content dl dt a .headerlink,a .rst-content p.caption .headerlink,.rst-content p.caption a .headerlink,a .rst-content table>caption .headerlink,.rst-content table>caption a .headerlink,a .rst-content .code-block-caption .headerlink,.rst-content .code-block-caption a .headerlink,a .rst-content tt.download span:first-child,.rst-content tt.download a span:first-child,a .rst-content code.download span:first-child,.rst-content code.download a span:first-child,a .icon{display:inline-block;text-decoration:inherit}.btn .fa,.btn .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .btn span.toctree-expand,.btn .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .btn span.toctree-expand,.btn .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .btn span.toctree-expand,.btn .rst-content .admonition-title,.rst-content .btn .admonition-title,.btn .rst-content h1 .headerlink,.rst-content h1 .btn .headerlink,.btn .rst-content h2 .headerlink,.rst-content h2 .btn .headerlink,.btn .rst-content h3 .headerlink,.rst-content h3 .btn .headerlink,.btn .rst-content h4 .headerlink,.rst-content h4 .btn .headerlink,.btn .rst-content h5 .headerlink,.rst-content h5 .btn .headerlink,.btn .rst-content h6 .headerlink,.rst-content h6 .btn .headerlink,.btn .rst-content dl dt .headerlink,.rst-content dl dt .btn .headerlink,.btn .rst-content p.caption .headerlink,.rst-content p.caption .btn .headerlink,.btn .rst-content table>caption .headerlink,.rst-content table>caption .btn .headerlink,.btn .rst-content .code-block-caption .headerlink,.rst-content .code-block-caption .btn .headerlink,.btn .rst-content tt.download span:first-child,.rst-content tt.download .btn span:first-child,.btn .rst-content code.download span:first-child,.rst-content code.download .btn span:first-child,.btn .icon,.nav .fa,.nav .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .nav span.toctree-expand,.nav .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .nav span.toctree-expand,.nav .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .nav span.toctree-expand,.nav .rst-content .admonition-title,.rst-content .nav .admonition-title,.nav .rst-content h1 .headerlink,.rst-content h1 .nav .headerlink,.nav .rst-content h2 .headerlink,.rst-content h2 .nav .headerlink,.nav .rst-content h3 .headerlink,.rst-content h3 .nav .headerlink,.nav .rst-content h4 .headerlink,.rst-content h4 .nav .headerlink,.nav .rst-content h5 .headerlink,.rst-content h5 .nav .headerlink,.nav .rst-content h6 .headerlink,.rst-content h6 .nav .headerlink,.nav .rst-content dl dt .headerlink,.rst-content dl dt .nav .headerlink,.nav .rst-content p.caption .headerlink,.rst-content p.caption .nav .headerlink,.nav .rst-content table>caption .headerlink,.rst-content table>caption .nav .headerlink,.nav .rst-content .code-block-caption .headerlink,.rst-content .code-block-caption .nav .headerlink,.nav .rst-content tt.download span:first-child,.rst-content tt.download .nav span:first-child,.nav .rst-content code.download span:first-child,.rst-content code.download .nav span:first-child,.nav .icon{display:inline}.btn .fa.fa-large,.btn .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .btn span.fa-large.toctree-expand,.btn .rst-content .fa-large.admonition-title,.rst-content .btn .fa-large.admonition-title,.btn .rst-content h1 .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.btn .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .btn .fa-large.headerlink,.btn .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .btn .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.btn .rst-content .code-block-caption .fa-large.headerlink,.rst-content .code-block-caption .btn .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .btn span.fa-large:first-child,.btn .rst-content code.download span.fa-large:first-child,.rst-content code.download .btn span.fa-large:first-child,.btn .fa-large.icon,.nav .fa.fa-large,.nav .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .nav span.fa-large.toctree-expand,.nav .rst-content .fa-large.admonition-title,.rst-content .nav .fa-large.admonition-title,.nav .rst-content h1 .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.nav .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.nav .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .nav .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.nav .rst-content .code-block-caption .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.nav .rst-content code.download span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.nav .fa-large.icon{line-height:.9em}.btn .fa.fa-spin,.btn .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .btn span.fa-spin.toctree-expand,.btn .rst-content .fa-spin.admonition-title,.rst-content .btn .fa-spin.admonition-title,.btn .rst-content h1 .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.btn .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .btn .fa-spin.headerlink,.btn .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .btn .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.btn .rst-content .code-block-caption .fa-spin.headerlink,.rst-content .code-block-caption .btn .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .btn span.fa-spin:first-child,.btn .rst-content code.download span.fa-spin:first-child,.rst-content code.download .btn span.fa-spin:first-child,.btn .fa-spin.icon,.nav .fa.fa-spin,.nav .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .nav span.fa-spin.toctree-expand,.nav .rst-content .fa-spin.admonition-title,.rst-content .nav .fa-spin.admonition-title,.nav .rst-content h1 .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.nav .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.nav .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .nav .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.nav .rst-content .code-block-caption .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.nav .rst-content code.download span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.nav .fa-spin.icon{display:inline-block}.btn.fa:before,.wy-menu-vertical li span.btn.toctree-expand:before,.rst-content .btn.admonition-title:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content dl dt .btn.headerlink:before,.rst-content p.caption .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.rst-content code.download span.btn:first-child:before,.btn.icon:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.wy-menu-vertical li span.btn.toctree-expand:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content p.caption .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.rst-content code.download span.btn:first-child:hover:before,.btn.icon:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li .btn-mini span.toctree-expand:before,.btn-mini .rst-content .admonition-title:before,.rst-content .btn-mini .admonition-title:before,.btn-mini .rst-content h1 .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.btn-mini .rst-content dl dt .headerlink:before,.rst-content dl dt .btn-mini .headerlink:before,.btn-mini .rst-content p.caption .headerlink:before,.rst-content p.caption .btn-mini .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.rst-content tt.download .btn-mini span:first-child:before,.btn-mini .rst-content code.download span:first-child:before,.rst-content code.download .btn-mini span:first-child:before,.btn-mini .icon:before{font-size:14px;vertical-align:-15%}.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo,.rst-content .admonition{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.wy-alert-title,.rst-content .admonition-title{color:#fff;font-weight:bold;display:block;color:#fff;background:#6ab0de;margin:-12px;padding:6px 12px;margin-bottom:12px}.wy-alert.wy-alert-danger,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.admonition{background:#fdf3f2}.wy-alert.wy-alert-danger .wy-alert-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .danger .wy-alert-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .danger .admonition-title,.rst-content .error .admonition-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition .admonition-title{background:#f29f97}.wy-alert.wy-alert-warning,.rst-content .wy-alert-warning.note,.rst-content .attention,.rst-content .caution,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.tip,.rst-content .warning,.rst-content .wy-alert-warning.seealso,.rst-content .admonition-todo,.rst-content .wy-alert-warning.admonition{background:#ffedcc}.wy-alert.wy-alert-warning .wy-alert-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .attention .wy-alert-title,.rst-content .caution .wy-alert-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .attention .admonition-title,.rst-content .caution .admonition-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .warning .admonition-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .admonition-todo .admonition-title,.rst-content .wy-alert-warning.admonition .admonition-title{background:#f0b37e}.wy-alert.wy-alert-info,.rst-content .note,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.rst-content .seealso,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.admonition{background:#e7f2fa}.wy-alert.wy-alert-info .wy-alert-title,.rst-content .note .wy-alert-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.rst-content .note .admonition-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .seealso .admonition-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition .admonition-title{background:#6ab0de}.wy-alert.wy-alert-success,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.warning,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.admonition{background:#dbfaf4}.wy-alert.wy-alert-success .wy-alert-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .hint .wy-alert-title,.rst-content .important .wy-alert-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .hint .admonition-title,.rst-content .important .admonition-title,.rst-content .tip .admonition-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition .admonition-title{background:#1abc9c}.wy-alert.wy-alert-neutral,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.admonition{background:#f3f6f6}.wy-alert.wy-alert-neutral .wy-alert-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition .admonition-title{color:#404040;background:#e1e4e5}.wy-alert.wy-alert-neutral a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a{color:#2980B9}.wy-alert p:last-child,.rst-content .note p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.rst-content .seealso p:last-child,.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0px;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,0.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27AE60}.wy-tray-container li.wy-tray-item-info{background:#2980B9}.wy-tray-container li.wy-tray-item-warning{background:#E67E22}.wy-tray-container li.wy-tray-item-danger{background:#E74C3C}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width: 768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px 12px;color:#fff;border:1px solid rgba(0,0,0,0.1);background-color:#27AE60;text-decoration:none;font-weight:normal;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:0px 1px 2px -1px rgba(255,255,255,0.5) inset,0px -2px 0px 0px rgba(0,0,0,0.1) inset;outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:0px -1px 0px 0px rgba(0,0,0,0.05) inset,0px 2px 0px 0px rgba(0,0,0,0.1) inset;padding:8px 12px 6px 12px}.btn:visited{color:#fff}.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn-disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn-disabled:hover,.btn-disabled:focus,.btn-disabled:active{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980B9 !important}.btn-info:hover{background-color:#2e8ece !important}.btn-neutral{background-color:#f3f6f6 !important;color:#404040 !important}.btn-neutral:hover{background-color:#e5ebeb !important;color:#404040}.btn-neutral:visited{color:#404040 !important}.btn-success{background-color:#27AE60 !important}.btn-success:hover{background-color:#295 !important}.btn-danger{background-color:#E74C3C !important}.btn-danger:hover{background-color:#ea6153 !important}.btn-warning{background-color:#E67E22 !important}.btn-warning:hover{background-color:#e98b39 !important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f !important}.btn-link{background-color:transparent !important;color:#2980B9;box-shadow:none;border-color:transparent !important}.btn-link:hover{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:active{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:visited{color:#9B59B6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:before,.wy-btn-group:after{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:solid 1px #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,0.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980B9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:solid 1px #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type="search"]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980B9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned input,.wy-form-aligned textarea,.wy-form-aligned select,.wy-form-aligned .wy-help-inline,.wy-form-aligned label{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{border:0;margin:0;padding:0}legend{display:block;width:100%;border:0;padding:0;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label{display:block;margin:0 0 .3125em 0;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;*zoom:1;max-width:68em;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#E74C3C}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full input[type="text"],.wy-control-group .wy-form-full input[type="password"],.wy-control-group .wy-form-full input[type="email"],.wy-control-group .wy-form-full input[type="url"],.wy-control-group .wy-form-full input[type="date"],.wy-control-group .wy-form-full input[type="month"],.wy-control-group .wy-form-full input[type="time"],.wy-control-group .wy-form-full input[type="datetime"],.wy-control-group .wy-form-full input[type="datetime-local"],.wy-control-group .wy-form-full input[type="week"],.wy-control-group .wy-form-full input[type="number"],.wy-control-group .wy-form-full input[type="search"],.wy-control-group .wy-form-full input[type="tel"],.wy-control-group .wy-form-full input[type="color"],.wy-control-group .wy-form-halves input[type="text"],.wy-control-group .wy-form-halves input[type="password"],.wy-control-group .wy-form-halves input[type="email"],.wy-control-group .wy-form-halves input[type="url"],.wy-control-group .wy-form-halves input[type="date"],.wy-control-group .wy-form-halves input[type="month"],.wy-control-group .wy-form-halves input[type="time"],.wy-control-group .wy-form-halves input[type="datetime"],.wy-control-group .wy-form-halves input[type="datetime-local"],.wy-control-group .wy-form-halves input[type="week"],.wy-control-group .wy-form-halves input[type="number"],.wy-control-group .wy-form-halves input[type="search"],.wy-control-group .wy-form-halves input[type="tel"],.wy-control-group .wy-form-halves input[type="color"],.wy-control-group .wy-form-thirds input[type="text"],.wy-control-group .wy-form-thirds input[type="password"],.wy-control-group .wy-form-thirds input[type="email"],.wy-control-group .wy-form-thirds input[type="url"],.wy-control-group .wy-form-thirds input[type="date"],.wy-control-group .wy-form-thirds input[type="month"],.wy-control-group .wy-form-thirds input[type="time"],.wy-control-group .wy-form-thirds input[type="datetime"],.wy-control-group .wy-form-thirds input[type="datetime-local"],.wy-control-group .wy-form-thirds input[type="week"],.wy-control-group .wy-form-thirds input[type="number"],.wy-control-group .wy-form-thirds input[type="search"],.wy-control-group .wy-form-thirds input[type="tel"],.wy-control-group .wy-form-thirds input[type="color"]{width:100%}.wy-control-group .wy-form-full{float:left;display:block;margin-right:2.3576515979%;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.3576515979%;width:48.821174201%}.wy-control-group .wy-form-halves:last-child{margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n+1){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.3576515979%;width:31.7615656014%}.wy-control-group .wy-form-thirds:last-child{margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control{margin:6px 0 0 0;font-size:90%}.wy-control-no-input{display:inline-block;margin:6px 0 0 0;font-size:90%}.wy-control-group.fluid-input input[type="text"],.wy-control-group.fluid-input input[type="password"],.wy-control-group.fluid-input input[type="email"],.wy-control-group.fluid-input input[type="url"],.wy-control-group.fluid-input input[type="date"],.wy-control-group.fluid-input input[type="month"],.wy-control-group.fluid-input input[type="time"],.wy-control-group.fluid-input input[type="datetime"],.wy-control-group.fluid-input input[type="datetime-local"],.wy-control-group.fluid-input input[type="week"],.wy-control-group.fluid-input input[type="number"],.wy-control-group.fluid-input input[type="search"],.wy-control-group.fluid-input input[type="tel"],.wy-control-group.fluid-input input[type="color"]{width:100%}.wy-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;*overflow:visible}input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type="datetime-local"]{padding:.34375em .625em}input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}input[type="text"]:focus,input[type="password"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus{outline:0;outline:thin dotted \9;border-color:#333}input.no-focus:focus{border-color:#ccc !important}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:1px auto #129FEA}input[type="text"][disabled],input[type="password"][disabled],input[type="email"][disabled],input[type="url"][disabled],input[type="date"][disabled],input[type="month"][disabled],input[type="time"][disabled],input[type="datetime"][disabled],input[type="datetime-local"][disabled],input[type="week"][disabled],input[type="number"][disabled],input[type="search"][disabled],input[type="tel"][disabled],input[type="color"][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#E74C3C;border:1px solid #E74C3C}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#E74C3C}input[type="file"]:focus:invalid:focus,input[type="radio"]:focus:invalid:focus,input[type="checkbox"]:focus:invalid:focus{outline-color:#E74C3C}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type="radio"][disabled],input[type="checkbox"][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:solid 1px #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{position:absolute;content:"";display:block;left:0;top:0;width:36px;height:12px;border-radius:4px;background:#ccc;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{position:absolute;content:"";display:block;width:18px;height:18px;border-radius:4px;background:#999;left:-3px;top:-3px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27AE60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#E74C3C}.wy-control-group.wy-control-group-error input[type="text"],.wy-control-group.wy-control-group-error input[type="password"],.wy-control-group.wy-control-group-error input[type="email"],.wy-control-group.wy-control-group-error input[type="url"],.wy-control-group.wy-control-group-error input[type="date"],.wy-control-group.wy-control-group-error input[type="month"],.wy-control-group.wy-control-group-error input[type="time"],.wy-control-group.wy-control-group-error input[type="datetime"],.wy-control-group.wy-control-group-error input[type="datetime-local"],.wy-control-group.wy-control-group-error input[type="week"],.wy-control-group.wy-control-group-error input[type="number"],.wy-control-group.wy-control-group-error input[type="search"],.wy-control-group.wy-control-group-error input[type="tel"],.wy-control-group.wy-control-group-error input[type="color"]{border:solid 1px #E74C3C}.wy-control-group.wy-control-group-error textarea{border:solid 1px #E74C3C}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27AE60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#E74C3C}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#E67E22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980B9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width: 480px){.wy-form button[type="submit"]{margin:.7em 0 0}.wy-form input[type="text"],.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:.3em;display:block}.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0 0}.wy-form .wy-help-inline,.wy-form-message-inline,.wy-form-message{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width: 768px){.tablet-hide{display:none}}@media screen and (max-width: 480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.wy-table,.rst-content table.docutils,.rst-content table.field-list{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.wy-table caption,.rst-content table.docutils caption,.rst-content table.field-list caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td,.wy-table th,.rst-content table.docutils th,.rst-content table.field-list th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.wy-table td:first-child,.rst-content table.docutils td:first-child,.rst-content table.field-list td:first-child,.wy-table th:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list th:first-child{border-left-width:0}.wy-table thead,.rst-content table.docutils thead,.rst-content table.field-list thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.wy-table thead th,.rst-content table.docutils thead th,.rst-content table.field-list thead th{font-weight:bold;border-bottom:solid 2px #e1e4e5}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td{background-color:transparent;vertical-align:middle}.wy-table td p,.rst-content table.docutils td p,.rst-content table.field-list td p{line-height:18px}.wy-table td p:last-child,.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child{margin-bottom:0}.wy-table .wy-table-cell-min,.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min{width:1%;padding-right:0}.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:gray;font-size:90%}.wy-table-tertiary{color:gray;font-size:80%}.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td,.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td{background-color:#f3f6f6}.wy-table-backed{background-color:#f3f6f6}.wy-table-bordered-all,.rst-content table.docutils{border:1px solid #e1e4e5}.wy-table-bordered-all td,.rst-content table.docutils td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.wy-table-bordered-all tbody>tr:last-child td,.rst-content table.docutils tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0 !important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980B9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9B59B6}html{height:100%;overflow-x:hidden}body{font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;font-weight:normal;color:#404040;min-height:100%;overflow-x:hidden;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#E67E22 !important}a.wy-text-warning:hover{color:#eb9950 !important}.wy-text-info{color:#2980B9 !important}a.wy-text-info:hover{color:#409ad5 !important}.wy-text-success{color:#27AE60 !important}a.wy-text-success:hover{color:#36d278 !important}.wy-text-danger{color:#E74C3C !important}a.wy-text-danger:hover{color:#ed7669 !important}.wy-text-neutral{color:#404040 !important}a.wy-text-neutral:hover{color:#595959 !important}h1,h2,.rst-content .toctree-wrapper p.caption,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif}p{line-height:24px;margin:0;font-size:16px;margin-bottom:24px}h1{font-size:175%}h2,.rst-content .toctree-wrapper p.caption{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}code,.rst-content tt,.rst-content code{white-space:nowrap;max-width:100%;background:#fff;border:solid 1px #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;color:#E74C3C;overflow-x:auto}code.code-large,.rst-content tt.code-large{font-size:90%}.wy-plain-list-disc,.rst-content .section ul,.rst-content .toctree-wrapper ul,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.wy-plain-list-disc li,.rst-content .section ul li,.rst-content .toctree-wrapper ul li,article ul li{list-style:disc;margin-left:24px}.wy-plain-list-disc li p:last-child,.rst-content .section ul li p:last-child,.rst-content .toctree-wrapper ul li p:last-child,article ul li p:last-child{margin-bottom:0}.wy-plain-list-disc li ul,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li ul,article ul li ul{margin-bottom:0}.wy-plain-list-disc li li,.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,article ul li li{list-style:circle}.wy-plain-list-disc li li li,.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,article ul li li li{list-style:square}.wy-plain-list-disc li ol li,.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,article ul li ol li{list-style:decimal}.wy-plain-list-decimal,.rst-content .section ol,.rst-content ol.arabic,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.wy-plain-list-decimal li,.rst-content .section ol li,.rst-content ol.arabic li,article ol li{list-style:decimal;margin-left:24px}.wy-plain-list-decimal li p:last-child,.rst-content .section ol li p:last-child,.rst-content ol.arabic li p:last-child,article ol li p:last-child{margin-bottom:0}.wy-plain-list-decimal li ul,.rst-content .section ol li ul,.rst-content ol.arabic li ul,article ol li ul{margin-bottom:0}.wy-plain-list-decimal li ul li,.rst-content .section ol li ul li,.rst-content ol.arabic li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:before,.wy-breadcrumbs:after{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.wy-breadcrumbs li code,.wy-breadcrumbs li .rst-content tt,.rst-content .wy-breadcrumbs li tt{padding:5px;border:none;background:none}.wy-breadcrumbs li code.literal,.wy-breadcrumbs li .rst-content tt.literal,.rst-content .wy-breadcrumbs li tt.literal{color:#404040}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width: 480px){.wy-breadcrumbs-extra{display:none}.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:before,.wy-menu-horiz:after{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz ul,.wy-menu-horiz li{display:inline-block}.wy-menu-horiz li:hover{background:rgba(255,255,255,0.1)}.wy-menu-horiz li.divide-left{border-left:solid 1px #404040}.wy-menu-horiz li.divide-right{border-right:solid 1px #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#3a7ca8;height:32px;display:inline-block;line-height:32px;padding:0 1.618em;margin:12px 0 0 0;display:block;font-weight:bold;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:solid 1px #404040}.wy-menu-vertical li.divide-bottom{border-bottom:solid 1px #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:gray;border-right:solid 1px #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.wy-menu-vertical li code,.wy-menu-vertical li .rst-content tt,.rst-content .wy-menu-vertical li tt{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li span.toctree-expand{display:block;float:left;margin-left:-1.2em;font-size:.8em;line-height:1.6em;color:#4d4d4d}.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a{color:#404040;padding:.4045em 1.618em;font-weight:bold;position:relative;background:#fcfcfc;border:none;padding-left:1.618em -4px}.wy-menu-vertical li.on a:hover,.wy-menu-vertical li.current>a:hover{background:#fcfcfc}.wy-menu-vertical li.on a:hover span.toctree-expand,.wy-menu-vertical li.current>a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand{display:block;font-size:.8em;line-height:1.6em;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:solid 1px #c9c9c9;border-top:solid 1px #c9c9c9}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a{color:#404040}.wy-menu-vertical li.toctree-l1.current li.toctree-l2>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>ul{display:none}.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current>ul{display:block}.wy-menu-vertical li.toctree-l2.current>a{background:#c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{display:block;background:#c9c9c9;padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l2 span.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3{font-size:.9em}.wy-menu-vertical li.toctree-l3.current>a{background:#bdbdbd;padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{display:block;background:#bdbdbd;padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l3 span.toctree-expand{color:#969696}.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:normal}.wy-menu-vertical a{display:inline-block;line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover span.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980B9;cursor:pointer;color:#fff}.wy-menu-vertical a:active span.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980B9;text-align:center;padding:.809em;display:block;color:#fcfcfc;margin-bottom:.809em}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em auto;height:45px;width:45px;background-color:#2980B9;padding:5px;border-radius:100%}.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a{color:#fcfcfc;font-size:100%;font-weight:bold;display:inline-block;padding:4px 6px;margin-bottom:.809em}.wy-side-nav-search>a:hover,.wy-side-nav-search .wy-dropdown>a:hover{background:rgba(255,255,255,0.1)}.wy-side-nav-search>a img.logo,.wy-side-nav-search .wy-dropdown>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search>a.icon img.logo,.wy-side-nav-search .wy-dropdown>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:normal;color:rgba(255,255,255,0.3)}.wy-nav .wy-menu-vertical header{color:#2980B9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980B9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980B9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:before,.wy-nav-top:after{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:bold}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980B9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,0.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:gray}footer p{margin-bottom:12px}footer span.commit code,footer span.commit .rst-content tt,.rst-content footer span.commit tt{padding:0px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;font-size:1em;background:none;border:none;color:gray}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:before,.rst-footer-buttons:after{width:100%}.rst-footer-buttons:before,.rst-footer-buttons:after{display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:before,.rst-breadcrumbs-buttons:after{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:solid 1px #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:solid 1px #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:gray;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width: 768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-side-scroll{width:auto}.wy-side-nav-search{width:auto}.wy-menu.wy-menu-vertical{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width: 1100px){.wy-nav-content-wrap{background:rgba(0,0,0,0.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,footer,.wy-nav-side{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content p.caption .headerlink,.rst-content p.caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .icon{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content img{max-width:100%;height:auto}.rst-content div.figure{margin-bottom:24px}.rst-content div.figure p.caption{font-style:italic}.rst-content div.figure p:last-child.caption{margin-bottom:0px}.rst-content div.figure.align-center{text-align:center}.rst-content .section>img,.rst-content .section>a>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px 12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;display:block;overflow:auto}.rst-content pre.literal-block,.rst-content div[class^='highlight']{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px 0}.rst-content pre.literal-block div[class^='highlight'],.rst-content div[class^='highlight'] div[class^='highlight']{padding:0px;border:none;margin:0}.rst-content div[class^='highlight'] td.code{width:100%}.rst-content .linenodiv pre{border-right:solid 1px #e6e9ea;margin:0;padding:12px 12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^='highlight'] pre{white-space:pre;margin:0;padding:12px 12px;display:block;overflow:auto}.rst-content div[class^='highlight'] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content pre.literal-block,.rst-content div[class^='highlight'] pre,.rst-content .linenodiv pre{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;font-size:12px;line-height:1.4}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^='highlight'],.rst-content div[class^='highlight'] pre{white-space:pre-wrap}}.rst-content .note .last,.rst-content .attention .last,.rst-content .caution .last,.rst-content .danger .last,.rst-content .error .last,.rst-content .hint .last,.rst-content .important .last,.rst-content .tip .last,.rst-content .warning .last,.rst-content .seealso .last,.rst-content .admonition-todo .last,.rst-content .admonition .last{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,0.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent !important;border-color:rgba(0,0,0,0.1) !important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha li{list-style:upper-alpha}.rst-content .section ol p,.rst-content .section ul p{margin-bottom:12px}.rst-content .section ol p:last-child,.rst-content .section ul p:last-child{margin-bottom:24px}.rst-content .line-block{margin-left:0px;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0px}.rst-content .topic-title{font-weight:bold;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0px 0px 24px 24px}.rst-content .align-left{float:left;margin:0px 24px 24px 0px}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content .toctree-wrapper p.caption .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content .code-block-caption .headerlink{visibility:hidden;font-size:14px}.rst-content h1 .headerlink:after,.rst-content h2 .headerlink:after,.rst-content .toctree-wrapper p.caption .headerlink:after,.rst-content h3 .headerlink:after,.rst-content h4 .headerlink:after,.rst-content h5 .headerlink:after,.rst-content h6 .headerlink:after,.rst-content dl dt .headerlink:after,.rst-content p.caption .headerlink:after,.rst-content table>caption .headerlink:after,.rst-content .code-block-caption .headerlink:after{content:"";font-family:FontAwesome}.rst-content h1:hover .headerlink:after,.rst-content h2:hover .headerlink:after,.rst-content .toctree-wrapper p.caption:hover .headerlink:after,.rst-content h3:hover .headerlink:after,.rst-content h4:hover .headerlink:after,.rst-content h5:hover .headerlink:after,.rst-content h6:hover .headerlink:after,.rst-content dl dt:hover .headerlink:after,.rst-content p.caption:hover .headerlink:after,.rst-content table>caption:hover .headerlink:after,.rst-content .code-block-caption:hover .headerlink:after{visibility:visible}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:solid 1px #e1e4e5}.rst-content .sidebar p,.rst-content .sidebar ul,.rst-content .sidebar dl{font-size:90%}.rst-content .sidebar .last{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif;font-weight:bold;background:#e1e4e5;padding:6px 12px;margin:-24px;margin-bottom:24px;font-size:100%}.rst-content .highlighted{background:#F1C40F;display:inline-block;font-weight:bold;padding:0 6px}.rst-content .footnote-reference,.rst-content .citation-reference{vertical-align:baseline;position:relative;top:-0.4em;line-height:0;font-size:90%}.rst-content table.docutils.citation,.rst-content table.docutils.footnote{background:none;border:none;color:gray}.rst-content table.docutils.citation td,.rst-content table.docutils.citation tr,.rst-content table.docutils.footnote td,.rst-content table.docutils.footnote tr{border:none;background-color:transparent !important;white-space:normal}.rst-content table.docutils.citation td.label,.rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}.rst-content table.docutils.citation tt,.rst-content table.docutils.citation code,.rst-content table.docutils.footnote tt,.rst-content table.docutils.footnote code{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}.rst-content table.docutils td .last,.rst-content table.docutils td .last :last-child{margin-bottom:0}.rst-content table.field-list{border:none}.rst-content table.field-list td{border:none}.rst-content table.field-list td p{font-size:inherit;line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content tt,.rst-content tt,.rst-content code{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;padding:2px 5px}.rst-content tt big,.rst-content tt em,.rst-content tt big,.rst-content code big,.rst-content tt em,.rst-content code em{font-size:100% !important;line-height:normal}.rst-content tt.literal,.rst-content tt.literal,.rst-content code.literal{color:#E74C3C}.rst-content tt.xref,a .rst-content tt,.rst-content tt.xref,.rst-content code.xref,a .rst-content tt,a .rst-content code{font-weight:bold;color:#404040}.rst-content pre,.rst-content kbd,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace}.rst-content a tt,.rst-content a tt,.rst-content a code{color:#2980B9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:bold;margin-bottom:12px}.rst-content dl p,.rst-content dl table,.rst-content dl ul,.rst-content dl ol{margin-bottom:12px !important}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl:not(.docutils){margin-bottom:24px}.rst-content dl:not(.docutils) dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980B9;border-top:solid 3px #6ab0de;padding:6px;position:relative}.rst-content dl:not(.docutils) dt:before{color:#6ab0de}.rst-content dl:not(.docutils) dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dl dt{margin-bottom:6px;border:none;border-left:solid 3px #ccc;background:#f0f0f0;color:#555}.rst-content dl:not(.docutils) dl dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dt:first-child{margin-top:0}.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) code{font-weight:bold}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) code.descclassname{background-color:transparent;border:none;padding:0;font-size:100% !important}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname{font-weight:bold}.rst-content dl:not(.docutils) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:bold}.rst-content dl:not(.docutils) .property{display:inline-block;padding-right:8px}.rst-content .viewcode-link,.rst-content .viewcode-back{display:inline-block;color:#27AE60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:bold}.rst-content tt.download,.rst-content code.download{background:inherit;padding:inherit;font-weight:normal;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content tt.download span:first-child,.rst-content code.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .versionmodified{font-style:italic}@media screen and (max-width: 480px){.rst-content .sidebar{width:100%}}span[id*='MathJax-Span']{color:#404040}.math{text-align:center}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-regular.eot");src:url("../fonts/Lato/lato-regular.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-regular.woff2") format("woff2"),url("../fonts/Lato/lato-regular.woff") format("woff"),url("../fonts/Lato/lato-regular.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-bold.eot");src:url("../fonts/Lato/lato-bold.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-bold.woff2") format("woff2"),url("../fonts/Lato/lato-bold.woff") format("woff"),url("../fonts/Lato/lato-bold.ttf") format("truetype");font-weight:700;font-style:normal}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-bolditalic.eot");src:url("../fonts/Lato/lato-bolditalic.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-bolditalic.woff2") format("woff2"),url("../fonts/Lato/lato-bolditalic.woff") format("woff"),url("../fonts/Lato/lato-bolditalic.ttf") format("truetype");font-weight:700;font-style:italic}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-italic.eot");src:url("../fonts/Lato/lato-italic.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-italic.woff2") format("woff2"),url("../fonts/Lato/lato-italic.woff") format("woff"),url("../fonts/Lato/lato-italic.ttf") format("truetype");font-weight:400;font-style:italic}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:400;src:url("../fonts/RobotoSlab/roboto-slab.eot");src:url("../fonts/RobotoSlab/roboto-slab-v7-regular.eot?#iefix") format("embedded-opentype"),url("../fonts/RobotoSlab/roboto-slab-v7-regular.woff2") format("woff2"),url("../fonts/RobotoSlab/roboto-slab-v7-regular.woff") format("woff"),url("../fonts/RobotoSlab/roboto-slab-v7-regular.ttf") format("truetype")}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:700;src:url("../fonts/RobotoSlab/roboto-slab-v7-bold.eot");src:url("../fonts/RobotoSlab/roboto-slab-v7-bold.eot?#iefix") format("embedded-opentype"),url("../fonts/RobotoSlab/roboto-slab-v7-bold.woff2") format("woff2"),url("../fonts/RobotoSlab/roboto-slab-v7-bold.woff") format("woff"),url("../fonts/RobotoSlab/roboto-slab-v7-bold.ttf") format("truetype")} + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.rst-content .wy-breadcrumbs li tt,.wy-breadcrumbs li .rst-content tt,.wy-breadcrumbs li code{padding:5px;border:none;background:none}.rst-content .wy-breadcrumbs li tt.literal,.wy-breadcrumbs li .rst-content tt.literal,.wy-breadcrumbs li code.literal{color:#404040}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.field-list>dt:after,html.writer-html5 .rst-content dl.footnote>dt:after{content:":"}html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.footnote>dt>span.brackets{margin-right:.5rem}html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{font-style:italic}html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.footnote>dd p,html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{font-size:inherit;line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/doc/build/html/_static/doctools.js b/doc/build/html/_static/doctools.js index 527b876c..e509e483 100644 --- a/doc/build/html/_static/doctools.js +++ b/doc/build/html/_static/doctools.js @@ -2,155 +2,325 @@ * doctools.js * ~~~~~~~~~~~ * - * Base JavaScript utilities for all Sphinx HTML documentation. + * Sphinx JavaScript utilities for all documentation. * * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ -"use strict"; - -const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ - "TEXTAREA", - "INPUT", - "SELECT", - "BUTTON", -]); - -const _ready = (callback) => { - if (document.readyState !== "loading") { - callback(); - } else { - document.addEventListener("DOMContentLoaded", callback); + +/** + * select a different prefix for underscore + */ +$u = _.noConflict(); + +/** + * make the code below compatible with browsers without + * an installed firebug like debugger +if (!window.console || !console.firebug) { + var names = ["log", "debug", "info", "warn", "error", "assert", "dir", + "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", + "profile", "profileEnd"]; + window.console = {}; + for (var i = 0; i < names.length; ++i) + window.console[names[i]] = function() {}; +} + */ + +/** + * small helper function to urldecode strings + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL + */ +jQuery.urldecode = function(x) { + if (!x) { + return x + } + return decodeURIComponent(x.replace(/\+/g, ' ')); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s === 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; } + return result; }; +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node, addItems) { + if (node.nodeType === 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { + var span; + var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.className = className; + } + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + if (isInSVG) { + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + var bbox = node.parentElement.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute('class', className); + addItems.push({ + "parent": node.parentNode, + "target": rect}); + } + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this, addItems); + }); + } + } + var addItems = []; + var result = this.each(function() { + highlight(this, addItems); + }); + for (var i = 0; i < addItems.length; ++i) { + jQuery(addItems[i].parent).before(addItems[i].target); + } + return result; +}; + +/* + * backward compatibility for jQuery.browser + * This will be supported until firefox bug is fixed. + */ +if (!jQuery.browser) { + jQuery.uaMatch = function(ua) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec(ua) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || + /(msie) ([\w.]+)/.exec(ua) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + }; + jQuery.browser = {}; + jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; +} + /** * Small JavaScript module for the documentation. */ -const Documentation = { - init: () => { - Documentation.initDomainIndexTable(); - Documentation.initOnKeyListeners(); +var Documentation = { + + init : function() { + this.fixFirefoxAnchorBug(); + this.highlightSearchWords(); + this.initIndexTable(); + if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) { + this.initOnKeyListeners(); + } }, /** * i18n support */ - TRANSLATIONS: {}, - PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), - LOCALE: "unknown", + TRANSLATIONS : {}, + PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; }, + LOCALE : 'unknown', // gettext and ngettext don't access this so that the functions // can safely bound to a different name (_ = Documentation.gettext) - gettext: (string) => { - const translated = Documentation.TRANSLATIONS[string]; - switch (typeof translated) { - case "undefined": - return string; // no translation - case "string": - return translated; // translation exists - default: - return translated[0]; // (singular, plural) translation tuple exists - } + gettext : function(string) { + var translated = Documentation.TRANSLATIONS[string]; + if (typeof translated === 'undefined') + return string; + return (typeof translated === 'string') ? translated : translated[0]; }, - ngettext: (singular, plural, n) => { - const translated = Documentation.TRANSLATIONS[singular]; - if (typeof translated !== "undefined") - return translated[Documentation.PLURAL_EXPR(n)]; - return n === 1 ? singular : plural; + ngettext : function(singular, plural, n) { + var translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated === 'undefined') + return (n == 1) ? singular : plural; + return translated[Documentation.PLURALEXPR(n)]; }, - addTranslations: (catalog) => { - Object.assign(Documentation.TRANSLATIONS, catalog.messages); - Documentation.PLURAL_EXPR = new Function( - "n", - `return (${catalog.plural_expr})` - ); - Documentation.LOCALE = catalog.locale; + addTranslations : function(catalog) { + for (var key in catalog.messages) + this.TRANSLATIONS[key] = catalog.messages[key]; + this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); + this.LOCALE = catalog.locale; }, /** - * helper function to focus on search bar + * add context elements like header anchor links */ - focusSearchBar: () => { - document.querySelectorAll("input[name=q]")[0]?.focus(); + addContextElements : function() { + $('div[id] > :header:first').each(function() { + $('\u00B6'). + attr('href', '#' + this.id). + attr('title', _('Permalink to this headline')). + appendTo(this); + }); + $('dt[id]').each(function() { + $('\u00B6'). + attr('href', '#' + this.id). + attr('title', _('Permalink to this definition')). + appendTo(this); + }); }, /** - * Initialise the domain index toggle buttons + * workaround a firefox stupidity + * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 */ - initDomainIndexTable: () => { - const toggler = (el) => { - const idNumber = el.id.substr(7); - const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); - if (el.src.substr(-9) === "minus.png") { - el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; - toggledRows.forEach((el) => (el.style.display = "none")); - } else { - el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; - toggledRows.forEach((el) => (el.style.display = "")); + fixFirefoxAnchorBug : function() { + if (document.location.hash && $.browser.mozilla) + window.setTimeout(function() { + document.location.href += ''; + }, 10); + }, + + /** + * highlight the search words provided in the url in the text + */ + highlightSearchWords : function() { + var params = $.getQueryParameters(); + var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; + if (terms.length) { + var body = $('div.body'); + if (!body.length) { + body = $('body'); } - }; + window.setTimeout(function() { + $.each(terms, function() { + body.highlightText(this.toLowerCase(), 'highlighted'); + }); + }, 10); + $('') + .appendTo($('#searchbox')); + } + }, + + /** + * init the domain index toggle buttons + */ + initIndexTable : function() { + var togglers = $('img.toggler').click(function() { + var src = $(this).attr('src'); + var idnum = $(this).attr('id').substr(7); + $('tr.cg-' + idnum).toggle(); + if (src.substr(-9) === 'minus.png') + $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); + else + $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); + }).css('display', ''); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { + togglers.click(); + } + }, - const togglerElements = document.querySelectorAll("img.toggler"); - togglerElements.forEach((el) => - el.addEventListener("click", (event) => toggler(event.currentTarget)) - ); - togglerElements.forEach((el) => (el.style.display = "")); - if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + /** + * helper function to hide the search marks again + */ + hideSearchWords : function() { + $('#searchbox .highlight-link').fadeOut(300); + $('span.highlighted').removeClass('highlighted'); + var url = new URL(window.location); + url.searchParams.delete('highlight'); + window.history.replaceState({}, '', url); }, - initOnKeyListeners: () => { - // only install a listener if it is really needed - if ( - !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && - !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS - ) - return; - - document.addEventListener("keydown", (event) => { - // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; - // bail with special keys - if (event.altKey || event.ctrlKey || event.metaKey) return; - - if (!event.shiftKey) { - switch (event.key) { - case "ArrowLeft": - if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; - - const prevLink = document.querySelector('link[rel="prev"]'); - if (prevLink && prevLink.href) { - window.location.href = prevLink.href; - event.preventDefault(); + /** + * make the url absolute + */ + makeURL : function(relativeURL) { + return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; + }, + + /** + * get the current relative url + */ + getCurrentURL : function() { + var path = document.location.pathname; + var parts = path.split(/\//); + $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { + if (this === '..') + parts.pop(); + }); + var url = parts.join('/'); + return path.substring(url.lastIndexOf('/') + 1, path.length - 1); + }, + + initOnKeyListeners: function() { + $(document).keydown(function(event) { + var activeElementType = document.activeElement.tagName; + // don't navigate when in search box, textarea, dropdown or button + if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT' + && activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey + && !event.shiftKey) { + switch (event.keyCode) { + case 37: // left + var prevHref = $('link[rel="prev"]').prop('href'); + if (prevHref) { + window.location.href = prevHref; + return false; } break; - case "ArrowRight": - if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; - - const nextLink = document.querySelector('link[rel="next"]'); - if (nextLink && nextLink.href) { - window.location.href = nextLink.href; - event.preventDefault(); + case 39: // right + var nextHref = $('link[rel="next"]').prop('href'); + if (nextHref) { + window.location.href = nextHref; + return false; } break; } } - - // some keyboard layouts may need Shift to get / - switch (event.key) { - case "/": - if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; - Documentation.focusSearchBar(); - event.preventDefault(); - } }); - }, + } }; // quick alias for translations -const _ = Documentation.gettext; +_ = Documentation.gettext; -_ready(Documentation.init); +$(document).ready(function() { + Documentation.init(); +}); diff --git a/doc/build/html/_static/documentation_options.js b/doc/build/html/_static/documentation_options.js index be87342d..3b2655ae 100644 --- a/doc/build/html/_static/documentation_options.js +++ b/doc/build/html/_static/documentation_options.js @@ -1,14 +1,12 @@ var DOCUMENTATION_OPTIONS = { URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), VERSION: '0.20', - LANGUAGE: 'en', + LANGUAGE: 'None', COLLAPSE_INDEX: false, BUILDER: 'html', FILE_SUFFIX: '.html', LINK_SUFFIX: '.html', HAS_SOURCE: true, SOURCELINK_SUFFIX: '.txt', - NAVIGATION_WITH_KEYS: false, - SHOW_SEARCH_SUMMARY: true, - ENABLE_SEARCH_SHORTCUTS: true, + NAVIGATION_WITH_KEYS: false }; \ No newline at end of file diff --git a/doc/build/html/_static/jquery.js b/doc/build/html/_static/jquery.js index c4c6022f..b0614034 100644 --- a/doc/build/html/_static/jquery.js +++ b/doc/build/html/_static/jquery.js @@ -1,2 +1,2 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0"),i("table.docutils.footnote").wrap("
"),i("table.docutils.citation").wrap("
"),i(".wy-menu-vertical ul").not(".simple").siblings("a").each(function(){var e=i(this);expand=i(''),expand.on("click",function(n){return t.toggleCurrent(e),n.stopPropagation(),!1}),e.prepend(expand)})},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),i=e.find('[href="'+n+'"]');if(0===i.length){var t=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(i=e.find('[href="#'+t.attr("id")+'"]')).length&&(i=e.find('[href="#"]'))}0this.docHeight||(this.navBar.scrollTop(i),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",function(){this.linkScroll=!1})},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current"),e.siblings().find("li.current").removeClass("current"),e.find("> ul li.current").removeClass("current"),e.toggleClass("current")}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:e.exports.ThemeNav,StickyNav:e.exports.ThemeNav}),function(){for(var r=0,n=["ms","moz","webkit","o"],e=0;e"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t { - const [docname, title, anchor, descr, score, filename] = result - return score + score: function(result) { + return result[4]; }, */ @@ -30,11 +28,9 @@ if (typeof Scorer === "undefined") { // or matches in the last dotted part of the object name objPartialMatch: 6, // Additive scores depending on the priority of the object - objPrio: { - 0: 15, // used to be importantResults - 1: 5, // used to be objectResults - 2: -5, // used to be unimportantResults - }, + objPrio: {0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5}, // used to be unimportantResults // Used when the priority is not in the mapping. objPrioDefault: 0, @@ -43,495 +39,456 @@ if (typeof Scorer === "undefined") { partialTitle: 7, // query found in terms term: 5, - partialTerm: 2, + partialTerm: 2 }; } -const _removeChildren = (element) => { - while (element && element.lastChild) element.removeChild(element.lastChild); -}; - -/** - * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping - */ -const _escapeRegExp = (string) => - string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string - -const _displayItem = (item, searchTerms) => { - const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; - const docUrlRoot = DOCUMENTATION_OPTIONS.URL_ROOT; - const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; - const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; - const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; - - const [docName, title, anchor, descr, score, _filename] = item; - - let listItem = document.createElement("li"); - let requestUrl; - let linkUrl; - if (docBuilder === "dirhtml") { - // dirhtml builder - let dirname = docName + "/"; - if (dirname.match(/\/index\/$/)) - dirname = dirname.substring(0, dirname.length - 6); - else if (dirname === "index/") dirname = ""; - requestUrl = docUrlRoot + dirname; - linkUrl = requestUrl; - } else { - // normal html builders - requestUrl = docUrlRoot + docName + docFileSuffix; - linkUrl = docName + docLinkSuffix; +if (!splitQuery) { + function splitQuery(query) { + return query.split(/\s+/); } - let linkEl = listItem.appendChild(document.createElement("a")); - linkEl.href = linkUrl + anchor; - linkEl.dataset.score = score; - linkEl.innerHTML = title; - if (descr) - listItem.appendChild(document.createElement("span")).innerHTML = - " (" + descr + ")"; - else if (showSearchSummary) - fetch(requestUrl) - .then((responseData) => responseData.text()) - .then((data) => { - if (data) - listItem.appendChild( - Search.makeSearchSummary(data, searchTerms) - ); - }); - Search.output.appendChild(listItem); -}; -const _finishSearch = (resultCount) => { - Search.stopPulse(); - Search.title.innerText = _("Search Results"); - if (!resultCount) - Search.status.innerText = Documentation.gettext( - "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." - ); - else - Search.status.innerText = _( - `Search finished, found ${resultCount} page(s) matching the search query.` - ); -}; -const _displayNextItem = ( - results, - resultCount, - searchTerms -) => { - // results left, load the summary and display it - // this is intended to be dynamic (don't sub resultsCount) - if (results.length) { - _displayItem(results.pop(), searchTerms); - setTimeout( - () => _displayNextItem(results, resultCount, searchTerms), - 5 - ); - } - // search finished, update title and status message - else _finishSearch(resultCount); -}; - -/** - * Default splitQuery function. Can be overridden in ``sphinx.search`` with a - * custom function per language. - * - * The regular expression works by splitting the string on consecutive characters - * that are not Unicode letters, numbers, underscores, or emoji characters. - * This is the same as ``\W+`` in Python, preserving the surrogate pair area. - */ -if (typeof splitQuery === "undefined") { - var splitQuery = (query) => query - .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) - .filter(term => term) // remove remaining empty strings } /** * Search Module */ -const Search = { - _index: null, - _queued_query: null, - _pulse_status: -1, - - htmlToText: (htmlString) => { - const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); - htmlElement.querySelectorAll(".headerlink").forEach((el) => { el.remove() }); - const docContent = htmlElement.querySelector('[role="main"]'); - if (docContent !== undefined) return docContent.textContent; - console.warn( - "Content block not found. Sphinx search tries to obtain it via '[role=main]'. Could you check your theme or template." - ); - return ""; +var Search = { + + _index : null, + _queued_query : null, + _pulse_status : -1, + + htmlToText : function(htmlString) { + var virtualDocument = document.implementation.createHTMLDocument('virtual'); + var htmlElement = $(htmlString, virtualDocument); + htmlElement.find('.headerlink').remove(); + docContent = htmlElement.find('[role=main]')[0]; + if(docContent === undefined) { + console.warn("Content block not found. Sphinx search tries to obtain it " + + "via '[role=main]'. Could you check your theme or template."); + return ""; + } + return docContent.textContent || docContent.innerText; }, - init: () => { - const query = new URLSearchParams(window.location.search).get("q"); - document - .querySelectorAll('input[name="q"]') - .forEach((el) => (el.value = query)); - if (query) Search.performSearch(query); + init : function() { + var params = $.getQueryParameters(); + if (params.q) { + var query = params.q[0]; + $('input[name="q"]')[0].value = query; + this.performSearch(query); + } }, - loadIndex: (url) => - (document.body.appendChild(document.createElement("script")).src = url), + loadIndex : function(url) { + $.ajax({type: "GET", url: url, data: null, + dataType: "script", cache: true, + complete: function(jqxhr, textstatus) { + if (textstatus != "success") { + document.getElementById("searchindexloader").src = url; + } + }}); + }, - setIndex: (index) => { - Search._index = index; - if (Search._queued_query !== null) { - const query = Search._queued_query; - Search._queued_query = null; - Search.query(query); + setIndex : function(index) { + var q; + this._index = index; + if ((q = this._queued_query) !== null) { + this._queued_query = null; + Search.query(q); } }, - hasIndex: () => Search._index !== null, - - deferQuery: (query) => (Search._queued_query = query), + hasIndex : function() { + return this._index !== null; + }, - stopPulse: () => (Search._pulse_status = -1), + deferQuery : function(query) { + this._queued_query = query; + }, - startPulse: () => { - if (Search._pulse_status >= 0) return; + stopPulse : function() { + this._pulse_status = 0; + }, - const pulse = () => { + startPulse : function() { + if (this._pulse_status >= 0) + return; + function pulse() { + var i; Search._pulse_status = (Search._pulse_status + 1) % 4; - Search.dots.innerText = ".".repeat(Search._pulse_status); - if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); - }; + var dotString = ''; + for (i = 0; i < Search._pulse_status; i++) + dotString += '.'; + Search.dots.text(dotString); + if (Search._pulse_status > -1) + window.setTimeout(pulse, 500); + } pulse(); }, /** * perform a search for something (or wait until index is loaded) */ - performSearch: (query) => { + performSearch : function(query) { // create the required interface elements - const searchText = document.createElement("h2"); - searchText.textContent = _("Searching"); - const searchSummary = document.createElement("p"); - searchSummary.classList.add("search-summary"); - searchSummary.innerText = ""; - const searchList = document.createElement("ul"); - searchList.classList.add("search"); - - const out = document.getElementById("search-results"); - Search.title = out.appendChild(searchText); - Search.dots = Search.title.appendChild(document.createElement("span")); - Search.status = out.appendChild(searchSummary); - Search.output = out.appendChild(searchList); - - const searchProgress = document.getElementById("search-progress"); - // Some themes don't use the search progress node - if (searchProgress) { - searchProgress.innerText = _("Preparing search..."); - } - Search.startPulse(); + this.out = $('#search-results'); + this.title = $('

' + _('Searching') + '

').appendTo(this.out); + this.dots = $('').appendTo(this.title); + this.status = $('

 

').appendTo(this.out); + this.output = $('
- -
- - -