diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66f16af3..50ec6bd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: - name: Run Tests with coverage and save report run: | - coverage run -m pytest pytest/ --junitxml=report.xml --html=report.html + coverage run -m pytest tests/ --junitxml=report.xml --html=report.html coverage xml -o coverage.xml continue-on-error: true # Continue to the next step even if tests fail diff --git a/.gitignore b/.gitignore index ea6f81b1..ddd329e6 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,17 @@ dist/pcntoolkit-0.27-py3.11.egg # Basic test functions for SHASH tests/test_SHASH.ipynb +# Dist folder +dist/pcntoolkit-0.30.post2-py3.12.egg +dist/* +dist +build/* + +# CLI test folder +tests/cli_test/* +!tests/cli_test/test_cli.sh +!tests/cli_test/split_data.py + +docs/autoapi/* +docs/_build/* +docs \ No newline at end of file diff --git a/.nojekyll b/.nojekyll deleted file mode 100644 index 8b137891..00000000 --- a/.nojekyll +++ /dev/null @@ -1 +0,0 @@ - diff --git a/CHANGES b/CHANGES index 169180f0..99fa20b5 100644 --- a/CHANGES +++ b/CHANGES @@ -87,3 +87,18 @@ version 0.29 - Addedd functionality to compute SHASH z-scores from normative.py - Updated requirements - Basic pytest continuous integration framework implemented + +version 0.30.0 +- Minor bug fixes + +version 0.31.0 +- Major changes: + - Move to Poetry for dependency management in pyproject.toml. + - PCNToolkit must now be installed using python -m pip install .. See the README for complete instructions. + - A CLI command normative is automatically created, and can be used instead of python normative.py. + - Nutpie can be used as a sampler for HBR by setting `nuts_sampler='nutpie''. Nutpie and numba must first be installed using conda. +- Minor changes + - torque jobs now support multicore jobs via the keyword 'n_cores_per_batch' + - Backwards compatibilty improved by using pd.read_pickle instead of pickle.load + - SHASH classes have been refactored and improved + - HBR priors improved diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index f9bd1455..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include requirements.txt diff --git a/PCNtoolkit.drawio b/PCNtoolkit.drawio deleted file mode 100644 index 5bf3ed1d..00000000 --- a/PCNtoolkit.drawio +++ /dev/null @@ -1,909 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/README.md b/README.md index fa37c7d4..eddeb9ad 100644 --- a/README.md +++ b/README.md @@ -8,64 +8,94 @@ Methods for normative modelling, spatial statistics and pattern recognition. Doc ## Basic installation (on a local machine) -i) install anaconda3 ii) create enviornment with "conda create --name " iii) activate environment by "source activate " iv) install required conda packages +#### Install anaconda3 + +using the download here: https://www.anaconda.com/download + +#### Create environment +``` +conda create -n python==3.12 +``` + +#### Activate environment ``` -conda install pip pandas scipy +source activate ``` -v) install PCNtoolkit (plus dependencies) +### Install nutpie and numba from conda-forge +``` +conda install nutpie numba -c conda-forge +``` + +#### Install torch from pytorch.org + +Use the command that you get from the command builder here: https://pytorch.org/get-started/locally/. This will ensure you do not install the CUDA version of torch if your pc does not have a GPU. We also recommend that you use the `conda` option. + + +#### Install PCNtoolkit + +Using pip: ``` pip install pcntoolkit ``` +Using a local clone of the repo: +``` +python -m pip install . +``` + ## Alternative installation (on a shared resource) -Make sure conda is available on the system. + +#### Make sure conda is available on the system. Otherwise install it first from https://www.anaconda.com/ ``` conda --version ``` -Create a conda environment in a shared location +#### Create a conda environment in a shared location ``` -conda create -y python==3.8.3 numpy mkl blas --prefix=/shared/conda/ +conda create -y python==3.12 numpy mkl blas --prefix=/shared/conda/ ``` -Activate the conda environment +#### Activate the conda environment ``` conda activate /shared/conda/ ``` -Install other dependencies +### Install nutpie using conda ``` -conda install -y pandas scipy +conda install nutpie numba -c conda-forge ``` -Install pip dependencies +#### install torch + +Using the command that you get from the command builder here: ``` -pip --no-cache-dir install nibabel scikit-learn torch glob3 +https://pytorch.org/get-started/locally/ ``` -Clone the repo +If your shared resource has no GPU, make sure you select the 'CPU' field in the 'Compute Platform' row. Here we also prefer conda over pip. + +#### Clone the repo ``` git clone https://github.com/amarquand/PCNtoolkit.git ``` -install in the conda environment +### Install in the conda environment ``` cd PCNtoolkit/ -python3 setup.py install +python -m pip install . ``` - -Test +### Test ``` python -c "import pcntoolkit as pk;print(pk.__file__)" ``` diff --git a/build/lib/pcntoolkit/configs.py b/build/lib/pcntoolkit/configs.py deleted file mode 100644 index 98b56f17..00000000 --- a/build/lib/pcntoolkit/configs.py +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Mon Dec 7 12:51:07 2020 - -@author: seykia -""" - -PICKLE_PROTOCOL = 4 diff --git a/dist/pcntoolkit-0.30.post2-py3.12.egg b/dist/pcntoolkit-0.30.post2-py3.12.egg new file mode 100644 index 00000000..69167a63 Binary files /dev/null and b/dist/pcntoolkit-0.30.post2-py3.12.egg differ diff --git a/doc/build/doctrees/environment.pickle b/doc/build/doctrees/environment.pickle index ed234a5f..1161f7a2 100644 Binary files a/doc/build/doctrees/environment.pickle and b/doc/build/doctrees/environment.pickle differ diff --git a/doc/build/doctrees/index.doctree b/doc/build/doctrees/index.doctree index 6b3b646b..de33cbad 100644 Binary files a/doc/build/doctrees/index.doctree and b/doc/build/doctrees/index.doctree differ diff --git a/doc/build/doctrees/pages/BLR_normativemodel_protocol.doctree b/doc/build/doctrees/pages/BLR_normativemodel_protocol.doctree index 46f3849e..d85feca8 100644 Binary files a/doc/build/doctrees/pages/BLR_normativemodel_protocol.doctree and b/doc/build/doctrees/pages/BLR_normativemodel_protocol.doctree differ diff --git a/doc/build/doctrees/pages/FAQs.doctree b/doc/build/doctrees/pages/FAQs.doctree index 28e269f6..c71de8ed 100644 Binary files a/doc/build/doctrees/pages/FAQs.doctree and b/doc/build/doctrees/pages/FAQs.doctree differ diff --git a/doc/build/doctrees/pages/HBR_NormativeModel_FCONdata_Tutorial.doctree b/doc/build/doctrees/pages/HBR_NormativeModel_FCONdata_Tutorial.doctree index 107aed98..b1c2b183 100644 Binary files a/doc/build/doctrees/pages/HBR_NormativeModel_FCONdata_Tutorial.doctree and b/doc/build/doctrees/pages/HBR_NormativeModel_FCONdata_Tutorial.doctree differ diff --git a/doc/build/doctrees/pages/acknowledgements.doctree b/doc/build/doctrees/pages/acknowledgements.doctree index 7dd52a20..73416d90 100644 Binary files a/doc/build/doctrees/pages/acknowledgements.doctree and b/doc/build/doctrees/pages/acknowledgements.doctree differ diff --git a/doc/build/doctrees/pages/apply_normative_models.doctree b/doc/build/doctrees/pages/apply_normative_models.doctree index 4a24e45c..a1c3942e 100644 Binary files a/doc/build/doctrees/pages/apply_normative_models.doctree and b/doc/build/doctrees/pages/apply_normative_models.doctree differ diff --git a/doc/build/doctrees/pages/citing.doctree b/doc/build/doctrees/pages/citing.doctree index 1aa638d9..ea469b4c 100644 Binary files a/doc/build/doctrees/pages/citing.doctree and b/doc/build/doctrees/pages/citing.doctree differ diff --git a/doc/build/doctrees/pages/glossary.doctree b/doc/build/doctrees/pages/glossary.doctree index 6151007c..6cea70a6 100644 Binary files a/doc/build/doctrees/pages/glossary.doctree and b/doc/build/doctrees/pages/glossary.doctree differ diff --git a/doc/build/doctrees/pages/installation.doctree b/doc/build/doctrees/pages/installation.doctree index a7a2b7f4..a220a7dd 100644 Binary files a/doc/build/doctrees/pages/installation.doctree and b/doc/build/doctrees/pages/installation.doctree differ diff --git a/doc/build/doctrees/pages/modindex.doctree b/doc/build/doctrees/pages/modindex.doctree index 6a4a8aad..10b1aecd 100644 Binary files a/doc/build/doctrees/pages/modindex.doctree and b/doc/build/doctrees/pages/modindex.doctree differ diff --git a/doc/build/doctrees/pages/normative_modelling_walkthrough.doctree b/doc/build/doctrees/pages/normative_modelling_walkthrough.doctree index b2e8da1b..d8cdc849 100644 Binary files a/doc/build/doctrees/pages/normative_modelling_walkthrough.doctree and b/doc/build/doctrees/pages/normative_modelling_walkthrough.doctree differ diff --git a/doc/build/doctrees/pages/other_predictive_models.doctree b/doc/build/doctrees/pages/other_predictive_models.doctree index 7cf79227..31015fbb 100644 Binary files a/doc/build/doctrees/pages/other_predictive_models.doctree and b/doc/build/doctrees/pages/other_predictive_models.doctree differ diff --git a/doc/build/doctrees/pages/pcntoolkit_background.doctree b/doc/build/doctrees/pages/pcntoolkit_background.doctree index 26cc2d52..004776a9 100644 Binary files a/doc/build/doctrees/pages/pcntoolkit_background.doctree and b/doc/build/doctrees/pages/pcntoolkit_background.doctree differ diff --git a/doc/build/doctrees/pages/post_hoc_analysis.doctree b/doc/build/doctrees/pages/post_hoc_analysis.doctree index 5ee952c3..bd824185 100644 Binary files a/doc/build/doctrees/pages/post_hoc_analysis.doctree and b/doc/build/doctrees/pages/post_hoc_analysis.doctree differ diff --git a/doc/build/doctrees/pages/visualizations.doctree b/doc/build/doctrees/pages/visualizations.doctree index fcbac270..6181093c 100644 Binary files a/doc/build/doctrees/pages/visualizations.doctree and b/doc/build/doctrees/pages/visualizations.doctree differ diff --git a/doc/build/html/.buildinfo b/doc/build/html/.buildinfo index f65417bd..fbe3938f 100644 --- a/doc/build/html/.buildinfo +++ b/doc/build/html/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 -# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 1b4d3aa4bec5023b71b7824b4d305268 +# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. +config: d51dcb6ba2cb6f1cbbeaf2b9ee091add tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/doc/build/html/_images/other_predictive_models_17_1.png b/doc/build/html/_images/other_predictive_models_17_1.png index f832a949..99be9491 100644 Binary files a/doc/build/html/_images/other_predictive_models_17_1.png and b/doc/build/html/_images/other_predictive_models_17_1.png differ diff --git a/doc/build/html/_images/other_predictive_models_56_0.png b/doc/build/html/_images/other_predictive_models_56_0.png index 86713369..fabb2f3b 100644 Binary files a/doc/build/html/_images/other_predictive_models_56_0.png and b/doc/build/html/_images/other_predictive_models_56_0.png differ diff --git a/doc/build/html/_images/other_predictive_models_73_0.png b/doc/build/html/_images/other_predictive_models_73_0.png index 0ce9b332..f18578d9 100644 Binary files a/doc/build/html/_images/other_predictive_models_73_0.png and b/doc/build/html/_images/other_predictive_models_73_0.png differ diff --git a/doc/build/html/_modules/bayesreg.html b/doc/build/html/_modules/bayesreg.html index ae91d215..5bf47224 100644 --- a/doc/build/html/_modules/bayesreg.html +++ b/doc/build/html/_modules/bayesreg.html @@ -1,28 +1,24 @@ + + - + bayesreg — Predictive Clinical Neuroscience Toolkit 0.20 documentation - - - + + + - - - - - - - - - + + + + + @@ -62,13 +58,24 @@

Tutorials

Other Useful Stuff

    @@ -111,7 +118,9 @@

    Source code for bayesreg

     from scipy.linalg import LinAlgError
     
     
    -
    [docs]class BLR: +
    +[docs] +class BLR: """Bayesian linear regression Estimation and prediction of Bayesian linear regression models @@ -253,7 +262,9 @@

    Source code for bayesreg

     
             return beta, alpha, gamma
     
    -
    [docs] def post(self, hyp, X, y, Xv=None): +
    +[docs] + 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 @@ -306,7 +317,10 @@

    Source code for bayesreg

             self.D = D
             self.hyp = hyp
    -
    [docs] def loglik(self, hyp, X, y, Xv=None): + +
    +[docs] + def loglik(self, hyp, X, y, Xv=None): """ Function to compute compute log (marginal) likelihood """ # hyperparameters (alpha not needed) @@ -363,7 +377,10 @@

    Source code for bayesreg

             self.nlZ = nlZ
             return nlZ
    -
    [docs] def penalized_loglik(self, hyp, X, y, Xv=None, l=0.1, norm='L1'): + +
    +[docs] + 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 @@ -382,7 +399,10 @@

    Source code for bayesreg

                 print("Requested penalty not recognized, choose between 'L1' or 'L2'.")
             return L
    -
    [docs] def dloglik(self, hyp, X, y, Xv=None): + +
    +[docs] + def dloglik(self, hyp, X, y, Xv=None): """ Function to compute derivatives """ # hyperparameters @@ -484,8 +504,11 @@

    Source code for bayesreg

             self.dnlZ = dnlZ
             return dnlZ
    + # model estimation (optimization) -
    [docs] def estimate(self, hyp0, X, y, **kwargs): +
    +[docs] + def estimate(self, hyp0, X, y, **kwargs): """ Function to estimate the model :param hyp: hyperparameter vector @@ -542,7 +565,10 @@

    Source code for bayesreg

     
             return self.hyp
    -
    [docs] def predict(self, hyp, X, y, Xs, + +
    +[docs] + def predict(self, hyp, X, y, Xs, var_groups_test=None, var_covariates_test=None, **kwargs): """ Function to make predictions from the model @@ -595,7 +621,10 @@

    Source code for bayesreg

     
             return ys, s2
    -
    [docs] def predict_and_adjust(self, hyp, X, y, Xs=None, + +
    +[docs] + def predict_and_adjust(self, hyp, X, y, Xs=None, ys=None, var_groups_test=None, var_groups_adapt=None, **kwargs): @@ -687,7 +716,9 @@

    Source code for bayesreg

                         ys = ys - residuals_mu
                     s2_out = None
     
    -        return ys_out, s2_out
    + return ys_out, s2_out
    +
    +
    diff --git a/doc/build/html/_modules/fileio.html b/doc/build/html/_modules/fileio.html index 54095935..99728826 100644 --- a/doc/build/html/_modules/fileio.html +++ b/doc/build/html/_modules/fileio.html @@ -1,28 +1,24 @@ + + - + fileio — Predictive Clinical Neuroscience Toolkit 0.20 documentation - - - + + + - - - - - - - - - + + + + + @@ -62,13 +58,24 @@

Tutorials

Other Useful Stuff

    @@ -138,7 +145,9 @@

    Source code for fileio

     # ------------------------
     
     
    -
    [docs]def predictive_interval(s2_forward, +
    +[docs] +def predictive_interval(s2_forward, cov_forward, multiplicator): """ @@ -153,7 +162,10 @@

    Source code for fileio

         return PI
    -
    [docs]def create_mask(data_array, mask, verbose=False): + +
    +[docs] +def create_mask(data_array, mask, verbose=False): """ Create a mask from a data array or a nifti file @@ -189,7 +201,10 @@

    Source code for fileio

         return maskvol
    -
    [docs]def vol2vec(dat, mask, verbose=False): + +
    +[docs] +def vol2vec(dat, mask, verbose=False): """ Vectorise a 3d image @@ -224,7 +239,10 @@

    Source code for fileio

         return dat
    -
    [docs]def file_type(filename): + +
    +[docs] +def file_type(filename): """ Determine the file type of a file @@ -250,7 +268,10 @@

    Source code for fileio

         return ftype
    -
    [docs]def file_extension(filename): + +
    +[docs] +def file_extension(filename): """ Determine the file extension of a file (e.g. .nii.gz) @@ -282,7 +303,10 @@

    Source code for fileio

         return ext
    -
    [docs]def file_stem(filename): + +
    +[docs] +def file_stem(filename): """ Determine the file stem of a file (e.g. /path/to/file.nii.gz -> file) @@ -297,12 +321,15 @@

    Source code for fileio

     
         return stm
    + # -------------- # nifti routines # -------------- -
    [docs]def load_nifti(datafile, mask=None, vol=False, verbose=False): +
    +[docs] +def load_nifti(datafile, mask=None, vol=False, verbose=False): """ Load a nifti file into a numpy array @@ -330,7 +357,10 @@

    Source code for fileio

         return dat
    -
    [docs]def save_nifti(data, filename, examplenii, mask, dtype=None): + +
    +[docs] +def save_nifti(data, filename, examplenii, mask, dtype=None): ''' Write output to nifti @@ -372,12 +402,15 @@

    Source code for fileio

     
         nib.save(array_img, filename)
    + # -------------- # cifti routines # -------------- -
    [docs]def load_cifti(filename, vol=False, mask=None, rmtmp=True): +
    +[docs] +def load_cifti(filename, vol=False, mask=None, rmtmp=True): """ Load a cifti file into a numpy array @@ -439,7 +472,10 @@

    Source code for fileio

         return out
    -
    [docs]def save_cifti(data, filename, example, mask=None, vol=True, volatlas=None): + +
    +[docs] +def save_cifti(data, filename, example, mask=None, vol=True, volatlas=None): """ Save a cifti file from a numpy array @@ -535,12 +571,15 @@

    Source code for fileio

         for f in tmpfiles:
             os.remove(f)
    + # -------------- # ascii routines # -------------- -
    [docs]def load_pd(filename): +
    +[docs] +def load_pd(filename): """ Load a csv file into a pandas dataframe @@ -558,7 +597,10 @@

    Source code for fileio

         return x
    -
    [docs]def save_pd(data, filename): + +
    +[docs] +def save_pd(data, filename): """ Save a pandas dataframe to a csv file @@ -577,7 +619,10 @@

    Source code for fileio

                     na_rep='NaN')
    -
    [docs]def load_ascii(filename): + +
    +[docs] +def load_ascii(filename): """ Load an ascii file into a numpy array @@ -593,7 +638,10 @@

    Source code for fileio

         return x
    -
    [docs]def save_ascii(data, filename): + +
    +[docs] +def save_ascii(data, filename): """ Save a numpy array to an ascii file @@ -607,12 +655,15 @@

    Source code for fileio

         # based on pandas
         np.savetxt(filename, data)
    + # ---------------- # generic routines # ---------------- -
    [docs]def save(data, filename, example=None, mask=None, text=False, dtype=None): +
    +[docs] +def save(data, filename, example=None, mask=None, text=False, dtype=None): """ Save a numpy array to a file @@ -639,7 +690,10 @@

    Source code for fileio

             data.to_pickle(filename, protocol=PICKLE_PROTOCOL)
    -
    [docs]def load(filename, mask=None, text=False, vol=True): + +
    +[docs] +def load(filename, mask=None, text=False, vol=True): """ Load a numpy array from a file @@ -664,12 +718,15 @@

    Source code for fileio

             x = x.to_numpy()
         return x
    + # ------------------- # sorting routines for batched in normative parallel # ------------------- -
    [docs]def tryint(s): +
    +[docs] +def tryint(s): """ Try to convert a string to an integer @@ -686,7 +743,10 @@

    Source code for fileio

             return s
    -
    [docs]def alphanum_key(s): + +
    +[docs] +def alphanum_key(s): """ Turn a string into a list of numbers @@ -699,7 +759,10 @@

    Source code for fileio

         return [tryint(c) for c in re.split('([0-9]+)', s)]
    -
    [docs]def sort_nicely(l): + +
    +[docs] +def sort_nicely(l): """ Sort a list of strings in a natural way @@ -711,6 +774,7 @@

    Source code for fileio

         """
     
         return sorted(l, key=alphanum_key)
    +
    diff --git a/doc/build/html/_modules/gp.html b/doc/build/html/_modules/gp.html index 72e63591..1fff0f37 100644 --- a/doc/build/html/_modules/gp.html +++ b/doc/build/html/_modules/gp.html @@ -1,28 +1,24 @@ + + - + gp — Predictive Clinical Neuroscience Toolkit 0.20 documentation - - - + + + - - - - - - - - - + + + + + @@ -62,13 +58,24 @@

Tutorials

Other Useful Stuff

    @@ -134,7 +141,9 @@

    Source code for gp

     # --------------------
     
     
    -
    [docs]class CovBase(with_metaclass(ABCMeta)): +
    +[docs] +class CovBase(with_metaclass(ABCMeta)): """ Base class for covariance functions. All covariance functions must define the following methods:: @@ -148,7 +157,9 @@

    Source code for gp

         def __init__(self, x=None):
             self.n_params = np.nan
     
    -
    [docs] def get_n_params(self): +
    +[docs] + def get_n_params(self): """ Report the number of parameters required """ assert not np.isnan(self.n_params), \ @@ -156,17 +167,27 @@

    Source code for gp

     
             return self.n_params
    -
    [docs] @abstractmethod + +
    +[docs] + @abstractmethod def cov(self, theta, x, z=None): """ Return the full covariance (or cross-covariance if z is given) """
    -
    [docs] @abstractmethod + +
    +[docs] + @abstractmethod def dcov(self, theta, x, i): """ Return the derivative of the covariance function with respect to - the i-th hyperparameter """
    + the i-th hyperparameter """
    +
    + -
    [docs]class CovLin(CovBase): +
    +[docs] +class CovLin(CovBase): """ Linear covariance function (no hyperparameters) """ @@ -174,7 +195,9 @@

    Source code for gp

             self.n_params = 0
             self.first_call = False
     
    -
    [docs] def cov(self, theta, x, z=None): +
    +[docs] + def cov(self, theta, x, z=None): if not self.first_call and not theta and theta is not None: self.first_call = True if len(theta) > 0 and theta[0] is not None: @@ -186,11 +209,18 @@

    Source code for gp

             K = x.dot(z.T)
             return K
    -
    [docs] def dcov(self, theta, x, i): - raise ValueError("Invalid covariance function parameter")
    + +
    +[docs] + def dcov(self, theta, x, i): + raise ValueError("Invalid covariance function parameter")
    +
    -
    [docs]class CovSqExp(CovBase): + +
    +[docs] +class CovSqExp(CovBase): """ Ordinary squared exponential covariance function. The hyperparameters are:: @@ -202,7 +232,9 @@

    Source code for gp

         def __init__(self, x=None):
             self.n_params = 2
     
    -
    [docs] def cov(self, theta, x, z=None): +
    +[docs] + def cov(self, theta, x, z=None): self.ell = np.exp(theta[0]) self.sf2 = np.exp(2*theta[1]) @@ -213,7 +245,10 @@

    Source code for gp

             K = self.sf2 * np.exp(-R/2)
             return K
    -
    [docs] def dcov(self, theta, x, i): + +
    +[docs] + def dcov(self, theta, x, i): self.ell = np.exp(theta[0]) self.sf2 = np.exp(2*theta[1]) @@ -226,10 +261,14 @@

    Source code for gp

                 dK = 2*self.sf2 * np.exp(-R/2)
                 return dK
             else:
    -            raise ValueError("Invalid covariance function parameter")
    + raise ValueError("Invalid covariance function parameter")
    +
    + -
    [docs]class CovSqExpARD(CovBase): +
    +[docs] +class CovSqExpARD(CovBase): """ Squared exponential covariance function with ARD The hyperparameters are:: @@ -247,7 +286,9 @@

    Source code for gp

                 self.D = x.shape[1]
             self.n_params = self.D + 1
     
    -
    [docs] def cov(self, theta, x, z=None): +
    +[docs] + def cov(self, theta, x, z=None): self.ell = np.exp(theta[0:self.D]) self.sf2 = np.exp(2*theta[self.D]) @@ -259,7 +300,10 @@

    Source code for gp

             K = self.sf2*np.exp(-R/2)
             return K
    -
    [docs] def dcov(self, theta, x, i): + +
    +[docs] + def dcov(self, theta, x, i): K = self.cov(theta, x) if i < self.D: # return derivative of lengthscale parameter dK = K * squared_dist(x[:, i]/self.ell[i], x[:, i]/self.ell[i]) @@ -268,10 +312,14 @@

    Source code for gp

                 dK = 2*K
                 return dK
             else:
    -            raise ValueError("Invalid covariance function parameter")
    + raise ValueError("Invalid covariance function parameter")
    +
    -
    [docs]class CovSum(CovBase): + +
    +[docs] +class CovSum(CovBase): """ Sum of covariance functions. These are passed in as a cell array and intialised automatically. For example:: @@ -303,7 +351,9 @@

    Source code for gp

             else:
                 self.N, self.D = x.shape
     
    -
    [docs] def cov(self, theta, x, z=None): +
    +[docs] + def cov(self, theta, x, z=None): theta_offset = 0 for ci, covfunc in enumerate(self.covfuncs): try: @@ -320,7 +370,10 @@

    Source code for gp

                     K += covfunc.cov(theta_c, x, z)
             return K
    -
    [docs] def dcov(self, theta, x, i): + +
    +[docs] + def dcov(self, theta, x, i): theta_offset = 0 for covfunc in self.covfuncs: n_params_c = covfunc.get_n_params() @@ -333,14 +386,18 @@

    Source code for gp

                         dK = covfunc.dcov(theta_c, x, i)
                     else:
                         dK += covfunc.dcov(theta_c, x, i)
    -        return dK
    + return dK
    +
    + # ----------------------- # Gaussian process models # ----------------------- -
    [docs]class GPR: +
    +[docs] +class GPR: """Gaussian process regression Estimation and prediction of Gaussian process regression models @@ -407,7 +464,9 @@

    Source code for gp

             else:
                 return True
     
    -
    [docs] def post(self, hyp, covfunc, X, y): +
    +[docs] + def post(self, hyp, covfunc, X, y): """ Generic function to compute posterior distribution. """ @@ -435,7 +494,10 @@

    Source code for gp

             self.hyp = hyp
             self.covfunc = covfunc
    -
    [docs] def loglik(self, hyp, covfunc, X, y): + +
    +[docs] + def loglik(self, hyp, covfunc, X, y): """ Function to compute compute log (marginal) likelihood """ @@ -475,7 +537,10 @@

    Source code for gp

     
             return self.nlZ
    -
    [docs] def dloglik(self, hyp, covfunc, X, y): + +
    +[docs] + def dloglik(self, hyp, covfunc, X, y): """ Function to compute derivatives """ @@ -526,8 +591,11 @@

    Source code for gp

     
             return self.dnlZ
    + # model estimation (optimization) -
    [docs] def estimate(self, hyp0, covfunc, X, y, optimizer='cg'): +
    +[docs] + def estimate(self, hyp0, covfunc, X, y, optimizer='cg'): """ Function to estimate the model """ if len(X.shape) == 1: @@ -556,7 +624,10 @@

    Source code for gp

     
             return self.hyp
    -
    [docs] def predict(self, hyp, X, y, Xs): + +
    +[docs] + def predict(self, hyp, X, y, Xs): """ Function to make predictions from the model """ if len(hyp.shape) > 1: # force 1d hyperparameter array @@ -591,7 +662,9 @@

    Source code for gp

             v = solve(self.L, Ks.T)
             ys2 = kss - v.T.dot(v) + sn2
     
    -        return ymu, ys2
    + return ymu, ys2
    +
    +
    diff --git a/doc/build/html/_modules/index.html b/doc/build/html/_modules/index.html index 4cc795aa..087396d6 100644 --- a/doc/build/html/_modules/index.html +++ b/doc/build/html/_modules/index.html @@ -1,28 +1,24 @@ + + - + Overview: module code — Predictive Clinical Neuroscience Toolkit 0.20 documentation - - - + + + - - - - - - - - - + + + + + @@ -62,13 +58,24 @@

Tutorials

Other Useful Stuff

    diff --git a/doc/build/html/_modules/normative.html b/doc/build/html/_modules/normative.html index bb71c8e8..c143588b 100644 --- a/doc/build/html/_modules/normative.html +++ b/doc/build/html/_modules/normative.html @@ -1,28 +1,24 @@ + + - + normative — Predictive Clinical Neuroscience Toolkit 0.20 documentation - - - + + + - - - - - - - - - + + + + + @@ -62,13 +58,24 @@

Tutorials

Other Useful Stuff

    @@ -116,25 +123,37 @@

    Source code for normative

     #  Written by A. Marquand
     # ------------------------------------------------------------------------------
     
    -from __future__ import print_function
    -from __future__ import division
    +from __future__ import division, print_function
     
    -import os
    -import sys
    -import numpy as np
     import argparse
    -import pickle
     import glob
    +import os
    +import pickle
    +import sys
    +from pathlib import Path
     
    +import numpy as np
     from sklearn.model_selection import KFold
    -from pathlib import Path
    +
    +try:
    +    import nutpie
    +except ImportError:
    +    # warnings.warn("Nutpie not installed. For fitting HBR models with the nutpie backend, install it with `conda install nutpie numba`")
    +    pass
    +
     
     try:  # run as a package if installed
         from pcntoolkit import configs
         from pcntoolkit.dataio import fileio
         from pcntoolkit.normative_model.norm_utils import norm_init
    -    from pcntoolkit.util.utils import compute_pearsonr, CustomCV, explained_var
    -    from pcntoolkit.util.utils import compute_MSLL, scaler, get_package_versions
    +    from pcntoolkit.util.utils import (
    +        CustomCV,
    +        compute_MSLL,
    +        compute_pearsonr,
    +        explained_var,
    +        get_package_versions,
    +        scaler,
    +    )
     except ImportError:
         pass
     
    @@ -146,15 +165,22 @@ 

    Source code for normative

     
         import configs
         from dataio import fileio
    -
    -    from util.utils import compute_pearsonr, CustomCV, explained_var, compute_MSLL
    -    from util.utils import scaler, get_package_versions
         from normative_model.norm_utils import norm_init
    +    from util.utils import (
    +        CustomCV,
    +        compute_MSLL,
    +        compute_pearsonr,
    +        explained_var,
    +        get_package_versions,
    +        scaler,
    +    )
     
     PICKLE_PROTOCOL = configs.PICKLE_PROTOCOL
     
     
    -
    [docs]def load_response_vars(datafile, maskfile=None, vol=True): +
    +[docs] +def load_response_vars(datafile, maskfile=None, vol=True): """ 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 @@ -181,7 +207,10 @@

    Source code for normative

         return Y, volmask
    -
    [docs]def get_args(*args): + +
    +[docs] +def get_args(*args): """ Parse command line arguments for normative modeling @@ -197,74 +226,67 @@

    Source code for normative

         :returns configparam: Parameters controlling the estimation algorithm
         :returns kw_args: Additional keyword arguments
         """
    -
    +    args = args[0][0]
         # parse arguments
         parser = argparse.ArgumentParser(description="Normative Modeling")
    -    parser.add_argument("responses")
    -    parser.add_argument("-f", help="Function to call", dest="func",
    -                        default="estimate")
    +    parser.add_argument("respfile", help="Response variables for the normative model")
    +    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",
    -                        default=None)
    -    parser.add_argument("-k", help="cross-validation folds", dest="cvfolds",
    -                        default=None)
    -    parser.add_argument("-t", help="covariates (test data)", dest="testcov",
    -                        default=None)
    -    parser.add_argument("-r", help="responses (test data)", dest="testresp",
    -                        default=None)
    +    parser.add_argument("-c", help="covariates file", dest="covfile", default=None)
    +    parser.add_argument("-k", help="cross-validation folds", dest="cvfolds", default=None)
    +    parser.add_argument("-t", help="covariates (test data)", dest="testcov", default=None)
    +    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",
    -                        dest="configparam", default=None)
    -    # parser.add_argument('-s', action='store_false',
    -    #                 help="Flag to skip standardization.", dest="standardize")
    -    parser.add_argument("keyword_args", nargs=argparse.REMAINDER)
    +    parser.add_argument("-x", help="algorithm specific config options", dest="configparam", default=None)
    +    parsed_args, keyword_args = parser.parse_known_args(args)
     
    -    args = parser.parse_args()
    -
    -    # Process required  arguemnts
    +    # Process required arguments
         wdir = os.path.realpath(os.path.curdir)
    -    respfile = os.path.join(wdir, args.responses)
    -    if args.covfile is None:
    +    respfile = os.path.join(wdir, parsed_args.respfile)
    +    if parsed_args.covfile is None:
             raise ValueError("No covariates specified")
         else:
    -        covfile = args.covfile
    +        covfile = parsed_args.covfile
     
         # Process optional arguments
    -    if args.maskfile is None:
    +    if parsed_args.maskfile is None:
             maskfile = None
         else:
    -        maskfile = os.path.join(wdir, args.maskfile)
    -    if args.testcov is None and args.cvfolds is not None:
    +        maskfile = os.path.join(wdir, parsed_args.maskfile)
    +    if parsed_args.testcov is None and parsed_args.cvfolds is not None:
             testcov = None
             testresp = None
    -        cvfolds = int(args.cvfolds)
    +        cvfolds = int(parsed_args.cvfolds)
             print("Running under " + str(cvfolds) + " fold cross-validation.")
         else:
             print("Test covariates specified")
    -        testcov = args.testcov
    +        testcov = parsed_args.testcov
             cvfolds = None
    -        if args.testresp is None:
    +        if parsed_args.testresp is None:
                 testresp = None
                 print("No test response variables specified")
             else:
    -            testresp = args.testresp
    -        if args.cvfolds is not None:
    +            testresp = parsed_args.testresp
    +        if parsed_args.cvfolds is not None:
                 print("Ignoring cross-valdation specification (test data given)")
     
         # Process addtional keyword arguments. These are always added as strings
         kw_args = {}
    -    for kw in args.keyword_args:
    +    for kw in keyword_args:
             kw_arg = kw.split('=')
     
             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, parsed_args.func, parsed_args.alg, \ + parsed_args.configparam, kw_args
    + -
    [docs]def evaluate(Y, Yhat, S2=None, mY=None, sY=None, nlZ=None, nm=None, Xz_tr=None, alg=None, +
    +[docs] +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']): ''' Compute error metrics This function will compute error metrics based on a set of predictions Yhat @@ -349,7 +371,10 @@

    Source code for normative

         return results
    -
    [docs]def save_results(respfile, Yhat, S2, maskvol, Z=None, Y=None, outputsuffix=None, + +
    +[docs] +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. @@ -406,7 +431,10 @@

    Source code for normative

                                 example=exfile, mask=maskvol)
    -
    [docs]def estimate(covfile, respfile, **kwargs): + +
    +[docs] +def estimate(covfile, respfile, **kwargs): """ Estimate a normative model This will estimate a model in one of two settings according to @@ -474,7 +502,9 @@

    Source code for normative

         # '_' is in the outputsuffix to
         # avoid file name parsing problem.
         inscaler = kwargs.pop('inscaler', 'None')
    +    print(f"inscaler: {inscaler}")
         outscaler = kwargs.pop('outscaler', 'None')
    +    print(f"outscaler: {outscaler}")
         warp = kwargs.get('warp', None)
     
         # convert from strings if necessary
    @@ -627,7 +657,8 @@ 

    Source code for normative

                         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)
    @@ -730,7 +761,10 @@ 

    Source code for normative

             return output
    -
    [docs]def fit(covfile, respfile, **kwargs): + +
    +[docs] +def fit(covfile, respfile, **kwargs): """ Fits a normative model to the data. @@ -756,6 +790,8 @@

    Source code for normative

         outputsuffix = "_" + outputsuffix.replace("_", "")
         inscaler = kwargs.pop('inscaler', 'None')
         outscaler = kwargs.pop('outscaler', 'None')
    +    print(f"inscaler: {inscaler}")
    +    print(f"outscaler: {outscaler}")
     
         if savemodel and not os.path.isdir('Models'):
             os.mkdir('Models')
    @@ -824,7 +860,10 @@ 

    Source code for normative

         return nm
    -
    [docs]def predict(covfile, respfile, maskfile=None, **kwargs): + +
    +[docs] +def predict(covfile, respfile, maskfile=None, **kwargs): ''' Make predictions on the basis of a pre-estimated normative model If only the covariates are specified then only predicted mean and variance @@ -905,6 +944,10 @@

    Source code for normative

         X = fileio.load(covfile)
         if len(X.shape) == 1:
             X = X[:, np.newaxis]
    +    if respfile is not None:
    +        Y, maskvol = load_response_vars(respfile, maskfile)
    +        if len(Y.shape) == 1:
    +            Y = Y[:, np.newaxis]
     
         sample_num = X.shape[0]
         if models is not None:
    @@ -922,9 +965,13 @@ 

    Source code for normative

             Xz = scaler_cov[fold].transform(X)
         else:
             Xz = X
    +    if respfile is not None:
    +        if outscaler in ['standardize', 'minmax', 'robminmax']:
    +            Yz = scaler_resp[fold].transform(Y)
    +        else:
    +            Yz = Y
     
         # 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)
    @@ -947,6 +994,10 @@ 

    Source code for normative

             else:
                 Yhat[:, i] = yhat.squeeze()
                 S2[:, i] = s2.squeeze()
    +        if respfile is not None:
    +            if alg == 'hbr':
    +                # Z scores for HBR must be computed independently for each model
    +                Z[:, i] = nm.get_mcmc_zscores(Xz, Yz[:, i:i+1], **kwargs)
     
         if respfile is None:
             save_results(None, Yhat, S2, None, outputsuffix=outputsuffix)
    @@ -954,7 +1005,6 @@ 

    Source code for normative

             return (Yhat, S2)
     
         else:
    -        Y, maskvol = load_response_vars(respfile, maskfile)
             if models is not None and len(Y.shape) > 1:
                 Y = Y[:, models]
                 if meta_data:
    @@ -986,7 +1036,9 @@ 

    Source code for normative

             else:
                 warp = False
     
    -        Z = (Y - Yhat) / np.sqrt(S2)
    +        if alg != 'hbr':
    +            # For HBR the Z scores are already computed
    +            Z = (Y - Yhat) / np.sqrt(S2)
     
             print("Evaluating the model ...")
             if meta_data and not warp:
    @@ -1008,7 +1060,10 @@ 

    Source code for normative

                 return (Yhat, S2, Z)
    -
    [docs]def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, + +
    +[docs] +def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, **kwargs): ''' Transfer learning on the basis of a pre-estimated normative model by using @@ -1044,14 +1099,14 @@

    Source code for normative

             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())):
    +    elif ('model_path' not in list(kwargs.keys())) or \
    +            ('trbefile' not 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':
    -        if (not 'output_path' in list(kwargs.keys())):
    +        if ('output_path' not in list(kwargs.keys())):
                 print('InputError: Some mandatory arguments for hbr are missing.')
                 return
             else:
    @@ -1063,7 +1118,7 @@ 

    Source code for normative

         # or (testresp==None)
         elif alg == 'blr':
             if (testcov == None) or \
    -                (not 'tsbefile' in list(kwargs.keys())):
    +                ('tsbefile' not in list(kwargs.keys())):
                 print('InputError: Some mandatory arguments for blr are missing.')
                 return
         # general arguments
    @@ -1266,7 +1321,10 @@ 

    Source code for normative

             Path(done_path).touch()
    -
    [docs]def extend(covfile, respfile, maskfile=None, **kwargs): + +
    +[docs] +def extend(covfile, respfile, maskfile=None, **kwargs): ''' This function extends an existing HBR model with data from new sites/scanners. @@ -1299,9 +1357,9 @@

    Source code for normative

         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())):
    +    elif ('model_path' not in list(kwargs.keys())) or \
    +        ('output_path' not in list(kwargs.keys())) or \
    +            ('trbefile' not in list(kwargs.keys())):
             print('InputError: Some mandatory arguments are missing.')
             return
         else:
    @@ -1376,7 +1434,10 @@ 

    Source code for normative

                                      str(i) + outputsuffix + '.pkl'))
    -
    [docs]def tune(covfile, respfile, maskfile=None, **kwargs): + +
    +[docs] +def tune(covfile, respfile, maskfile=None, **kwargs): ''' This function tunes an existing HBR model with real data. @@ -1410,9 +1471,9 @@

    Source code for normative

         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())):
    +    elif ('model_path' not in list(kwargs.keys())) or \
    +        ('output_path' not in list(kwargs.keys())) or \
    +            ('trbefile' not in list(kwargs.keys())):
             print('InputError: Some mandatory arguments are missing.')
             return
         else:
    @@ -1487,7 +1548,10 @@ 

    Source code for normative

                                      str(i) + outputsuffix + '.pkl'))
    -
    [docs]def merge(covfile=None, respfile=None, **kwargs): + +
    +[docs] +def merge(covfile=None, respfile=None, **kwargs): ''' This function extends an existing HBR model with data from new sites/scanners. @@ -1518,9 +1582,9 @@

    Source code for normative

         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())):
    +    elif ('model_path1' not in list(kwargs.keys())) or \
    +        ('model_path2' not in list(kwargs.keys())) or \
    +            ('output_path' not in list(kwargs.keys())):
             print('InputError: Some mandatory arguments are missing.')
             return
         else:
    @@ -1591,7 +1655,10 @@ 

    Source code for normative

                                             str(i) + outputsuffix + '.pkl'))
    -
    [docs]def main(*args): + +
    +[docs] +def main(*args): """ Parse arguments and estimate model """ @@ -1620,6 +1687,13 @@

    Source code for normative

         exec(func + '(' + all_args + ')')
    +
    +[docs] +def entrypoint(): + main(sys.argv[1:])
    + + + # For running from the command line: if __name__ == "__main__": main(sys.argv[1:]) diff --git a/doc/build/html/_modules/normative_parallel.html b/doc/build/html/_modules/normative_parallel.html index 1cb6ed98..bcd334f9 100644 --- a/doc/build/html/_modules/normative_parallel.html +++ b/doc/build/html/_modules/normative_parallel.html @@ -1,28 +1,24 @@ + + - + normative_parallel — Predictive Clinical Neuroscience Toolkit 0.20 documentation - - - + + + - - - - - - - - - + + + + + @@ -62,13 +58,24 @@

Tutorials

Other Useful Stuff

    @@ -135,7 +142,8 @@

    Source code for normative_parallel

     import time
     import numpy as np
     import pandas as pd
    -from subprocess import call, check_output
    +from datetime import datetime
    +from subprocess import run, check_output
     
     
     try:
    @@ -157,7 +165,9 @@ 

    Source code for normative_parallel

     PICKLE_PROTOCOL = configs.PICKLE_PROTOCOL
     
     
    -
    [docs]def execute_nm(processing_dir, +
    +[docs] +def execute_nm(processing_dir, python_path, job_name, covfile_path, @@ -192,6 +202,7 @@

    Source code for normative_parallel

         :param testrespfile_path: Full path to a .txt file that contains all test features
         :param log_path: Path for saving log files
         :param binary: If True uses binary format for response file otherwise it is text
    +    :param cluster_spec: 'torque' for PBS Torque and 'slurm' for Slurm clusters. 
         :param interactive: If False (default) the user should manually 
                             rerun the failed jobs or collect the results.
                             If 'auto' the job status are checked until all 
    @@ -211,10 +222,11 @@ 

    Source code for normative_parallel

         cv_folds = kwargs.get('cv_folds', None)
         testcovfile_path = kwargs.get('testcovfile_path', None)
         testrespfile_path = kwargs.get('testrespfile_path', None)
    -    outputsuffix = kwargs.get('outputsuffix', 'estimate')
    +    outputsuffix = kwargs.get('outputsuffix', '_estimate')
         cluster_spec = kwargs.pop('cluster_spec', 'torque')
         log_path = kwargs.get('log_path', None)
         binary = kwargs.pop('binary', False)
    +    cores = kwargs.pop('n_cores_per_batch','1')
     
         split_nm(processing_dir,
                  respfile_path,
    @@ -234,6 +246,8 @@ 

    Source code for normative_parallel

     
         kwargs.update({'batch_size': str(batch_size)})
         job_ids = []
    +    start_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
    +
         for n in range(1, number_of_batches+1):
             kwargs.update({'job_id': str(n)})
             if testrespfile_path is not None:
    @@ -266,9 +280,10 @@ 

    Source code for normative_parallel

                         job_id = qsub_nm(job_path=batch_job_path,
                                          log_path=log_path,
                                          memory=memory,
    -                                     duration=duration)
    +                                     duration=duration,
    +                                     cores=cores)
                         job_ids.append(job_id)
    -                elif cluster_spec == 'sbatch':
    +                elif cluster_spec == 'slurm':
                         # update the response file
                         kwargs.update({'testrespfile_path':
                                        batch_testrespfile_path})
    @@ -282,8 +297,10 @@ 

    Source code for normative_parallel

                                       memory=memory,
                                       duration=duration,
                                       **kwargs)
    -                    sbatch_nm(job_path=batch_job_path,
    -                              log_path=log_path)
    +
    +                    job_id = sbatch_nm(job_path=batch_job_path)
    +                    job_ids.append(job_id)
    +
                     elif cluster_spec == 'new':
                         # this part requires addition in different envioronment [
                         sbatchwrap_nm(processing_dir=batch_processing_dir,
    @@ -310,9 +327,10 @@ 

    Source code for normative_parallel

                         job_id = qsub_nm(job_path=batch_job_path,
                                          log_path=log_path,
                                          memory=memory,
    -                                     duration=duration)
    +                                     duration=duration, 
    +                                     cores=cores)
                         job_ids.append(job_id)
    -                elif cluster_spec == 'sbatch':
    +                elif cluster_spec == 'slurm':
                         sbatchwrap_nm(batch_processing_dir,
                                       python_path,
                                       normative_path,
    @@ -323,8 +341,9 @@ 

    Source code for normative_parallel

                                       memory=memory,
                                       duration=duration,
                                       **kwargs)
    -                    sbatch_nm(job_path=batch_job_path,
    -                              log_path=log_path)
    +
    +                    job_id = sbatch_nm(job_path=batch_job_path)
    +                    job_ids.append(job_id)
                     elif cluster_spec == 'new':
                         # this part requires addition in different envioronment [
                         bashwrap_nm(processing_dir=batch_processing_dir, func=func,
    @@ -352,9 +371,10 @@ 

    Source code for normative_parallel

                         job_id = qsub_nm(job_path=batch_job_path,
                                          log_path=log_path,
                                          memory=memory,
    -                                     duration=duration)
    +                                     duration=duration,
    +                                     cores=cores)
                         job_ids.append(job_id)
    -                elif cluster_spec == 'sbatch':
    +                elif cluster_spec == 'slurm':
                         sbatchwrap_nm(batch_processing_dir,
                                       python_path,
                                       normative_path,
    @@ -365,8 +385,10 @@ 

    Source code for normative_parallel

                                       memory=memory,
                                       duration=duration,
                                       **kwargs)
    -                    sbatch_nm(job_path=batch_job_path,
    -                              log_path=log_path)
    +
    +                    job_id = sbatch_nm(job_path=batch_job_path)
    +                    job_ids.append(job_id)
    +
                     elif cluster_spec == 'new':
                         # this part requires addition in different envioronment [
                         bashwrap_nm(processing_dir=batch_processing_dir, func=func,
    @@ -376,7 +398,7 @@ 

    Source code for normative_parallel

     
         if interactive:
     
    -        check_jobs(job_ids, delay=60)
    +        check_jobs(job_ids, cluster_spec, start_time, delay=60)
     
             success = False
             while (not success):
    @@ -393,16 +415,33 @@ 

    Source code for normative_parallel

                     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,
    -                                 interactive=interactive)
    +                        if cluster_spec == 'torque':
    +                            rerun_nm(processing_dir, log_path=log_path, memory=memory,
    +                                     duration=duration, binary=binary,
    +                                     interactive=interactive, cores=cores)
    +                        elif cluster_spec == 'slurm':
    +                            sbatchrerun_nm(processing_dir,
    +                                           memory=memory,
    +                                           duration=duration,
    +                                           binary=binary,
    +                                           log_path=log_path,
    +                                           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)
    +                    if cluster_spec == 'torque':
    +                        rerun_nm(processing_dir, log_path=log_path, memory=memory,
    +                                 duration=duration, binary=binary,
    +                                 interactive=interactive, cores=cores)
    +                    elif cluster_spec == 'slurm':
    +                        sbatchrerun_nm(processing_dir,
    +                                       memory=memory,
    +                                       duration=duration,
    +                                       binary=binary,
    +                                       log_path=log_path,
    +                                       interactive=interactive)
     
             if interactive == 'query':
                 response = yes_or_no('Collect the results?')
    @@ -425,10 +464,13 @@ 

    Source code for normative_parallel

                                      outputsuffix=outputsuffix)
    + """routines that are environment independent""" -
    [docs]def split_nm(processing_dir, +
    +[docs] +def split_nm(processing_dir, respfile_path, batch_size, binary, @@ -543,7 +585,10 @@

    Source code for normative_parallel

                                              protocol=PICKLE_PROTOCOL)
    -
    [docs]def collect_nm(processing_dir, + +
    +[docs] +def collect_nm(processing_dir, job_name, func='estimate', collect=False, @@ -581,19 +626,15 @@

    Source code for normative_parallel

         batch_fail = []
     
         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).
    -        for batch in batches:
    -            if file_example == []:
    -                file_example = glob.glob(batch + 'yhat' + outputsuffix
    +        files = glob.glob(processing_dir + 'batch_*/' + 'yhat' + outputsuffix
                                              + file_extentions)
    -            else:
    -                break
    -        if binary is False:
    -            file_example = fileio.load(file_example[0])
    +        if len(files) > 0:
    +            file_example = fileio.load(files[0])
             else:
    -            file_example = pd.read_pickle(file_example[0])
    +            raise ValueError(f"Missing output files (yhats at: {processing_dir + 'batch_*/' + 'yhat' + outputsuffix + file_extentions}")
    +
             numsubjects = file_example.shape[0]
             try:
                 # doesn't exist if size=1, and txt file
    @@ -947,7 +988,10 @@ 

    Source code for normative_parallel

             return False
    -
    [docs]def delete_nm(processing_dir, + +
    +[docs] +def delete_nm(processing_dir, binary=False): '''This function deletes all processing for normative modelling and just keeps the combined output. @@ -971,11 +1015,14 @@

    Source code for normative_parallel

             os.remove(processing_dir + 'failed_batches' + file_extentions)
    + # all routines below are envronment dependent and require adaptation in novel # environments -> copy those routines and adapt them in accrodance with your # environment -
    [docs]def bashwrap_nm(processing_dir, +
    +[docs] +def bashwrap_nm(processing_dir, python_path, normative_path, job_name, @@ -1036,7 +1083,7 @@

    Source code for normative_parallel

             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 testrespfile_path or cvfold
                   must be specified.""")
     
         # add algorithm-specific parameters
    @@ -1065,10 +1112,14 @@ 

    Source code for normative_parallel

         os.chmod(processing_dir + job_name, 0o770)
    -
    [docs]def qsub_nm(job_path, + +
    +[docs] +def qsub_nm(job_path, log_path, memory, - duration): + duration, + cores): '''This function submits a job.sh scipt to the torque custer using the qsub command. Basic usage:: @@ -1088,10 +1139,10 @@

    Source code for normative_parallel

         # created qsub command
         if log_path is None:
             qsub_call = ['echo ' + job_path + ' | qsub -N ' + job_path + ' -l ' +
    -                     'procs=1' + ',mem=' + memory + ',walltime=' + duration]
    +                     'nodes=1:ppn='+ cores + ',mem=' + memory + ',walltime=' + duration]
         else:
             qsub_call = ['echo ' + job_path + ' | qsub -N ' + job_path +
    -                     ' -l ' + 'procs=1' + ',mem=' + memory + ',walltime=' +
    +                     ' -l ' + 'nodes=1:ppn='+ cores + ',mem=' + memory + ',walltime=' +
                          duration + ' -o ' + log_path + ' -e ' + log_path]
     
         # submits job to cluster
    @@ -1102,10 +1153,15 @@ 

    Source code for normative_parallel

         return job_id
    -
    [docs]def rerun_nm(processing_dir, + +
    +[docs] +def rerun_nm(processing_dir, log_path, memory, duration, + cluster_spec, + cores, binary=False, interactive=False): '''This function reruns all failed batched in processing_dir after collect_nm has identified the failed batches. @@ -1133,7 +1189,8 @@

    Source code for normative_parallel

                 job_id = qsub_nm(job_path=jobpath,
                                  log_path=log_path,
                                  memory=memory,
    -                             duration=duration)
    +                             duration=duration,
    +                             cores=cores)
                 job_ids.append(job_id)
         else:
             file_extentions = '.txt'
    @@ -1146,17 +1203,21 @@ 

    Source code for normative_parallel

                 job_id = qsub_nm(job_path=jobpath,
                                  log_path=log_path,
                                  memory=memory,
    -                             duration=duration)
    +                             duration=duration,
    +                             cores=cores)
                 job_ids.append(job_id)
     
         if interactive:
    -        check_jobs(job_ids, delay=60)
    + check_jobs(job_ids, cluster_spec, delay=60)
    + # COPY the rotines above here and aadapt those to your cluster # bashwarp_nm; qsub_nm; rerun_nm -
    [docs]def sbatchwrap_nm(processing_dir, +
    +[docs] +def sbatchwrap_nm(processing_dir, python_path, normative_path, job_name, @@ -1164,6 +1225,7 @@

    Source code for normative_parallel

                       respfile_path,
                       memory,
                       duration,
    +                  log_path,
                       func='estimate',
                       **kwargs):
         '''This function wraps normative modelling into a bash script to run it
    @@ -1202,14 +1264,15 @@ 

    Source code for normative_parallel

         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_jobname = '#SBATCH --job-name=' + job_name + '\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_log_out = '#SBATCH -o ' + log_path + '%x_%j.out' + '\n'
    +    sbatch_log_error = '#SBATCH -e ' + log_path + '%x_%j.err' + '\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 "
    @@ -1217,12 +1280,13 @@ 

    Source code for normative_parallel

     
         bash_environment = [sbatch_init +
                             sbatch_jobname +
    -                        sbatch_account +
                             sbatch_nodes +
                             sbatch_tasks +
                             sbatch_time +
    -                        sbatch_module +
    -                        sbatch_anaconda]
    +                        sbatch_memory +
    +                        sbatch_log_out +
    +                        sbatch_log_error
    +                        ]
     
         # creates call of function for normative modelling
         if (testrespfile_path is not None) and (testcovfile_path is not None):
    @@ -1239,7 +1303,7 @@ 

    Source code for normative_parallel

             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 testrespfile_path or cv_folds
                   must be specified.""")
     
         # add algorithm-specific parameters
    @@ -1268,19 +1332,20 @@ 

    Source code for normative_parallel

         os.chmod(processing_dir + job_name, 0o770)
    -
    [docs]def sbatch_nm(job_path, - log_path): + +
    +[docs] +def sbatch_nm(job_path): '''This function submits a job.sh scipt to the torque custer using the qsub command. Basic usage:: - sbatch_nm(job_path, log_path) + sbatch_nm(job_path) :param job_path: Full path to the job.sh file - :param log_path: The logs are currently stored in the working dir - :outputs: Submission of the job to the (torque) cluster. + :outputs: Submission of the job to the slurm cluster. written by (primarily) T Wolfers, (adapted) S Rutherford. ''' @@ -1289,15 +1354,22 @@

    Source code for normative_parallel

         sbatch_call = ['sbatch ' + job_path]
     
         # submits job to cluster
    -    call(sbatch_call, shell=True)
    + job_id = check_output(sbatch_call, shell=True).decode( + sys.stdout.encoding).replace("\n", "") + + return job_id
    -
    [docs]def sbatchrerun_nm(processing_dir, + +
    +[docs] +def sbatchrerun_nm(processing_dir, memory, duration, new_memory=False, new_duration=False, binary=False, + interactive=False, **kwargs): '''This function reruns all failed batched in processing_dir after collect_nm has identified he failed batches. @@ -1315,7 +1387,12 @@

    Source code for normative_parallel

     
          written by (primarily) T Wolfers, (adapted) S Rutherford.
         '''
    -    log_path = kwargs.pop('log_path', None)
    +
    +    # log_path = kwargs.pop('log_path', None)
    +
    +    job_ids = []
    +
    +    start_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
     
         if binary:
             file_extentions = '.pkl'
    @@ -1329,11 +1406,12 @@ 

    Source code for normative_parallel

                     with fileinput.FileInput(jobpath, inplace=True) as file:
                         for line in file:
                             print(line.replace(duration, new_duration), end='')
    -                if new_memory != False:
    -                    with fileinput.FileInput(jobpath, inplace=True) as file:
    -                        for line in file:
    -                            print(line.replace(memory, new_memory), end='')
    -                sbatch_nm(jobpath, log_path)
    +            if new_memory != False:
    +                with fileinput.FileInput(jobpath, inplace=True) as file:
    +                    for line in file:
    +                        print(line.replace(memory, new_memory), end='')
    +            job_id = sbatch_nm(jobpath)
    +            job_ids.append(job_id)
     
         else:
             file_extentions = '.txt'
    @@ -1347,73 +1425,124 @@ 

    Source code for normative_parallel

                     with fileinput.FileInput(jobpath, inplace=True) as file:
                         for line in file:
                             print(line.replace(duration, new_duration), end='')
    -                if new_memory != False:
    -                    with fileinput.FileInput(jobpath, inplace=True) as file:
    -                        for line in file:
    -                            print(line.replace(memory, new_memory), end='')
    -                sbatch_nm(jobpath,
    -                          log_path)
    + if new_memory != False: + with fileinput.FileInput(jobpath, inplace=True) as file: + for line in file: + print(line.replace(memory, new_memory), end='') + job_id = sbatch_nm(jobpath) + job_ids.append(job_id) + + if interactive: + check_jobs(job_ids, cluster_spec='slurm', + start_time=start_time, delay=60)
    + -
    [docs]def retrieve_jobs(): +
    +[docs] +def retrieve_jobs(cluster_spec, start_time=None): """ A utility function to retrieve task status from the outputs of qstat. + :param cluster_spec: type of cluster, either 'torque' or 'slurm'. + :return: a dictionary of jobs. """ - output = check_output('qstat', shell=True).decode(sys.stdout.encoding) - output = output.split('\n') - jobs = dict() - for line in output[2:-1]: - (Job_ID, Job_Name, User, Wall_Time, Status, Queue) = line.split() - jobs[Job_ID] = dict() - jobs[Job_ID]['name'] = Job_Name - jobs[Job_ID]['walltime'] = Wall_Time - jobs[Job_ID]['status'] = Status + if cluster_spec == 'torque': + + output = check_output('qstat', shell=True).decode(sys.stdout.encoding) + output = output.split('\n') + jobs = dict() + for line in output[2:-1]: + (Job_ID, Job_Name, User, Wall_Time, Status, Queue) = line.split() + jobs[Job_ID] = dict() + jobs[Job_ID]['name'] = Job_Name + jobs[Job_ID]['walltime'] = Wall_Time + jobs[Job_ID]['status'] = Status + + elif cluster_spec == 'slurm': + + end_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") + cmd = ['sacct', '-n', '-X', '--parsable2', '--noheader', + '-S', start_time, '-E', end_time, '--format=JobName,State'] + jobs = run(cmd, capture_output=True, text=True) return jobs
    -
    [docs]def check_job_status(jobs): + +
    +[docs] +def check_job_status(jobs, cluster_spec, start_time=None): """ 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 + :param cluster_spec: type of cluster, either 'torque' or 'slurm'. + :return returns the number of taks athat are queued, running, completed etc """ - running_jobs = retrieve_jobs() + running_jobs = retrieve_jobs(cluster_spec, start_time) r = 0 c = 0 q = 0 u = 0 - for job in jobs: - try: - if running_jobs[job]['status'] == 'C': + + if cluster_spec == 'torque': + + for job in jobs: + try: + if running_jobs[job]['status'] == 'C': + c += 1 + elif running_jobs[job]['status'] == 'Q': + q += 1 + elif running_jobs[job]['status'] == 'R': + r += 1 + else: + u += 1 + except: # probably meanwhile the job is finished. c += 1 - elif running_jobs[job]['status'] == 'Q': - q += 1 - elif running_jobs[job]['status'] == 'R': - r += 1 - else: - u += 1 - except: # probably meanwhile the job is finished. - c += 1 - continue + continue + + print('Total Jobs:%d, Queued:%d, Running:%d, Completed:%d, Unknown:%d' + % (len(jobs), q, r, c, u)) + + elif cluster_spec == 'slurm': + + lines = running_jobs.stdout.strip().split('\n') + + for line in lines: + if line: + parts = line.split('|') + if len(parts) >= 2: + job_name, state = parts[0], parts[1] + if state == 'PENDING': + q += 1 + elif state == 'RUNNING': + r += 1 + elif state == 'COMPLETED': + c += 1 + elif state == 'FAILED': + u += 1 + + print('Total Jobs:%d, Pending:%d, Running:%d, Completed:%d, Failed:%d' + % (len(jobs), 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
    -
    [docs]def check_jobs(jobs, delay=60): + +
    +[docs] +def check_jobs(jobs, cluster_spec, start_time=None, delay=60): """ A utility function for chacking the status of submitted jobs. :param jobs: list of job ids. + :param cluster_spec: type of cluster, either 'torque' or 'slurm'. :param delay: the delay (in sec) between two consequative checks, defaults to 60. """ @@ -1421,11 +1550,12 @@

    Source code for normative_parallel

         n = len(jobs)
     
         while (True):
    -        q, r, c, u = check_job_status(jobs)
    +        q, r, c, u = check_job_status(jobs, cluster_spec, start_time)
             if c == n:
                 print('All jobs are completed!')
                 break
             time.sleep(delay)
    +
    diff --git a/doc/build/html/_modules/rfa.html b/doc/build/html/_modules/rfa.html index 986a44c9..0ea213e2 100644 --- a/doc/build/html/_modules/rfa.html +++ b/doc/build/html/_modules/rfa.html @@ -1,28 +1,24 @@ + + - + rfa — Predictive Clinical Neuroscience Toolkit 0.20 documentation - - - + + + - - - - - - - - - + + + + + @@ -62,13 +58,24 @@

Tutorials

Other Useful Stuff

    @@ -110,7 +117,9 @@

    Source code for rfa

     import torch
     
     
    -
    [docs]class GPRRFA: +
    +[docs] +class GPRRFA: """Random Feature Approximation for Gaussian Process Regression Estimation and prediction of Bayesian linear regression models @@ -191,11 +200,16 @@

    Source code for rfa

     
             return X, y, hyp
     
    -
    [docs] def get_n_params(self, X): +
    +[docs] + def get_n_params(self, X): return X.shape[1] + 2
    -
    [docs] def post(self, hyp, X, y): + +
    +[docs] + def post(self, hyp, X, y): """ Generic function to compute posterior distribution. This function will save the posterior mean and precision matrix as @@ -240,7 +254,10 @@

    Source code for rfa

             if hasattr(self, '_iterations'):
                 self._iterations += 1
    -
    [docs] def loglik(self, hyp, X, y): + +
    +[docs] + def loglik(self, hyp, X, y): """ Function to compute compute log (marginal) likelihood """ X, y, hyp = self._numpy2torch(X, y, hyp) @@ -275,14 +292,20 @@

    Source code for rfa

             self.nlZ = nlZ
             return nlZ
    -
    [docs] def dloglik(self, hyp, X, y): + +
    +[docs] + def dloglik(self, hyp, X, y): """ Function to compute derivatives """ print("derivatives not available") return
    -
    [docs] def estimate(self, hyp0, X, y, optimizer='lbfgs'): + +
    +[docs] + def estimate(self, hyp0, X, y, optimizer='lbfgs'): """ Function to estimate the model """ if type(hyp0) is torch.Tensor: @@ -324,7 +347,10 @@

    Source code for rfa

     
             return self.hyp.detach().numpy()
    -
    [docs] def predict(self, hyp, X, y, Xs): + +
    +[docs] + def predict(self, hyp, X, y, Xs): """ Function to make predictions from the model """ X, y, hyp = self._numpy2torch(X, y, hyp) @@ -347,7 +373,9 @@

    Source code for rfa

                 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()
    + return ys.detach().numpy().squeeze(), s2.detach().numpy().squeeze()
    +
    +
    diff --git a/doc/build/html/_modules/trendsurf.html b/doc/build/html/_modules/trendsurf.html index d01ca0b3..1470dd83 100644 --- a/doc/build/html/_modules/trendsurf.html +++ b/doc/build/html/_modules/trendsurf.html @@ -1,28 +1,24 @@ + + - + trendsurf — Predictive Clinical Neuroscience Toolkit 0.20 documentation - - - + + + - - - - - - - - - + + + + + @@ -62,13 +58,24 @@

Tutorials

Other Useful Stuff

    @@ -135,7 +142,9 @@

    Source code for trendsurf

         from model.bayesreg import BLR
     
     
    -
    [docs]def load_data(datafile, maskfile=None): +
    +[docs] +def load_data(datafile, maskfile=None): """ Load data from disk @@ -179,7 +188,10 @@

    Source code for trendsurf

         return dat, world, mask
    -
    [docs]def create_basis(X, basis, mask): + +
    +[docs] +def create_basis(X, basis, mask): """ Create a basis set @@ -223,7 +235,10 @@

    Source code for trendsurf

         return Phi
    -
    [docs]def write_nii(data, filename, examplenii, mask): + +
    +[docs] +def write_nii(data, filename, examplenii, mask): """ Write data to nifti file @@ -251,7 +266,10 @@

    Source code for trendsurf

         nib.save(array_img, filename)
    -
    [docs]def get_args(*args): + +
    +[docs] +def get_args(*args): """ Parse command line arguments @@ -293,7 +311,10 @@

    Source code for trendsurf

         return filename, maskfile, basis, args.a, args.o
    -
    [docs]def estimate(filename, maskfile, basis, ard=False, outputall=False, + +
    +[docs] +def estimate(filename, maskfile, basis, ard=False, outputall=False, saveoutput=True, **kwargs): """ Estimate a trend surface model @@ -406,13 +427,17 @@

    Source code for trendsurf

             return out
    -
    [docs]def main(*args): + +
    +[docs] +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/doc/build/html/_sources/pages/BLR_normativemodel_protocol.rst.txt b/doc/build/html/_sources/pages/BLR_normativemodel_protocol.rst.txt index 479b5163..4c23523b 100644 --- a/doc/build/html/_sources/pages/BLR_normativemodel_protocol.rst.txt +++ b/doc/build/html/_sources/pages/BLR_normativemodel_protocol.rst.txt @@ -1,27 +1,22 @@ -.. title:: BLR tutorial +`Predictive Clinical Neuroscience Toolkit `__ +====================================================================================== -Bayesian Linear Regression +The Normative Modeling Framework for Computational Psychiatry Protocol ====================================================================== -The Normative Modeling Framework for Computational Psychiatry. Nature Protocols. https://www.nature.com/articles/s41596-022-00696-5. +Using Bayesian Linear Regression and Multi-Site Cortical Thickness Data +----------------------------------------------------------------------- Created by `Saige Rutherford `__ -Using Multi-Site Cortical Thickness Data - -.. image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/predictive-clinical-neuroscience/PCNtoolkit-demo/blob/main/tutorials/BLR_protocol/BLR_normativemodel_protocol.ipynb - - -.. figure:: ./blr_fig2.png - :height: 400px - :align: center - Data Preparation ---------------------------------------------- +================ Install necessary libraries & grab data files -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------------------- + +Step 1. +~~~~~~~ Begin by cloning the GitHub repository using the following commands. This repository contains the necessary code and example data. Then @@ -33,33 +28,28 @@ your computer). ! git clone https://github.com/predictive-clinical-neuroscience/PCNtoolkit-demo.git - -.. parsed-literal:: - - Cloning into 'PCNtoolkit-demo'... - remote: Enumerating objects: 855, done. - remote: Counting objects: 100% (855/855), done. - remote: Compressing objects: 100% (737/737), done. - remote: Total 855 (delta 278), reused 601 (delta 101), pack-reused 0 - Receiving objects: 100% (855/855), 18.07 MiB | 13.53 MiB/s, done. - Resolving deltas: 100% (278/278), done. - - .. code:: ipython3 import os # set this path to the git cloned PCNtoolkit-demo repository --> Uncomment whichever line you need for either running on your own computer or on Google Colab. - #os.chdir('/Users/PCNtoolkit-demo/tutorials/BLR_protocol') # if running on your own computer, use this line (change the path to match where you cloned the repository) - os.chdir('/content/PCNtoolkit-demo/tutorials/BLR_protocol') # if running on Google Colab, use this line + #wdir = '/PCNtoolkit-demo' # if running on your own computer, use this line (change the path to match where you cloned the repository) + wdir ='/content/PCNtoolkit-demo' # if running on Google Colab, use this line + + os.chdir(os.path.join(wdir,'tutorials','BLR_protocol')) + .. code:: ipython3 + ! pip install nutpie + ! pip install pcntoolkit ! pip install -r requirements.txt - Prepare covariate data -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------- + +Step 2. +~~~~~~~ The data set (downloaded in Step 1) includes a multi-site dataset from the `Human Connectome Project Young Adult @@ -88,8 +78,8 @@ depending on the research question. .. code:: ipython3 # if running in Google colab, remove the "data/" folder from the path - hcp = pd.read_csv('/content/PCNtoolkit-demo/data/HCP1200_age_gender.csv') - ixi = pd.read_csv('/content/PCNtoolkit-demo/data/IXI_age_gender.csv') + hcp = pd.read_csv(os.path.join(wdir,'data','HCP1200_age_gender.csv')) + ixi = pd.read_csv(os.path.join(wdir,'data','IXI_age_gender.csv')) .. code:: ipython3 @@ -98,8 +88,8 @@ depending on the research question. .. parsed-literal:: - /usr/local/lib/python3.7/dist-packages/pandas/core/reshape/merge.py:1218: UserWarning: You are merging on int and float columns where the float values are not equal to their int representation - UserWarning, + :1: UserWarning: You are merging on int and float columns where the float values are not equal to their int representation. + cov = pd.merge(hcp, ixi, on=["participant_id", "age", "sex", "site"], how='outer') .. code:: ipython3 @@ -108,29 +98,348 @@ depending on the research question. .. code:: ipython3 - sns.displot(cov, x="age", hue="site", multiple="stack", height=6) + sns.displot(cov, x="age", hue="site", multiple="stack", height=6); +.. image:: BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_16_0.png -.. parsed-literal:: - +.. code:: ipython3 + cov.groupby(['site']).describe() -.. image:: BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_15_1.png +.. raw:: html + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    agesex
    countmeanstdmin25%50%75%maxcountmeanstdmin25%50%75%max
    site
    hcp1206.028.8374793.69053422.00000026.00000029.0000032.00000037.000001206.01.5439470.4982721.01.02.02.02.0
    ixi590.049.47653116.72086419.98083534.02772150.6119163.41341586.31896590.01.5559320.4972831.01.02.02.02.0
    +
    +
    + +
    + + + + + +
    + + +
    + + + + + +
    + +
    +
    -.. code:: ipython3 - cov.groupby(['site']).describe() +Preprare brain data +------------------- -Prepare brain data -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Step 3. +~~~~~~~ Next, format and combine the MRI data using the following commands. The example data contains cortical thickness maps estimated by running @@ -139,7 +448,7 @@ was reduced by using ROIs from the Desikan-Killiany atlas. Including the Euler number as a covariate is also recommended, as this is a proxy metric for data quality. The `Euler number `__ from -each subjects recon-all output folder was extracted into a text file +each subject’s recon-all output folder was extracted into a text file and is merged into the cortical thickness data frame. The Euler number is site-specific, thus, to use the same exclusion threshold across sites it is important to center the site by subtracting the site median from @@ -147,7 +456,7 @@ all subjects at a site. Then take the square root and multiply by negative one and exclude any subjects with a square root above 10. Here is some psuedo-code (run from a terminal in the folder that has all -subjects recon-all output folders) that was used to extract these ROIs: +subject’s recon-all output folders) that was used to extract these ROIs: ``export SUBJECTS_DIR=/path/to/study/freesurfer_data/`` @@ -157,14 +466,14 @@ subjects recon-all output folders) that was used to extract these ROIs: .. code:: ipython3 - hcpya = pd.read_csv('/content/PCNtoolkit-demo/data/HCP1200_aparc_thickness.csv') - ixi = pd.read_csv('/content/PCNtoolkit-demo/data/IXI_aparc_thickness.csv') + hcpya = pd.read_csv(os.path.join(wdir,'data','HCP1200_aparc_thickness.csv')) + ixi = pd.read_csv(os.path.join(wdir,'data','IXI_aparc_thickness.csv')) .. code:: ipython3 brain_all = pd.merge(ixi, hcpya, how='outer') -We extracted the euler number from each subjects recon-all output +We extracted the euler number from each subject’s recon-all output folder into a text file and we now need to format and combine these into our brain dataframe. @@ -173,12 +482,12 @@ recon-all.log for each subject. Run this from the terminal in the folder where your subjects recon-all output folders are located. This assumes that all of your subject IDs start with “sub-” prefix. -:literal:`for i in sub-*; do if [[ -e ${i}/scripts/recon-all.log ]]; then cat ${i}/scripts/recon-all.log | grep -A 1 "Computing euler" > temp_log; lh_en=$(cat temp_log | head -2 | tail -1 | awk -F '=' '{print $2}' | awk -F ',' '{print $1}'); rh_en=$(cat temp_log | head -2 | tail -1 | awk -F '=' '{print $3}'); echo "${i}, ${lh_en}, ${rh_en}" >> euler.csv; echo ${i}; fi; done` +:literal:`for i in sub-\*; do if [[ -e ${i}/scripts/recon-all.log ]]; then cat ${i}/scripts/recon-all.log | grep -A 1 "Computing euler" > temp_log; lh_en=`cat temp_log | head -2 | tail -1 | awk -F '=' '{print $2}' | awk -F ',' '{print $1}'\`; rh_en=`cat temp_log | head -2 | tail -1 | awk -F '=' '{print $3}'\`; echo "${i}, ${lh_en}, ${rh_en}" >> euler.csv; echo ${i}; fi; done` .. code:: ipython3 - hcp_euler = pd.read_csv('/content/PCNtoolkit-demo/data/hcp-ya_euler.csv') - ixi_euler = pd.read_csv('/content/PCNtoolkit-demo/data/ixi_euler.csv') + hcp_euler = pd.read_csv(os.path.join(wdir,'data','hcp-ya_euler.csv')) + ixi_euler = pd.read_csv(os.path.join(wdir,'data','ixi_euler.csv')) .. code:: ipython3 @@ -221,7 +530,7 @@ inclusion is not too strict or too lenient. .. code:: ipython3 - df_euler.groupby(by='site').median() + df_euler.groupby(by='site')[['lh_euler', 'rh_euler', 'avg_euler']].median() @@ -229,9 +538,8 @@ inclusion is not too strict or too lenient. .. raw:: html -
    -
    -
    +
    +
    - + +
    + + +
    + + + + + +
    +
    @@ -363,6 +805,13 @@ inclusion is not too strict or too lenient. df_euler['site_median'] = df_euler['site_median'].replace({'hcp':-43,'ixi':-56}) + +.. parsed-literal:: + + :1: FutureWarning: Downcasting behavior in `replace` is deprecated and will be removed in a future version. To retain the old behavior, explicitly call `result.infer_objects(copy=False)`. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)` + df_euler['site_median'] = df_euler['site_median'].replace({'hcp':-43,'ixi':-56}) + + .. code:: ipython3 df_euler['avg_euler_centered'] = df_euler['avg_euler'] - df_euler['site_median'] @@ -383,18 +832,19 @@ inclusion is not too strict or too lenient. brain_good = brain.query('avg_euler_centered_neg_sqrt < 10') -.. warning:: - **CRITICAL STEP:** If possible, data should be visually inspected to - verify that the data inclusion is not too strict or too lenient. - Subjects above the Euler number threshold should be manually checked to - verify and justify their exclusion due to poor data quality. This is - just one approach for automated QC used by the developers of the - PCNtoolkit. Other approaches such as the ENIGMA QC pipeline or UK - Biobanks QC pipeline are also viable options for automated QC. +**CRITICAL STEP:** If possible, data should be visually inspected to +verify that the data inclusion is not too strict or too lenient. +Subjects above the Euler number threshold should be manually checked to +verify and justify their exclusion due to poor data quality. This is +just one approach for automated QC used by the developers of the +PCNtoolkit. Other approaches such as the ENIGMA QC pipeline or UK +Biobank’s QC pipeline are also viable options for automated QC. Combine covariate & cortical thickness dataframes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------------- +Step 4. +~~~~~~~ The normative modeling function requires the covariate predictors and brain features to be in separate text files. However, it is important to @@ -407,7 +857,7 @@ their own dataframes, using the commands below. .. code:: ipython3 - # make sure to use how="inner" so that we only include subjects that have data in both the covariate and the cortical thickness files + # make sure to use how="inner" so that we only include subjects that have data in both the covariate and the cortical thickness files all_data = pd.merge(brain_good, cov, how='inner') .. code:: ipython3 @@ -433,13 +883,15 @@ their own dataframes, using the commands below. all_data_covariates = all_data[['age','sex','site']] -.. warning:: - **CRITICAL STEP:** ``roi_ids`` is a variable that represents which brain - areas will be modeled and can be used to select subsets of the data - frame if you do not wish to run models for the whole brain. +**CRITICAL STEP:** ``roi_ids`` is a variable that represents which brain +areas will be modeled and can be used to select subsets of the data +frame if you do not wish to run models for the whole brain. Add variable to model site/scanner effects -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------ + +Step 5. +~~~~~~~ Currently, the different sites are coded in a single column (named ‘site’) and are represented as a string data type. However, the @@ -451,17 +903,689 @@ variables (0=not in this site, 1=present in this site). .. code:: ipython3 - all_data_covariates = pd.get_dummies(all_data_covariates, columns=['site']) + all_data_covariates = pd.get_dummies(all_data_covariates, columns=['site'], dtype=int) .. code:: ipython3 - all_data['Average_Thickness'] = all_data[['lh_MeanThickness_thickness','rh_MeanThickness_thickness']].mean(axis=1) + all_data_covariates.head() + + + + +.. raw:: html + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    agesexsite_hcpsite_ixi
    027.0110
    127.0210
    233.0110
    327.0110
    435.0210
    +
    +
    + +
    + + + + + +
    + + +
    + + + + + +
    + +
    +
    + +.. code:: ipython3 + + all_data_covariates + + + + +.. raw:: html + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    agesexsite_hcpsite_ixi
    027.000000110
    127.000000210
    233.000000110
    327.000000110
    435.000000210
    ...............
    168747.723477101
    168850.395619101
    168942.989733101
    169046.220397101
    169141.741273101
    +

    1692 rows × 4 columns

    +
    +
    + +
    + + + + + +
    + + +
    + + + + + +
    + +
    + + + +
    + +
    +
    + + + + +.. code:: ipython3 + + all_data['Average_Thickness'] = all_data[['lh_MeanThickness_thickness','rh_MeanThickness_thickness']].mean(axis=1) + Train/test split -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------- +Step 6. +~~~~~~~ In this example, we use 80% of the data for training and 20% for testing. Please carefully read the experimental design section on @@ -501,11 +1625,12 @@ Verify that your train & test arrays are the same size Test response size is: (339, 6) -.. warning:: - **CRITICAL STEP:** The model would not learn the site effects if all the - data from one site was only in the test set. Therefore, we stratify the - train/test split using the site variable. +**CRITICAL STEP:** The model would not learn the site effects if all the +data from one site was only in the test set. Therefore, we stratify the +train/test split using the site variable. +Step 7. +~~~~~~~ When the data were split into train and test sets, the row index was not reset. This means that the row index in the train and test data frames @@ -540,9 +1665,11 @@ which sites to evaluate model performance for, as follows: # Create a list with sites names to use in evaluating per-site metrics site_names = ['hcp', 'ixi'] - Setup output directories -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------ + +Step 8. +------- Save each brain region to its own text file (organized in separate directories) using the following commands, because for each response @@ -557,6 +1684,7 @@ variable, Y (e.g., brain region) we fit a separate normative model. X_train.to_csv('cov_tr.txt', sep = '\t', header=False, index = False) + .. code:: ipython3 y_train.to_csv('resp_tr.txt', sep = '\t', header=False, index = False) @@ -580,12 +1708,25 @@ variable, Y (e.g., brain region) we fit a separate normative model. .. code:: ipython3 + # Note: please change the path in the following to wdir (depending on whether you are running on colab or not) + ! for i in `cat /content/PCNtoolkit-demo/data/roi_dir_names`; do if [[ -e resp_tr_${i}.txt ]]; then cd ROI_models; mkdir ${i}; cd ../; cp resp_tr_${i}.txt ROI_models/${i}/resp_tr.txt; cp resp_te_${i}.txt ROI_models/${i}/resp_te.txt; cp cov_tr.txt ROI_models/${i}/cov_tr.txt; cp cov_te.txt ROI_models/${i}/cov_te.txt; fi; done + +.. parsed-literal:: + + mkdir: cannot create directory ‘lh_MeanThickness_thickness’: File exists + mkdir: cannot create directory ‘lh_bankssts_thickness’: File exists + mkdir: cannot create directory ‘lh_caudalanteriorcingulate_thickness’: File exists + mkdir: cannot create directory ‘lh_superiorfrontal_thickness’: File exists + mkdir: cannot create directory ‘rh_MeanThickness_thickness’: File exists + mkdir: cannot create directory ‘rh_superiorfrontal_thickness’: File exists + + .. code:: ipython3 # clean up files - ! rm resp_*.txt + ! rm resp_*.txt .. code:: ipython3 @@ -593,11 +1734,13 @@ variable, Y (e.g., brain region) we fit a separate normative model. ! rm cov_t*.txt Algorithm & Modeling -------------------------------- +==================== Basis expansion using B-Splines -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------- +Step 9. +~~~~~~~ Now, set up a B-spline basis set that allows us to perform nonlinear regression using a linear model, using the following commands. This @@ -615,29 +1758,29 @@ al `__. .. code:: ipython3 # set this path to wherever your ROI_models folder is located (where you copied all of the covariate & response text files to in Step 4) - data_dir = '/content/PCNtoolkit-demo/tutorials/BLR_protocol/ROI_models/' + data_dir = os.path.join(wdir,'tutorials','BLR_protocol','ROI_models') # Create a cubic B-spline basis (used for regression) xmin = 10#16 # xmin & xmax are the boundaries for ages of participants in the dataset xmax = 95#90 B = create_bspline_basis(xmin, xmax) - # create the basis expansion for the covariates for each of the - for roi in roi_ids: + # create the basis expansion for the covariates for each of the + for roi in roi_ids: print('Creating basis expansion for ROI:', roi) roi_dir = os.path.join(data_dir, roi) os.chdir(roi_dir) - # create output dir + # create output dir os.makedirs(os.path.join(roi_dir,'blr'), exist_ok=True) # load train & test covariate data matrices X_tr = np.loadtxt(os.path.join(roi_dir, 'cov_tr.txt')) X_te = np.loadtxt(os.path.join(roi_dir, 'cov_te.txt')) - # add intercept column + # add intercept column X_tr = np.concatenate((X_tr, np.ones((X_tr.shape[0],1))), axis=1) X_te = np.concatenate((X_te, np.ones((X_te.shape[0],1))), axis=1) np.savetxt(os.path.join(roi_dir, 'cov_int_tr.txt'), X_tr) np.savetxt(os.path.join(roi_dir, 'cov_int_te.txt'), X_te) - - # create Bspline basis set + + # create Bspline basis set Phi = np.array([B(i) for i in X_tr[:,0]]) Phis = np.array([B(i) for i in X_te[:,0]]) X_tr = np.concatenate((X_tr, Phi), axis=1) @@ -657,8 +1800,10 @@ al `__. Estimate normative model -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------ +Step 10. +~~~~~~~~ Set up a variable (``data_dir``) that specifies the path to the ROI directories that were created in Step 7. Initiate two empty pandas data @@ -674,6 +1819,8 @@ these data frames will be saved as individual csv files. blr_metrics = pd.DataFrame(columns = ['ROI', 'MSLL', 'EV', 'SMSE', 'RMSE', 'Rho']) blr_site_metrics = pd.DataFrame(columns = ['ROI', 'site', 'MSLL', 'EV', 'SMSE', 'RMSE', 'Rho']) +Step 11. +~~~~~~~~ Estimate the normative models using a for loop to iterate over brain regions. An important consideration is whether to re-scale or @@ -693,70 +1840,69 @@ arguments that are worthy of commenting on: :: - alg = 'blr': specifies we should use Bayesian Linear Regression. - - optimizer = 'powell': use Powell's derivative-free optimization method (faster in this case than L-BFGS) - - savemodel = False: do not write out the final estimated model to disk + - optimizer = 'powell': use Powell's derivative-free optimization method (faster in this case than L-BFGS) + - savemodel = False: do not write out the final estimated model to disk - saveoutput = False: return the outputs directly rather than writing them to disk - standardize = False: Do not standardize the covariates or response variables -.. warning:: - **CRITICAL STEP:** This code fragment will loop through each region of - interest in the ``roi_ids`` list (created in step 4) using Bayesian - Linear Regression and evaluate the model on the independent test set. In - principle, we could estimate the normative models on the whole data - matrix at once (e.g., with the response variables stored in a - ``n_subjects`` by ``n_brain_measures`` NumPy array or a text file - instead of saved out into separate directories). However, running the - models iteratively gives some extra flexibility in that it does not - require that the included subjects are the same for each of the brain - measures. +**CRITICAL STEP:** This code fragment will loop through each region of +interest in the ``roi_ids`` list (created in step 4) using Bayesian +Linear Regression and evaluate the model on the independent test set. In +principle, we could estimate the normative models on the whole data +matrix at once (e.g., with the response variables stored in a +``n_subjects`` by ``n_brain_measures`` NumPy array or a text file +instead of saved out into separate directories). However, running the +models iteratively gives some extra flexibility in that it does not +require that the included subjects are the same for each of the brain +measures. .. code:: ipython3 # Loop through ROIs - for roi in roi_ids: + for roi in roi_ids: print('Running ROI:', roi) roi_dir = os.path.join(data_dir, roi) os.chdir(roi_dir) - - # configure the covariates to use. Change *_bspline_* to *_int_* to + + # configure the covariates to use. Change *_bspline_* to *_int_* to cov_file_tr = os.path.join(roi_dir, 'cov_bspline_tr.txt') cov_file_te = os.path.join(roi_dir, 'cov_bspline_te.txt') - + # load train & test response files resp_file_tr = os.path.join(roi_dir, 'resp_tr.txt') - resp_file_te = os.path.join(roi_dir, 'resp_te.txt') - + resp_file_te = os.path.join(roi_dir, 'resp_te.txt') + # run a basic model - yhat_te, s2_te, nm, Z, metrics_te = estimate(cov_file_tr, - resp_file_tr, - testresp=resp_file_te, - testcov=cov_file_te, - alg = 'blr', - optimizer = 'powell', - savemodel = True, + yhat_te, s2_te, nm, Z, metrics_te = estimate(cov_file_tr, + resp_file_tr, + testresp=resp_file_te, + testcov=cov_file_te, + alg = 'blr', + optimizer = 'powell', + savemodel = True, saveoutput = False, standardize = False) # save metrics blr_metrics.loc[len(blr_metrics)] = [roi, metrics_te['MSLL'][0], metrics_te['EXPV'][0], metrics_te['SMSE'][0], metrics_te['RMSE'][0], metrics_te['Rho'][0]] - + # Compute metrics per site in test set, save to pandas df # load true test data X_te = np.loadtxt(cov_file_te) y_te = np.loadtxt(resp_file_te) y_te = y_te[:, np.newaxis] # make sure it is a 2-d array - + # load training data (required to compute the MSLL) y_tr = np.loadtxt(resp_file_tr) y_tr = y_tr[:, np.newaxis] - - for num, site in enumerate(sites): + + for num, site in enumerate(sites): y_mean_te_site = np.array([[np.mean(y_te[site])]]) y_var_te_site = np.array([[np.var(y_te[site])]]) yhat_mean_te_site = np.array([[np.mean(yhat_te[site])]]) yhat_var_te_site = np.array([[np.var(yhat_te[site])]]) - + metrics_te_site = evaluate(y_te[site], yhat_te[site], s2_te[site], y_mean_te_site, y_var_te_site) - + site_name = site_names[num] blr_site_metrics.loc[len(blr_site_metrics)] = [roi, site_names[num], metrics_te_site['MSLL'][0], metrics_te_site['EXPV'][0], metrics_te_site['SMSE'][0], metrics_te_site['RMSE'][0], metrics_te_site['Rho'][0]] @@ -764,40 +1910,54 @@ arguments that are worthy of commenting on: .. parsed-literal:: Running ROI: lh_MeanThickness_thickness + inscaler: None + outscaler: None Processing data in /content/PCNtoolkit-demo/tutorials/BLR_protocol/ROI_models/lh_MeanThickness_thickness/resp_tr.txt Estimating model 1 of 1 configuring BLR ( order 1 ) Using default hyperparameters - Optimization terminated successfully. - Current function value: -1162.792820 - Iterations: 2 - Function evaluations: 47 - Saving model meta-data... - Evaluating the model ... .. parsed-literal:: - /usr/local/lib/python3.7/dist-packages/pcntoolkit/model/bayesreg.py:187: LinAlgWarning: Ill-conditioned matrix (rcond=1.15485e-18): result may not be accurate. - invAXt = linalg.solve(self.A, X.T, check_finite=False) - /usr/local/lib/python3.7/dist-packages/pcntoolkit/model/bayesreg.py:187: LinAlgWarning: Ill-conditioned matrix (rcond=4.51813e-19): result may not be accurate. + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/bayesreg.py:196: LinAlgWarning: Ill-conditioned matrix (rcond=1.15485e-18): result may not be accurate. invAXt = linalg.solve(self.A, X.T, check_finite=False) .. parsed-literal:: + Optimization terminated successfully. + Current function value: -1162.792820 + Iterations: 2 + Function evaluations: 43 + Saving model meta-data... + Evaluating the model ... Running ROI: rh_MeanThickness_thickness + inscaler: None + outscaler: None Processing data in /content/PCNtoolkit-demo/tutorials/BLR_protocol/ROI_models/rh_MeanThickness_thickness/resp_tr.txt Estimating model 1 of 1 configuring BLR ( order 1 ) Using default hyperparameters + + +.. parsed-literal:: + + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/bayesreg.py:196: LinAlgWarning: Ill-conditioned matrix (rcond=4.51813e-19): result may not be accurate. + invAXt = linalg.solve(self.A, X.T, check_finite=False) + + +.. parsed-literal:: + Optimization terminated successfully. Current function value: -1187.621858 Iterations: 2 - Function evaluations: 47 + Function evaluations: 43 Saving model meta-data... Evaluating the model ... Running ROI: lh_bankssts_thickness + inscaler: None + outscaler: None Processing data in /content/PCNtoolkit-demo/tutorials/BLR_protocol/ROI_models/lh_bankssts_thickness/resp_tr.txt Estimating model 1 of 1 configuring BLR ( order 1 ) @@ -805,10 +1965,12 @@ arguments that are worthy of commenting on: Optimization terminated successfully. Current function value: -578.945257 Iterations: 2 - Function evaluations: 46 + Function evaluations: 42 Saving model meta-data... Evaluating the model ... Running ROI: lh_caudalanteriorcingulate_thickness + inscaler: None + outscaler: None Processing data in /content/PCNtoolkit-demo/tutorials/BLR_protocol/ROI_models/lh_caudalanteriorcingulate_thickness/resp_tr.txt Estimating model 1 of 1 configuring BLR ( order 1 ) @@ -816,10 +1978,12 @@ arguments that are worthy of commenting on: Optimization terminated successfully. Current function value: -235.509099 Iterations: 3 - Function evaluations: 75 + Function evaluations: 69 Saving model meta-data... Evaluating the model ... Running ROI: lh_superiorfrontal_thickness + inscaler: None + outscaler: None Processing data in /content/PCNtoolkit-demo/tutorials/BLR_protocol/ROI_models/lh_superiorfrontal_thickness/resp_tr.txt Estimating model 1 of 1 configuring BLR ( order 1 ) @@ -827,10 +1991,12 @@ arguments that are worthy of commenting on: Optimization terminated successfully. Current function value: -716.547377 Iterations: 3 - Function evaluations: 91 + Function evaluations: 84 Saving model meta-data... Evaluating the model ... Running ROI: rh_superiorfrontal_thickness + inscaler: None + outscaler: None Processing data in /content/PCNtoolkit-demo/tutorials/BLR_protocol/ROI_models/rh_superiorfrontal_thickness/resp_tr.txt Estimating model 1 of 1 configuring BLR ( order 1 ) @@ -838,17 +2004,19 @@ arguments that are worthy of commenting on: Optimization terminated successfully. Current function value: -730.639309 Iterations: 2 - Function evaluations: 45 + Function evaluations: 41 Saving model meta-data... Evaluating the model ... Evaluation & Interpretation ----------------------------------------- +=========================== Describe the normative model performance -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------------- +Step 12. +~~~~~~~~ In step 11, when we looped over each region of interest in the ``roi_ids`` list (created in step 4) and evaluated the normative model @@ -917,7 +2085,7 @@ can organize them into a single file, and merge the deviation scores into the original data file. Visualize normative model outputs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------- Figure 4A viz ~~~~~~~~~~~~~ @@ -937,29 +2105,21 @@ Figure 4A viz plt.figure(dpi=380) fig, axes = joypy.joyplot(blr_site_metrics, column=['EV'], overlap=2.5, by="site", ylim='own', fill=True, figsize=(8,8) , legend=False, xlabels=True, ylabels=True, colormap=lambda x: color_gradient(x, start=(.08, .45, .8),stop=(.8, .34, .44)) - , alpha=0.6, linewidth=.5, linecolor='w', fade=True) + , alpha=0.6, linewidth=.5, linecolor='w', fade=True); plt.title('Test Set Explained Variance', fontsize=18, color='black', alpha=1) plt.xlabel('Explained Variance', fontsize=14, color='black', alpha=1) plt.ylabel('Site', fontsize=14, color='black', alpha=1) - plt.show - + plt.show() .. parsed-literal:: - +
    - -.. parsed-literal:: - -
    - - - -.. image:: BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_93_2.png +.. image:: BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_97_1.png The code used to create the visualizations shown in Figure 4 panels B-F, @@ -967,7 +2127,8 @@ can be found in this `notebook `__. Post-Hoc analysis ideas -~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- + The code for running SVM classification and classical case vs. control t-testing on the outputs of normative modeling can be found in this diff --git a/doc/build/html/_sources/pages/FAQs.rst.txt b/doc/build/html/_sources/pages/FAQs.rst.txt index a8879d36..efe98bd0 100644 --- a/doc/build/html/_sources/pages/FAQs.rst.txt +++ b/doc/build/html/_sources/pages/FAQs.rst.txt @@ -5,7 +5,7 @@ Frequently Asked Questions Most of the questions we recieve are about interpretation of normative modeling outputs. -The PCNtoolkit develoers have written a protocol for how to run a normative modeling analysis which should be helpful to you if you are just getting started. +The PCNtoolkit developers have written a protocol for how to run a normative modeling analysis which should be helpful to you if you are just getting started. Rutherford, S., Kia, S. M., Wolfers, T., ... Beckmann, C. F., & Marquand, A. F. (2022). The Normative Modeling Framework for Computational Psychiatry. Nature Protocols. https://www.nature.com/articles/s41596-022-00696-5. diff --git a/doc/build/html/_sources/pages/HBR_NormativeModel_FCONdata_Tutorial.rst.txt b/doc/build/html/_sources/pages/HBR_NormativeModel_FCONdata_Tutorial.rst.txt index d4933e38..b4fa2dfb 100644 --- a/doc/build/html/_sources/pages/HBR_NormativeModel_FCONdata_Tutorial.rst.txt +++ b/doc/build/html/_sources/pages/HBR_NormativeModel_FCONdata_Tutorial.rst.txt @@ -1,31 +1,29 @@ -.. title:: HBR tutorial +`Predictive Clinical Neuroscience Toolkit `__ +====================================================================================== -Hierarchical Bayesian Regression +Hierarchical Bayesian Regression Normative Modelling and Transfer onto unseen site. =================================================================================== This notebook will go through basic data preparation (training and -testing set, `see Saige's +testing set, `see Saige’s tutorial `__ on Normative Modelling for more detail), the actual training of the models, and will finally describe how to transfer the trained models onto unseen sites. Created by `Saige Rutherford `__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Adapted/edited by Andre Marquand and Pierre Berthet. - -.. image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/predictive-clinical-neuroscience/PCNtoolkit-demo/blob/main/tutorials/HBR_FCON/HBR_NormativeModel_FCONdata_Tutorial.ipynb - - +adapted/edited by Andre Marquand and Pierre Berthet +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Step 0: Install necessary libraries & grab data files ----------------------------------------------------- .. code:: ipython3 - ! pip install pcntoolkit==0.26 - + !pip install pcntoolkit + !pip install nutpie For this tutorial we will use data from the `Functional Connectom Project FCON1000 `__ to create a @@ -47,7 +45,7 @@ First we import the required package, and create a working directory. .. code:: ipython3 - processing_dir = "HBR_demo/" # replace with a path to your working directory + processing_dir = "HBR_demo" # replace with desired working directory if not os.path.isdir(processing_dir): os.makedirs(processing_dir) os.chdir(processing_dir) @@ -63,27 +61,33 @@ color coded by the various sites: .. code:: ipython3 - fcon = pd.read_csv('https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/main/data/fcon1000.csv') + fcon = pd.read_csv( + "https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/main/data/fcon1000.csv" + ) + + # extract the ICBM site for transfer + icbm = fcon.loc[fcon["site"] == "ICBM"] + icbm["sitenum"] = 0 - icbm = fcon.loc[fcon['site'] == 'ICBM'] - icbm['sitenum'] = 0 - fcon = fcon.loc[fcon['site'] != 'ICBM'] + # remove from the training set (also Pittsburgh because it only has 3 samples) + fcon = fcon.loc[fcon["site"] != "ICBM"] + fcon = fcon.loc[fcon["site"] != "Pittsburgh"] - sites = fcon['site'].unique() - fcon['sitenum'] = 0 + sites = fcon["site"].unique() + fcon["sitenum"] = 0 f, ax = plt.subplots(figsize=(12, 12)) - for i,s in enumerate(sites): - idx = fcon['site'] == s - fcon['sitenum'].loc[idx] = i + for i, s in enumerate(sites): + idx = fcon["site"] == s + fcon["sitenum"].loc[idx] = i - print('site',s, sum(idx)) - ax.scatter(fcon['age'].loc[idx], fcon['lh_MeanThickness_thickness'].loc[idx]) + print("site", s, sum(idx)) + ax.scatter(fcon["age"].loc[idx], fcon["lh_MeanThickness_thickness"].loc[idx]) ax.legend(sites) - ax.set_ylabel('LH mean cortical thickness [mm]') - ax.set_xlabel('age') + ax.set_ylabel("LH mean cortical thickness [mm]") + ax.set_xlabel("age") Step 1: Prepare training and testing sets @@ -111,16 +115,16 @@ then displayed. icbm_tr = icbm.loc[tr] icbm_te = icbm.loc[te] - print('sample size check') - for i,s in enumerate(sites): - idx = fcon_tr['site'] == s - idxte = fcon_te['site'] == s - print(i,s, sum(idx), sum(idxte)) + print("sample size check") + for i, s in enumerate(sites): + idx = fcon_tr["site"] == s + idxte = fcon_te["site"] == s + print(i, s, sum(idx), sum(idxte)) - fcon_tr.to_csv(processing_dir + '/fcon1000_tr.csv') - fcon_te.to_csv(processing_dir + '/fcon1000_te.csv') - icbm_tr.to_csv(processing_dir + '/fcon1000_icbm_tr.csv') - icbm_te.to_csv(processing_dir + '/fcon1000_icbm_te.csv') + fcon_tr.to_csv(processing_dir + "/fcon1000_tr.csv") + fcon_te.to_csv(processing_dir + "/fcon1000_te.csv") + icbm_tr.to_csv(processing_dir + "/fcon1000_icbm_tr.csv") + icbm_te.to_csv(processing_dir + "/fcon1000_icbm_te.csv") Otherwise you can just load these pre defined subsets: @@ -140,7 +144,7 @@ hemisphere: two idps. .. code:: ipython3 - idps = ['rh_MeanThickness_thickness','lh_MeanThickness_thickness'] + idps = ["rh_MeanThickness_thickness", "lh_MeanThickness_thickness"] As input to the model, we need covariates (used to describe predictable source of variability (fixed effects), here ‘age’), measures (here @@ -158,56 +162,79 @@ testing set (``_test``). .. code:: ipython3 - X_train = (fcon_tr['age']/100).to_numpy(dtype=float) + X_train = (fcon_tr["age"] / 100).to_numpy(dtype=float) Y_train = fcon_tr[idps].to_numpy(dtype=float) - batch_effects_train = fcon_tr[['sitenum','sex']].to_numpy(dtype=int) - with open('X_train.pkl', 'wb') as file: + # configure batch effects for site and sex + # batch_effects_train = fcon_tr[['sitenum','sex']].to_numpy(dtype=int) + + # or only site + batch_effects_train = fcon_tr[["sitenum"]].to_numpy(dtype=int) + + with open("X_train.pkl", "wb") as file: pickle.dump(pd.DataFrame(X_train), file) - with open('Y_train.pkl', 'wb') as file: + with open("Y_train.pkl", "wb") as file: pickle.dump(pd.DataFrame(Y_train), file) - with open('trbefile.pkl', 'wb') as file: + with open("trbefile.pkl", "wb") as file: pickle.dump(pd.DataFrame(batch_effects_train), file) - X_test = (fcon_te['age']/100).to_numpy(dtype=float) + X_test = (fcon_te["age"] / 100).to_numpy(dtype=float) Y_test = fcon_te[idps].to_numpy(dtype=float) - batch_effects_test = fcon_te[['sitenum','sex']].to_numpy(dtype=int) + # batch_effects_test = fcon_te[['sitenum','sex']].to_numpy(dtype=int) + batch_effects_test = fcon_te[["sitenum"]].to_numpy(dtype=int) - with open('X_test.pkl', 'wb') as file: + with open("X_test.pkl", "wb") as file: pickle.dump(pd.DataFrame(X_test), file) - with open('Y_test.pkl', 'wb') as file: + with open("Y_test.pkl", "wb") as file: pickle.dump(pd.DataFrame(Y_test), file) - with open('tsbefile.pkl', 'wb') as file: + with open("tsbefile.pkl", "wb") as file: pickle.dump(pd.DataFrame(batch_effects_test), file) + # a simple function to quickly load pickle files def ldpkl(filename: str): - with open(filename, 'rb') as f: + with open(filename, "rb") as f: return pickle.load(f) +.. code:: ipython3 + + batch_effects_test + Step 3: Files and Folders grooming ---------------------------------- .. code:: ipython3 - respfile = os.path.join(processing_dir, 'Y_train.pkl') # measurements (eg cortical thickness) of the training samples (columns: the various features/ROIs, rows: observations or subjects) - covfile = os.path.join(processing_dir, 'X_train.pkl') # covariates (eg age) the training samples (columns: covariates, rows: observations or subjects) + respfile = os.path.join( + processing_dir, "Y_train.pkl" + ) # measurements (eg cortical thickness) of the training samples (columns: the various features/ROIs, rows: observations or subjects) + covfile = os.path.join( + processing_dir, "X_train.pkl" + ) # covariates (eg age) the training samples (columns: covariates, rows: observations or subjects) - testrespfile_path = os.path.join(processing_dir, 'Y_test.pkl') # measurements for the testing samples - testcovfile_path = os.path.join(processing_dir, 'X_test.pkl') # covariate file for the testing samples + testrespfile_path = os.path.join( + processing_dir, "Y_test.pkl" + ) # measurements for the testing samples + testcovfile_path = os.path.join( + processing_dir, "X_test.pkl" + ) # covariate file for the testing samples - trbefile = os.path.join(processing_dir, 'trbefile.pkl') # training batch effects file (eg scanner_id, gender) (columns: the various batch effects, rows: observations or subjects) - tsbefile = os.path.join(processing_dir, 'tsbefile.pkl') # testing batch effects file + trbefile = os.path.join( + processing_dir, "trbefile.pkl" + ) # training batch effects file (eg scanner_id, gender) (columns: the various batch effects, rows: observations or subjects) + tsbefile = os.path.join(processing_dir, "tsbefile.pkl") # testing batch effects file - output_path = os.path.join(processing_dir, 'Models/') # output path, where the models will be written - log_dir = os.path.join(processing_dir, 'log/') # + output_path = os.path.join( + processing_dir, "Models/" + ) # output path, where the models will be written + 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' # a string to name the output files, of use only to you, so adapt it for your needs. + outputsuffix = "_estimate" # a string to name the output files, of use only to you, so adapt it for your needs. Step 4: Estimating the models ----------------------------- @@ -222,16 +249,26 @@ and output files will be written and how they will be named. .. code:: ipython3 - ptk.normative.estimate(covfile=covfile, - respfile=respfile, - tsbefile=tsbefile, - trbefile=trbefile, - alg='hbr', - log_path=log_dir, - binary=True, - output_path=output_path, testcov= testcovfile_path, - testresp = testrespfile_path, - outputsuffix=outputsuffix, savemodel=True) + ptk.normative.estimate( + covfile=covfile, + respfile=respfile, + tsbefile=tsbefile, + trbefile=trbefile, + inscaler="standardize", + outscaler="standardize", + linear_mu="True", + random_intercept_mu="True", + centered_intercept_mu="True", + alg="hbr", + log_path=log_dir, + binary=True, + output_path=output_path, + testcov=testcovfile_path, + testresp=testrespfile_path, + outputsuffix=outputsuffix, + savemodel=True, + nuts_sampler="nutpie", + ) Here some analyses can be done, there are also some error metrics that could be of interest. This is covered in step 6 and in `Saige’s @@ -247,42 +284,49 @@ training and testing set of covariates, measures and batch effects: .. code:: ipython3 - X_adapt = (icbm_tr['age']/100).to_numpy(dtype=float) + X_adapt = (icbm_tr["age"] / 100).to_numpy(dtype=float) Y_adapt = icbm_tr[idps].to_numpy(dtype=float) - batch_effects_adapt = icbm_tr[['sitenum','sex']].to_numpy(dtype=int) + # batch_effects_adapt = icbm_tr[['sitenum','sex']].to_numpy(dtype=int) + batch_effects_adapt = icbm_tr[["sitenum"]].to_numpy(dtype=int) - with open('X_adaptation.pkl', 'wb') as file: + with open("X_adaptation.pkl", "wb") as file: pickle.dump(pd.DataFrame(X_adapt), file) - with open('Y_adaptation.pkl', 'wb') as file: + with open("Y_adaptation.pkl", "wb") as file: pickle.dump(pd.DataFrame(Y_adapt), file) - with open('adbefile.pkl', 'wb') as file: + with open("adbefile.pkl", "wb") as file: pickle.dump(pd.DataFrame(batch_effects_adapt), file) # Test data (new dataset) - X_test_txfr = (icbm_te['age']/100).to_numpy(dtype=float) + X_test_txfr = (icbm_te["age"] / 100).to_numpy(dtype=float) Y_test_txfr = icbm_te[idps].to_numpy(dtype=float) - batch_effects_test_txfr = icbm_te[['sitenum','sex']].to_numpy(dtype=int) + # batch_effects_test_txfr = icbm_te[['sitenum','sex']].to_numpy(dtype=int) + batch_effects_test_txfr = icbm_te[["sitenum"]].to_numpy(dtype=int) - with open('X_test_txfr.pkl', 'wb') as file: + with open("X_test_txfr.pkl", "wb") as file: pickle.dump(pd.DataFrame(X_test_txfr), file) - with open('Y_test_txfr.pkl', 'wb') as file: + with open("Y_test_txfr.pkl", "wb") as file: pickle.dump(pd.DataFrame(Y_test_txfr), file) - with open('txbefile.pkl', 'wb') as file: + with open("txbefile.pkl", "wb") as file: pickle.dump(pd.DataFrame(batch_effects_test_txfr), file) + .. code:: ipython3 - respfile = os.path.join(processing_dir, 'Y_adaptation.pkl') - covfile = os.path.join(processing_dir, 'X_adaptation.pkl') - testrespfile_path = os.path.join(processing_dir, 'Y_test_txfr.pkl') - testcovfile_path = os.path.join(processing_dir, 'X_test_txfr.pkl') - trbefile = os.path.join(processing_dir, 'adbefile.pkl') - tsbefile = os.path.join(processing_dir, 'txbefile.pkl') + respfile = os.path.join(processing_dir, "Y_adaptation.pkl") + covfile = os.path.join(processing_dir, "X_adaptation.pkl") + testrespfile_path = os.path.join(processing_dir, "Y_test_txfr.pkl") + testcovfile_path = os.path.join(processing_dir, "X_test_txfr.pkl") + trbefile = os.path.join(processing_dir, "adbefile.pkl") + tsbefile = os.path.join(processing_dir, "txbefile.pkl") - log_dir = os.path.join(processing_dir, 'log_transfer/') - output_path = os.path.join(processing_dir, 'Transfer/') - model_path = os.path.join(processing_dir, 'Models/') # path to the previously trained models - outputsuffix = '_transfer' # suffix added to the output files from the transfer function + log_dir = os.path.join(processing_dir, "log_transfer/") + output_path = os.path.join(processing_dir, "Transfer/") + model_path = os.path.join( + processing_dir, "Models/" + ) # path to the previously trained models + outputsuffix = ( + "_transfer" # suffix added to the output files from the transfer function + ) Here, the difference is that the transfer function needs a model path, which points to the models we just trained, and new site data (training @@ -290,20 +334,36 @@ and testing). That is basically the only difference. .. code:: ipython3 - yhat, s2, z_scores = ptk.normative.transfer(covfile=covfile, - respfile=respfile, - tsbefile=tsbefile, - trbefile=trbefile, - model_path = model_path, - alg='hbr', - log_path=log_dir, - binary=True, - output_path=output_path, - testcov= testcovfile_path, - testresp = testrespfile_path, - outputsuffix=outputsuffix, - savemodel=True) + yhat, s2, z_scores = ptk.normative.transfer( + covfile=covfile, + respfile=respfile, + tsbefile=tsbefile, + trbefile=trbefile, + inscaler="standardize", + outscaler="standardize", + linear_mu="True", + random_intercept_mu="True", + centered_intercept_mu="True", + model_path=model_path, + alg="hbr", + log_path=log_dir, + binary=True, + output_path=output_path, + testcov=testcovfile_path, + testresp=testrespfile_path, + outputsuffix=outputsuffix, + savemodel=True, + nuts_sampler="nutpie", + ) + +.. code:: ipython3 + output_path + +.. code:: ipython3 + + EV = pd.read_pickle("EXPV_estimate.pkl") + print(EV) And that is it, you now have models that benefited from prior knowledge about different scanner sites to learn on unseen sites. @@ -311,19 +371,12 @@ about different scanner sites to learn on unseen sites. Step 6: Interpreting model performance -------------------------------------- -Output evaluation metrics definitions - -============= ============================================================== -Abbreviation Full name -============= ============================================================== -NM Normative Model -EV / EXPV Explained Variance -MSLL Mean Standardized Log Loss -SMSE Standardized Mean Squared Error -RMSE Root Mean Squared Error between true/predicted responses -Rho Pearson orrelation between true/predicted responses -pRho Parametric p-value for this correlation -Z Z-score or deviation score -yhat predictive mean -ys2 predictive variance -============= ============================================================== +Output evaluation metrics definitions: \* yhat - predictive mean \* ys2 +- predictive variance \* nm - normative model \* Z - deviance scores \* +Rho - Pearson correlation between true and predicted responses \* pRho - +parametric p-value for this correlation \* RMSE - root mean squared +error between true/predicted responses \* SMSE - standardised mean +squared error \* EV - explained variance \* MSLL - mean standardized log +loss \* See page 23 in +http://www.gaussianprocess.org/gpml/chapters/RW2.pdf + diff --git a/doc/build/html/_sources/pages/apply_normative_models.rst.txt b/doc/build/html/_sources/pages/apply_normative_models.rst.txt index 9a55d8b4..1dabeb43 100644 --- a/doc/build/html/_sources/pages/apply_normative_models.rst.txt +++ b/doc/build/html/_sources/pages/apply_normative_models.rst.txt @@ -1,17 +1,5 @@ -.. title:: Braincharts tutorial - -Braincharts: transfer -=================================== - -Code for transfering the models from `Charting Brain Growth and Aging at High Spatial Precision. `__ - -.. image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/predictive-clinical-neuroscience/braincharts/blob/master/scripts/apply_normative_models_ct.ipynb - - -.. figure:: ./brainchart_fig1.png - :height: 400px - :align: center +Using lifespan models to make predictions on new data +----------------------------------------------------- This notebook shows how to apply the coefficients from pre-estimated normative models to new data. This can be done in two different ways: @@ -24,34 +12,26 @@ datasets `__ and adapt the learned model to make predictions on these data. First, if necessary, we install PCNtoolkit (note: this tutorial requires -at least version 0.20) +at least version 0.27) .. code:: ipython3 - !pip install pcntoolkit==0.26 + !pip install pcntoolkit + !pip install nutpie .. code:: ipython3 ! git clone https://github.com/predictive-clinical-neuroscience/braincharts.git - -.. parsed-literal:: - - Cloning into 'braincharts'... - remote: Enumerating objects: 1444, done. - remote: Counting objects: 100% (1444/1444), done. - remote: Compressing objects: 100% (1365/1365), done. - remote: Total 1444 (delta 153), reused 1342 (delta 75), pack-reused 0 - Receiving objects: 100% (1444/1444), 57.99 MiB | 34.87 MiB/s, done. - Resolving deltas: 100% (153/153), done. - - .. code:: ipython3 # we need to be in the scripts folder when we import the libraries in the code block below, # because there is a function called nm_utils that is in the scripts folder that we need to import import os - os.chdir('/content/braincharts/scripts/') #this path is setup for running on Google Colab. Change it to match your local path if running locally + wdir = 'braincharts' + + os.chdir(wdir) #this path is setup for running on Google Colab. Change it to match your local path if running locally + root_dir = os.getcwd() Now we import the required libraries @@ -65,45 +45,34 @@ Now we import the required libraries from pcntoolkit.normative import estimate, predict, evaluate from pcntoolkit.util.utils import compute_MSLL, create_design_matrix + os.chdir(os.path.join(root_dir, 'scripts')) from nm_utils import remove_bad_subjects, load_2d + os.chdir(root_dir) We need to unzip the models. .. code:: ipython3 - os.chdir('/content/braincharts/models/') - -.. code:: ipython3 - - ls - - -.. parsed-literal:: - - lifespan_12K_57sites_mqc2_train.zip lifespan_29K_82sites_train.zip - lifespan_12K_59sites_mqc_train.zip lifespan_57K_82sites.zip - lifespan_23K_57sites_mqc2.zip README.md - + os.chdir(os.path.join(root_dir, 'models')) .. code:: ipython3 # we will use the biggest sample as our training set (approx. N=57000 subjects from 82 sites) - # for more info on the other pretrained models available in this repository, + # for more info on the other pretrained models available in this repository, # please refer to the accompanying paper https://elifesciences.org/articles/72904 ! unzip lifespan_57K_82sites.zip Next, we configure some basic variables, like where we want the analysis to be done and which model we want to use. -.. note:: - We maintain a list of site ids for each dataset, which - describe the site names in the training and test data (``site_ids_tr`` - and ``site_ids_te``), plus also the adaptation data . The training site - ids are provided as a text file in the distribution and the test ids are - extracted automatically from the pandas dataframe (see below). If you - use additional data from the sites (e.g. later waves from ABCD), it may - be necessary to adjust the site names to match the names in the training - set. See the accompanying paper for more details +**Note:** We maintain a list of site ids for each dataset, which +describe the site names in the training and test data (``site_ids_tr`` +and ``site_ids_te``), plus also the adaptation data . The training site +ids are provided as a text file in the distribution and the test ids are +extracted automatically from the pandas dataframe (see below). If you +use additional data from the sites (e.g. later waves from ABCD), it may +be necessary to adjust the site names to match the names in the training +set. See the accompanying paper for more details .. code:: ipython3 @@ -111,91 +80,35 @@ to be done and which model we want to use. model_name = 'lifespan_57K_82sites' site_names = 'site_ids_ct_82sites.txt' - # where the analysis takes place - root_dir = '/content/braincharts' + + # where the data files live + data_dir = os.path.join(root_dir,'docs') + + # where the models live out_dir = os.path.join(root_dir, 'models', model_name) # load a set of site ids from this model. This must match the training data with open(os.path.join(root_dir,'docs', site_names)) as f: site_ids_tr = f.read().splitlines() -Download test dataset ------------------------------------------------------ - -As mentioned above, to demonstrate this tool we will use a test dataset -derived from the FCON 1000 dataset. We provide a prepackaged -training/test split of these data in the required format (also after -removing sites with only a few data points), -`here `__. -you can get these data by running the following commmands: - -.. code:: ipython3 - - os.chdir(root_dir) - !wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/braincharts/master/docs/OpenNeuroTransfer_ct_te.csv - !wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/braincharts/master/docs/OpenNeuroTransfer_ct_tr.csv - - -.. parsed-literal:: - - --2022-02-17 15:01:31-- https://raw.githubusercontent.com/predictive-clinical-neuroscience/braincharts/master/docs/OpenNeuroTransfer_ct_te.csv - Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ... - Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected. - HTTP request sent, awaiting response... 200 OK - Length: 628752 (614K) [text/plain] - Saving to: ‘OpenNeuroTransfer_te.csv’ - - OpenNeuroTransfer_t 100%[===================>] 614.02K --.-KB/s in 0.03s - - 2022-02-17 15:01:31 (22.0 MB/s) - ‘OpenNeuroTransfer_te.csv’ saved [628752/628752] - - --2022-02-17 15:01:31-- https://raw.githubusercontent.com/predictive-clinical-neuroscience/braincharts/master/docs/OpenNeuroTransfer_ct_tr.csv - Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.109.133, 185.199.110.133, 185.199.108.133, ... - Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.109.133|:443... connected. - HTTP request sent, awaiting response... 200 OK - Length: 163753 (160K) [text/plain] - Saving to: ‘OpenNeuroTransfer_tr.csv’ - - OpenNeuroTransfer_c 100%[===================>] 159.92K --.-KB/s in 0.03s - - 2022-02-17 15:01:32 (6.08 MB/s) - ‘OpenNeuroTransfer_ct_tr.csv’ saved [163753/163753] - - - Load test data ------------------------------------------------------ - -Now we load the test data and remove some subjects that may have poor -scan quality. This asssesment is based on the Freesurfer Euler -characteristic as described in the papers below. +~~~~~~~~~~~~~~ -.. note:: - For the purposes of this tutorial, we make predictions for all - sites in the FCON 1000 dataset, but two of them were also included in - the training data (named ‘Baltimore’ and ‘NewYork_a’). In this case, - this will only slightly bias the accuracy, but in order to replicate the - results in the paper, it would be necessary to additionally remove these - sites from the test dataframe. - -**References** - `Kia et al -2021 `__ -- `Rosen et al -2018 `__ +**Note:** For the purposes of this tutorial, we make predictions for a +multi-site transfer dataset, derived from +`OpenNeuro `__. .. code:: ipython3 - test_data = os.path.join(root_dir, 'OpenNeuroTransfer_ct_te.csv') + test_data = os.path.join(data_dir, 'OpenNeuroTransfer_ct_te.csv') df_te = pd.read_csv(test_data) - # remove some bad subjects, this requires having a column called "avg_en" that corresponds to the average Euler number extracted from Freesurfer - # df_te, bad_sub = remove_bad_subjects(df_te, df_te) - # extract a list of unique site ids from the test set site_ids_te = sorted(set(df_te['site'].to_list())) (Optional) Load adaptation data ------------------------------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If the data you wish to make predictions for is not derived from the same scanning sites as those in the trainig set, it is necessary to @@ -207,13 +120,10 @@ same way, based on a the ‘sitenum’ column in the dataframe. .. code:: ipython3 - adaptation_data = os.path.join(root_dir, 'OpenNeuroTransfer_ct_tr.csv') + adaptation_data = os.path.join(data_dir, 'OpenNeuroTransfer_ct_ad.csv') df_ad = pd.read_csv(adaptation_data) - # remove some bad subjects, this requires having a column called "avg_en" that corresponds to the average Euler number extracted from Freesurfer - # df_ad, bad_sub = remove_bad_subjects(df_ad, df_ad) - # extract a list of unique site ids from the test set site_ids_ad = sorted(set(df_ad['site'].to_list())) @@ -221,7 +131,7 @@ same way, based on a the ‘sitenum’ column in the dataframe. print('Warning: some of the testing sites are not in the adaptation data') Configure which models to fit ------------------------------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now, we configure which imaging derived phenotypes (IDPs) we would like to process. This is just a list of column names in the dataframe we have @@ -233,15 +143,15 @@ models for … .. code:: ipython3 # load the list of idps for left and right hemispheres, plus subcortical regions - with open(os.path.join(root_dir,'docs','phenotypes_ct_lh.txt')) as f: + with open(os.path.join(data_dir, 'phenotypes_ct_lh.txt')) as f: idp_ids_lh = f.read().splitlines() - with open(os.path.join(root_dir,'docs','phenotypes_ct_rh.txt')) as f: + with open(os.path.join(data_dir, 'phenotypes_ct_rh.txt')) as f: idp_ids_rh = f.read().splitlines() - with open(os.path.join(root_dir,'docs','phenotypes_sc.txt')) as f: + with open(os.path.join(data_dir, 'phenotypes_sc.txt')) as f: idp_ids_sc = f.read().splitlines() # we choose here to process all idps - idp_ids = idp_ids_lh + idp_ids_rh + idp_ids_sc + idp_ids = idp_ids_lh + idp_ids_rh #+ idp_ids_sc … or alternatively, we could just specify a list @@ -250,7 +160,7 @@ models for … idp_ids = [ 'Left-Thalamus-Proper', 'Left-Lateral-Ventricle', 'rh_MeanThickness_thickness'] Configure covariates ------------------------------------------------------ +~~~~~~~~~~~~~~~~~~~~ Now, we configure some parameters to fit the model. First, we choose which columns of the pandas dataframe contain the covariates (age and @@ -274,18 +184,18 @@ accompanying paper and `Fraza et al .. code:: ipython3 - # which data columns do we wish to use as covariates? + # which data columns do we wish to use as covariates? cols_cov = ['age','sex'] - # limits for cubic B-spline basis - xmin = -5 + # limits for cubic B-spline basis + xmin = -5 xmax = 110 # Absolute Z treshold above which a sample is considered to be an outlier (without fitting any model) outlier_thresh = 7 Make predictions ------------------------------------------------------ +~~~~~~~~~~~~~~~~ This will make predictions for each IDP separately. This is done by extracting a column from the dataframe (i.e. specifying the IDP as the @@ -294,12 +204,12 @@ the covariates, which is a numpy data array having the number of rows equal to the number of datapoints in the test set. The columns are specified as follows: -- A global intercept (column of ones) -- The covariate columns (here age and sex, coded as 0=female/1=male) -- Dummy coded columns for the sites in the training set (one column per - site) -- Columns for the basis expansion (seven columns for the default - parameterisation) +- A global intercept (column of ones) +- The covariate columns (here age and sex, coded as 0=female/1=male) +- Dummy coded columns for the sites in the training set (one column per + site) +- Columns for the basis expansion (seven columns for the default + parameterisation) Once these are saved as numpy arrays in ascii format (as here) or (alternatively) in pickle format, these are passed as inputs to the @@ -309,74 +219,74 @@ These are written in the same format to the location specified by Z-statistics for the test dataset that we can take forward to further analysis. -When we need to make predictions on new data, the procedure is +Note that when we need to make predictions on new data, the procedure is more involved, since we need to prepare, process and store covariates, response variables and site ids for the adaptation data. .. code:: ipython3 - for idp_num, idp in enumerate(idp_ids): + for idp_num, idp in enumerate(idp_ids): print('Running IDP', idp_num, idp, ':') idp_dir = os.path.join(out_dir, idp) os.chdir(idp_dir) - + # extract and save the response variables for the test set y_te = df_te[idp].to_numpy() - + # save the variables - resp_file_te = os.path.join(idp_dir, 'resp_te.txt') + resp_file_te = os.path.join(idp_dir, 'resp_te.txt') np.savetxt(resp_file_te, y_te) - + # configure and save the design matrix cov_file_te = os.path.join(idp_dir, 'cov_bspline_te.txt') - X_te = create_design_matrix(df_te[cols_cov], + X_te = create_design_matrix(df_te[cols_cov], site_ids = df_te['site'], all_sites = site_ids_tr, - basis = 'bspline', - xmin = xmin, + basis = 'bspline', + xmin = xmin, xmax = xmax) np.savetxt(cov_file_te, X_te) - + # check whether all sites in the test set are represented in the training set if all(elem in site_ids_tr for elem in site_ids_te): print('All sites are present in the training data') - + # just make predictions - yhat_te, s2_te, Z = predict(cov_file_te, - alg='blr', - respfile=resp_file_te, + yhat_te, s2_te, Z = predict(cov_file_te, + alg='blr', + respfile=resp_file_te, model_path=os.path.join(idp_dir,'Models')) else: print('Some sites missing from the training data. Adapting model') - + # save the covariates for the adaptation data - X_ad = create_design_matrix(df_ad[cols_cov], + X_ad = create_design_matrix(df_ad[cols_cov], site_ids = df_ad['site'], all_sites = site_ids_tr, - basis = 'bspline', - xmin = xmin, + basis = 'bspline', + xmin = xmin, xmax = xmax) - cov_file_ad = os.path.join(idp_dir, 'cov_bspline_ad.txt') + cov_file_ad = os.path.join(idp_dir, 'cov_bspline_ad.txt') np.savetxt(cov_file_ad, X_ad) - + # save the responses for the adaptation data - resp_file_ad = os.path.join(idp_dir, 'resp_ad.txt') + resp_file_ad = os.path.join(idp_dir, 'resp_ad.txt') y_ad = df_ad[idp].to_numpy() np.savetxt(resp_file_ad, y_ad) - + # save the site ids for the adaptation data - sitenum_file_ad = os.path.join(idp_dir, 'sitenum_ad.txt') + sitenum_file_ad = os.path.join(idp_dir, 'sitenum_ad.txt') site_num_ad = df_ad['sitenum'].to_numpy(dtype=int) np.savetxt(sitenum_file_ad, site_num_ad) - - # save the site ids for the test data + + # save the site ids for the test data sitenum_file_te = os.path.join(idp_dir, 'sitenum_te.txt') site_num_te = df_te['sitenum'].to_numpy(dtype=int) np.savetxt(sitenum_file_te, site_num_te) - - yhat_te, s2_te, Z = predict(cov_file_te, - alg = 'blr', - respfile = resp_file_te, + + yhat_te, s2_te, Z = predict(cov_file_te, + alg = 'blr', + respfile = resp_file_te, model_path = os.path.join(idp_dir,'Models'), adaptrespfile = resp_file_ad, adaptcovfile = cov_file_ad, @@ -409,16 +319,8 @@ response variables and site ids for the adaptation data. Writing outputs ... -Evaluate the performance ------------------------------------------------------ - -.. figure:: ./brainchart_fig3.png - :height: 400px - :align: center - - Preparing dummy data for plotting ------------------------------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now, we plot the centiles of variation estimated by the normative model. @@ -431,9 +333,9 @@ dummy data for all the IDPs we wish to plot .. code:: ipython3 - # which sex do we want to plot? + # which sex do we want to plot? sex = 1 # 1 = male 0 = female - if sex == 1: + if sex == 1: clr = 'blue'; else: clr = 'red' @@ -459,7 +361,7 @@ dummy data for all the IDPs we wish to plot Plotting the normative models ------------------------------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now we loop through the IDPs, plotting each one separately. The outputs of this step are a set of quantitative regression metrics for each IDP @@ -474,83 +376,82 @@ space, then we need to warp them with the inverse of the estimated warping function. This can be done using the function ``nm.blr.warp.warp_predictions()``. -.. note:: - It is necessary to update the intercept for each of the sites. - For purposes of visualisation, here we do this by adjusting the median - of the data to match the dummy predictions, but note that all the - quantitative metrics are estimated using the predictions that are - adjusted properly using a learned offset (or adjusted using a hold-out - adaptation set, as above). Note also that for the calibration data we - require at least two data points of the same sex in each site to be able - to estimate the variance. Of course, in a real example, you would want - many more than just two since we need to get a reliable estimate of the - variance for each site. +**Note:** it is necessary to update the intercept for each of the sites. +For purposes of visualisation, here we do this by adjusting the median +of the data to match the dummy predictions, but note that all the +quantitative metrics are estimated using the predictions that are +adjusted properly using a learned offset (or adjusted using a hold-out +adaptation set, as above). Note also that for the calibration data we +require at least two data points of the same sex in each site to be able +to estimate the variance. Of course, in a real example, you would want +many more than just two since we need to get a reliable estimate of the +variance for each site. .. code:: ipython3 sns.set(style='whitegrid') - for idp_num, idp in enumerate(idp_ids): + for idp_num, idp in enumerate(idp_ids): print('Running IDP', idp_num, idp, ':') idp_dir = os.path.join(out_dir, idp) os.chdir(idp_dir) - + # load the true data points yhat_te = load_2d(os.path.join(idp_dir, 'yhat_predict.txt')) s2_te = load_2d(os.path.join(idp_dir, 'ys2_predict.txt')) y_te = load_2d(os.path.join(idp_dir, 'resp_te.txt')) - + # set up the covariates for the dummy data print('Making predictions with dummy covariates (for visualisation)') - yhat, s2 = predict(cov_file_dummy, - alg = 'blr', - respfile = None, - model_path = os.path.join(idp_dir,'Models'), + yhat, s2 = predict(cov_file_dummy, + alg = 'blr', + respfile = None, + model_path = os.path.join(idp_dir,'Models'), outputsuffix = '_dummy') - + # load the normative model with open(os.path.join(idp_dir,'Models', 'NM_0_0_estimate.pkl'), 'rb') as handle: - nm = pickle.load(handle) - + nm = pickle.load(handle) + # get the warp and warp parameters W = nm.blr.warp - warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] - + warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] + # first, we warp predictions for the true data and compute evaluation metrics med_te = W.warp_predictions(np.squeeze(yhat_te), np.squeeze(s2_te), warp_param)[0] med_te = med_te[:, np.newaxis] print('metrics:', evaluate(y_te, med_te)) - + # then, we warp dummy predictions to create the plots med, pr_int = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2), warp_param) - + # extract the different variance components to visualise beta, junk1, junk2 = nm.blr._parse_hyps(nm.blr.hyp, X_dummy) s2n = 1/beta # variation (aleatoric uncertainty) s2s = s2-s2n # modelling uncertainty (epistemic uncertainty) - + # plot the data points y_te_rescaled_all = np.zeros_like(y_te) for sid, site in enumerate(site_ids_te): - # plot the true test data points + # plot the true test data points if all(elem in site_ids_tr for elem in site_ids_te): # all data in the test set are present in the training set - + # first, we select the data points belonging to this particular site idx = np.where(np.bitwise_and(X_te[:,2] == sex, X_te[:,sid+len(cols_cov)+1] !=0))[0] if len(idx) == 0: print('No data for site', sid, site, 'skipping...') continue - + # then directly adjust the data idx_dummy = np.bitwise_and(X_dummy[:,1] > X_te[idx,1].min(), X_dummy[:,1] < X_te[idx,1].max()) y_te_rescaled = y_te[idx] - np.median(y_te[idx]) + np.median(med[idx_dummy]) else: - # we need to adjust the data based on the adaptation dataset - + # we need to adjust the data based on the adaptation dataset + # first, select the data point belonging to this particular site idx = np.where(np.bitwise_and(X_te[:,2] == sex, (df_te['site'] == site).to_numpy()))[0] - + # load the adaptation data y_ad = load_2d(os.path.join(idp_dir, 'resp_ad.txt')) X_ad = load_2d(os.path.join(idp_dir, 'cov_bspline_ad.txt')) @@ -558,19 +459,19 @@ warping function. This can be done using the function if len(idx) < 2 or len(idx_a) < 2: print('Insufficent data for site', sid, site, 'skipping...') continue - + # adjust and rescale the data - y_te_rescaled, s2_rescaled = nm.blr.predict_and_adjust(nm.blr.hyp, - X_ad[idx_a,:], - np.squeeze(y_ad[idx_a]), - Xs=None, + y_te_rescaled, s2_rescaled = nm.blr.predict_and_adjust(nm.blr.hyp, + X_ad[idx_a,:], + np.squeeze(y_ad[idx_a]), + Xs=None, ys=np.squeeze(y_te[idx])) # plot the (adjusted) data points plt.scatter(X_te[idx,1], y_te_rescaled, s=4, color=clr, alpha = 0.1) - + # plot the median of the dummy data plt.plot(xx, med, clr) - + # fill the gaps in between the centiles junk, pr_int25 = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2), warp_param, percentiles=[0.25,0.75]) junk, pr_int95 = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2), warp_param, percentiles=[0.05,0.95]) @@ -578,14 +479,14 @@ warping function. This can be done using the function plt.fill_between(xx, pr_int25[:,0], pr_int25[:,1], alpha = 0.1,color=clr) plt.fill_between(xx, pr_int95[:,0], pr_int95[:,1], alpha = 0.1,color=clr) plt.fill_between(xx, pr_int99[:,0], pr_int99[:,1], alpha = 0.1,color=clr) - + # make the width of each centile proportional to the epistemic uncertainty junk, pr_int25l = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2-0.5*s2s), warp_param, percentiles=[0.25,0.75]) junk, pr_int95l = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2-0.5*s2s), warp_param, percentiles=[0.05,0.95]) junk, pr_int99l = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2-0.5*s2s), warp_param, percentiles=[0.01,0.99]) junk, pr_int25u = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2+0.5*s2s), warp_param, percentiles=[0.25,0.75]) junk, pr_int95u = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2+0.5*s2s), warp_param, percentiles=[0.05,0.95]) - junk, pr_int99u = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2+0.5*s2s), warp_param, percentiles=[0.01,0.99]) + junk, pr_int99u = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2+0.5*s2s), warp_param, percentiles=[0.01,0.99]) plt.fill_between(xx, pr_int25l[:,0], pr_int25u[:,0], alpha = 0.3,color=clr) plt.fill_between(xx, pr_int95l[:,0], pr_int95u[:,0], alpha = 0.3,color=clr) plt.fill_between(xx, pr_int99l[:,0], pr_int99u[:,0], alpha = 0.3,color=clr) @@ -600,14 +501,14 @@ warping function. This can be done using the function plt.plot(xx, pr_int95[:,1],color=clr, linewidth=0.5) plt.plot(xx, pr_int99[:,0],color=clr, linewidth=0.5) plt.plot(xx, pr_int99[:,1],color=clr, linewidth=0.5) - + plt.xlabel('Age') - plt.ylabel(idp) + plt.ylabel(idp) plt.title(idp) plt.xlim((0,90)) plt.savefig(os.path.join(idp_dir, 'centiles_' + str(sex)), bbox_inches='tight') plt.show() - + os.chdir(out_dir) @@ -622,7 +523,7 @@ warping function. This can be done using the function -.. image:: apply_normative_models_files/apply_normative_models_29_1.png +.. image:: apply_normative_models_files/apply_normative_models_ct_27_1.png .. parsed-literal:: @@ -636,7 +537,7 @@ warping function. This can be done using the function -.. image:: apply_normative_models_files/apply_normative_models_29_3.png +.. image:: apply_normative_models_files/apply_normative_models_ct_27_3.png .. parsed-literal:: @@ -650,24 +551,23 @@ warping function. This can be done using the function -.. image:: apply_normative_models_files/apply_normative_models_29_5.png +.. image:: apply_normative_models_files/apply_normative_models_ct_27_5.png .. code:: ipython3 # explore an example output folder of a single model (one ROI) - # think about what each of these output files represents. + # think about what each of these output files represents. # Hint: look at the variable names and comments in the code block above ! ls rh_MeanThickness_thickness/ .. parsed-literal:: - centiles_1.png MSLL_predict.txt RMSE_predict.txt yhat_predict.txt - cov_bspline_ad.txt pRho_predict.txt sitenum_ad.txt ys2_dummy.pkl - cov_bspline_te.txt resp_ad.txt sitenum_te.txt ys2_predict.txt - EXPV_predict.txt resp_te.txt SMSE_predict.txt Z_predict.txt - Models Rho_predict.txt yhat_dummy.pkl + centiles_1.png Models Rho_predict.txt SMSE_predict.txt ys2_predict.txt + cov_bspline_ad.txt pRho_predict.txt RMSE_predict.txt yhat_dummy.pkl Z_predict.txt + cov_bspline_te.txt resp_ad.txt sitenum_ad.txt yhat_predict.txt + EXPV_predict.txt resp_te.txt sitenum_te.txt ys2_dummy.pkl .. code:: ipython3 @@ -698,7 +598,8 @@ into the original data file. .. code:: ipython3 - z_dir = '/content/braincharts/models/lifespan_57K_82sites/deviation_scores/' + z_dir = os.path.join(root_dir, 'models', model_name, 'deviation_scores') + filelist = [name for name in os.listdir(z_dir)] .. code:: ipython3 diff --git a/doc/build/html/_sources/pages/installation.rst.txt b/doc/build/html/_sources/pages/installation.rst.txt index ce1551ce..18e50880 100644 --- a/doc/build/html/_sources/pages/installation.rst.txt +++ b/doc/build/html/_sources/pages/installation.rst.txt @@ -20,13 +20,19 @@ Basic installation (on a local machine) source activate -4. Install required conda packages +4. Install torch using the torch instructions. + +.. code-block:: bash + + # Command found on the torch website: https://pytorch.org/get-started/locally/ + +5. Install required conda packages .. code-block:: bash - conda install pip pandas scipy + conda install numba nutpie -c conda-forge -5. Install PCNtoolkit (plus dependencies) +6. Install PCNtoolkit (plus dependencies) .. code-block:: bash @@ -60,7 +66,8 @@ Alternative installation (on a shared resource) .. code-block:: bash - conda install -y pandas scipy + # Command found on the torch website: https://pytorch.org/get-started/locally/ + conda install numba nutpie -c conda-forge 5. Install pip dependencies diff --git a/doc/build/html/_sources/pages/normative_modelling_walkthrough.rst.txt b/doc/build/html/_sources/pages/normative_modelling_walkthrough.rst.txt index f63ec8fa..5bbc326e 100644 --- a/doc/build/html/_sources/pages/normative_modelling_walkthrough.rst.txt +++ b/doc/build/html/_sources/pages/normative_modelling_walkthrough.rst.txt @@ -1,6 +1,4 @@ -.. title:: GPR tutorial - -Gaussian Process Regression +**DEMO ON NORMATIVE MODELING** ============================== Created by @@ -10,14 +8,9 @@ Mariam Zabihi `@m_zabihi `__ Saige Rutherford `@being_saige `__ Thomas Wolfers `@ThomasWolfers `__ -\______________________________________________________________________________\_ - - -.. image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/predictive-clinical-neuroscience/PCNtoolkit-demo/blob/main/tutorials/CPC_2020/normative_modelling_walkthrough.ipynb - +\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ -Background Story +**Background Story** -------------------- Morten and Ingrid are concerned about the health of their father, @@ -40,13 +33,13 @@ IQ, age as well as the same sex as Nordan. Do your best to get as far as you can. However, you do not need to feel bad if you cannot complete everything during the tutorial. -**Task 0:** Load data and install the pcntoolkit ------------------------------------------------- +**Task 0:** Load data and install PCNtoolkit +-------------------------------------------- .. code:: ipython3 - #install normative modeling - ! pip install pcntoolkit==0.26 + !pip install pcntoolkit + !pip install nutpie **Option 1:** Connect your Google Drive account, and load data from @@ -77,56 +70,8 @@ them to Google Drive. !wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_demographics_nordan.csv !wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_features.csv !wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_features_nordan.csv - - # code by S. Rutherford - - -.. parsed-literal:: - - --2022-02-17 15:03:58-- https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_demographics.csv - Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.108.133, 185.199.111.133, ... - Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected. - HTTP request sent, awaiting response... 200 OK - Length: 17484 (17K) [text/plain] - Saving to: ‘camcan_demographics.csv’ - - camcan_demographics 100%[===================>] 17.07K --.-KB/s in 0.001s - - 2022-02-17 15:03:58 (12.9 MB/s) - ‘camcan_demographics.csv’ saved [17484/17484] - - --2022-02-17 15:03:58-- https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_demographics_nordan.csv - Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ... - Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected. - HTTP request sent, awaiting response... 200 OK - Length: 332 [text/plain] - Saving to: ‘camcan_demographics_nordan.csv’ - - camcan_demographics 100%[===================>] 332 --.-KB/s in 0s - - 2022-02-17 15:03:58 (15.5 MB/s) - ‘camcan_demographics_nordan.csv’ saved [332/332] - - --2022-02-17 15:03:58-- https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_features.csv - Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ... - Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected. - HTTP request sent, awaiting response... 200 OK - Length: 188944 (185K) [text/plain] - Saving to: ‘camcan_features.csv’ - - camcan_features.csv 100%[===================>] 184.52K --.-KB/s in 0.05s - - 2022-02-17 15:03:58 (3.88 MB/s) - ‘camcan_features.csv’ saved [188944/188944] - - --2022-02-17 15:03:58-- https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_features_nordan.csv - Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ... - Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected. - HTTP request sent, awaiting response... 200 OK - Length: 1695 (1.7K) [text/plain] - Saving to: ‘camcan_features_nordan.csv’ - - camcan_features_nor 100%[===================>] 1.66K --.-KB/s in 0s - - 2022-02-17 15:03:59 (25.3 MB/s) - ‘camcan_features_nordan.csv’ saved [1695/1695] + # code by S. Rutherford **TASK 1:** Format input data @@ -160,7 +105,7 @@ between those two files. print(norm_demographics) print(norm_features) - # find overlap in terms of participants between norm_sample_features and + # find overlap in terms of participants between norm_sample_features and # norm_sample_demographics norm_demographics_features = pd.concat([norm_demographics, norm_features], @@ -172,54 +117,6 @@ between those two files. # code by T. Wolfers -.. parsed-literal:: - - age sex_name sex IQ_random - paricipants - CC110033 24 MALE 1 73 - CC110037 18 MALE 1 103 - CC110045 24 FEMALE 0 124 - CC110056 22 FEMALE 0 124 - CC110062 20 MALE 1 126 - ... ... ... ... ... - CC722542 79 MALE 1 116 - CC722651 79 FEMALE 0 128 - CC722891 84 FEMALE 0 129 - CC723197 80 FEMALE 0 96 - CC723395 86 FEMALE 0 145 - - [707 rows x 4 columns] - left_Hippocampal_tail ... right_Whole_hippocampus - participants ... - CC110033 482.768229 ... 3531.764896 - CC110037 595.269259 ... 3835.426137 - CC110045 655.847194 ... 3681.494304 - CC110056 561.345626 ... 3461.373764 - CC110062 756.521166 ... 4782.407821 - ... ... ... ... - CC722542 467.896808 ... 3284.108783 - CC722651 406.326167 ... 3210.272905 - CC722891 393.430481 ... 2423.675065 - CC723197 475.929914 ... 3043.146264 - CC723395 444.301617 ... 2988.001288 - - [651 rows x 26 columns] - age sex_name sex ... right_fimbria right_HATA right_Whole_hippocampus - CC110033 24 MALE 1 ... 87.127463 73.589184 3531.764896 - CC110037 18 MALE 1 ... 99.657823 60.920924 3835.426137 - CC110045 24 FEMALE 0 ... 69.436808 59.323542 3681.494304 - CC110056 22 FEMALE 0 ... 60.505521 51.726283 3461.373764 - CC110062 20 MALE 1 ... 92.215816 85.484454 4782.407821 - ... ... ... ... ... ... ... ... - CC722542 79 MALE 1 ... 46.144212 43.966509 3284.108783 - CC722651 79 FEMALE 0 ... 68.730322 59.699644 3210.272905 - CC722891 84 FEMALE 0 ... 27.913196 38.629828 2423.675065 - CC723197 80 FEMALE 0 ... 51.893458 65.474967 3043.146264 - CC723395 86 FEMALE 0 ... 68.335159 62.081225 2988.001288 - - [650 rows x 30 columns] - - **TASK 2:** Prepare the covariate_normsample and testresponse_normsample file. ------------------------------------------------------------------------------ @@ -252,22 +149,22 @@ putative biomarkers that are not restricted to brain imaging. # perpare covariate_normsample for sex and age covariate_normsample = norm_demographics_features[['sex', - 'age']] + 'age']] covariate_normsample.to_csv('covariate_normsample.txt', sep = ' ', - header = False, + header = False, index = False) # perpare features_normsample for relevant hyppocampal subfields - features_normsample = norm_demographics_features[['left_CA1', + features_normsample = norm_demographics_features[['left_CA1', 'left_CA3', 'right_CA1', 'right_CA3']] - features_normsample.to_csv('features_normsample.txt', - sep = ' ', - header = False, + features_normsample.to_csv('features_normsample.txt', + sep = ' ', + header = False, index = False) # code by T. Wolfers @@ -278,7 +175,7 @@ putative biomarkers that are not restricted to brain imaging. Once you have prepared and saved all the necessary files. Look at the pcntoolkit for running normative modeling. Select an appropritate method set up the toolkit and run your analyses using 2-fold cross validation -in the normsample. Change the output suffix from estimate to ’_2fold’. +in the normsample. Change the output suffix from estimate to ’\_2fold’. HINT: You primarily need the estimate function. @@ -296,7 +193,7 @@ you will have no doubt when it is correctly running. # run normative modeling using 2-fold cross-validation - pcn.normative.estimate(covfile = 'covariate_normsample.txt', + pcn.normative.estimate(covfile = 'covariate_normsample.txt', respfile = 'features_normsample.txt', cvfolds = 2, alg = 'gpr', @@ -307,55 +204,66 @@ you will have no doubt when it is correctly running. .. parsed-literal:: + inscaler: None + outscaler: None Processing data in features_normsample.txt Estimating model 1 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed - Warning: Estimation of posterior distribution failed + + +.. parsed-literal:: + + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:147: RuntimeWarning: overflow encountered in exp + self.sf2 = np.exp(2*theta[self.D]) + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:160: RuntimeWarning: invalid value encountered in multiply + dK = K * squared_dist(x[:, i]/self.ell[i], x[:, i]/self.ell[i]) + + +.. parsed-literal:: + Optimization terminated successfully. - Current function value: 1856.502251 - Iterations: 40 - Function evaluations: 99 - Gradient evaluations: 99 + Current function value: 1925.145213 + Iterations: 30 + Function evaluations: 75 + Gradient evaluations: 69 Estimating model 2 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed - Optimization terminated successfully. - Current function value: 1596.239263 - Iterations: 42 - Function evaluations: 93 - Gradient evaluations: 93 - Estimating model 3 of 4 - Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed - Optimization terminated successfully. - Current function value: 1862.316698 - Iterations: 47 - Function evaluations: 104 - Gradient evaluations: 104 - Estimating model 4 of 4 - Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Optimization terminated successfully. - Current function value: 1587.950935 - Iterations: 30 - Function evaluations: 64 - Gradient evaluations: 64 - Estimating model 1 of 4 + Current function value: 1627.864114 + Iterations: 41 + Function evaluations: 102 + Gradient evaluations: 102 + Estimating model 3 of 4 + + +.. parsed-literal:: + + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:147: RuntimeWarning: overflow encountered in exp + self.sf2 = np.exp(2*theta[self.D]) + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:160: RuntimeWarning: invalid value encountered in multiply + dK = K * squared_dist(x[:, i]/self.ell[i], x[:, i]/self.ell[i]) + + +.. parsed-literal:: + Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Optimization terminated successfully. - Current function value: 1916.461484 - Iterations: 44 - Function evaluations: 94 - Gradient evaluations: 87 - Estimating model 2 of 4 + Current function value: 1922.205071 + Iterations: 30 + Function evaluations: 73 + Gradient evaluations: 67 + Estimating model 4 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -365,36 +273,48 @@ you will have no doubt when it is correctly running. Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Optimization terminated successfully. - Current function value: 1611.661888 - Iterations: 34 - Function evaluations: 85 - Gradient evaluations: 85 - Estimating model 3 of 4 - Warning: Estimation of posterior distribution failed + Current function value: 1621.445961 + Iterations: 78 + Function evaluations: 181 + Gradient evaluations: 181 + Estimating model 1 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Optimization terminated successfully. - Current function value: 1912.665851 - Iterations: 61 - Function evaluations: 133 - Gradient evaluations: 126 - Estimating model 4 of 4 + Current function value: 1844.061877 + Iterations: 36 + Function evaluations: 81 + Gradient evaluations: 81 + Estimating model 2 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed + Optimization terminated successfully. + Current function value: 1580.315780 + Iterations: 37 + Function evaluations: 79 + Gradient evaluations: 79 + Estimating model 3 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed + Optimization terminated successfully. + Current function value: 1851.005493 + Iterations: 32 + Function evaluations: 68 + Gradient evaluations: 68 + Estimating model 4 of 4 + Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Optimization terminated successfully. - Current function value: 1619.045647 - Iterations: 43 - Function evaluations: 110 - Gradient evaluations: 105 + Current function value: 1584.089863 + Iterations: 39 + Function evaluations: 91 + Gradient evaluations: 91 Evaluating the model ... Writing outputs ... @@ -423,13 +343,13 @@ model using the appropriate specifications. 20, 30, 40, 50, 60, 70, 80]} covariate_forwardmodel = pd.DataFrame(data=covariate_forwardmodel) - covariate_forwardmodel.to_csv('covariate_forwardmodel.txt', - sep = ' ', - header = False, + covariate_forwardmodel.to_csv('covariate_forwardmodel.txt', + sep = ' ', + header = False, index = False) # estimate forward model - pcn.normative.estimate(covfile = 'covariate_normsample.txt', + pcn.normative.estimate(covfile = 'covariate_normsample.txt', respfile = 'features_normsample.txt', testcov = 'covariate_forwardmodel.txt', cvfolds = None, @@ -441,8 +361,22 @@ model using the appropriate specifications. .. parsed-literal:: + inscaler: None + outscaler: None Processing data in features_normsample.txt Estimating model 1 of 4 + + +.. parsed-literal:: + + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:147: RuntimeWarning: overflow encountered in exp + self.sf2 = np.exp(2*theta[self.D]) + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:160: RuntimeWarning: invalid value encountered in multiply + dK = K * squared_dist(x[:, i]/self.ell[i], x[:, i]/self.ell[i]) + + +.. parsed-literal:: + Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -451,8 +385,8 @@ model using the appropriate specifications. Optimization terminated successfully. Current function value: 3781.497401 Iterations: 20 - Function evaluations: 61 - Gradient evaluations: 54 + Function evaluations: 58 + Gradient evaluations: 52 Estimating model 2 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -466,10 +400,22 @@ model using the appropriate specifications. Warning: Estimation of posterior distribution failed Optimization terminated successfully. Current function value: 3201.761309 - Iterations: 39 - Function evaluations: 108 - Gradient evaluations: 108 + Iterations: 48 + Function evaluations: 114 + Gradient evaluations: 114 Estimating model 3 of 4 + + +.. parsed-literal:: + + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:147: RuntimeWarning: overflow encountered in exp + self.sf2 = np.exp(2*theta[self.D]) + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:160: RuntimeWarning: invalid value encountered in multiply + dK = K * squared_dist(x[:, i]/self.ell[i], x[:, i]/self.ell[i]) + + +.. parsed-literal:: + Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -480,9 +426,9 @@ model using the appropriate specifications. Warning: Estimation of posterior distribution failed Optimization terminated successfully. Current function value: 3771.310488 - Iterations: 47 - Function evaluations: 181 - Gradient evaluations: 167 + Iterations: 48 + Function evaluations: 156 + Gradient evaluations: 143 Estimating model 4 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -496,9 +442,9 @@ model using the appropriate specifications. Warning: Estimation of posterior distribution failed Optimization terminated successfully. Current function value: 3200.837262 - Iterations: 40 - Function evaluations: 104 - Gradient evaluations: 104 + Iterations: 42 + Function evaluations: 116 + Gradient evaluations: 116 Writing outputs ... @@ -508,7 +454,8 @@ model using the appropriate specifications. Visualize the forward model of the normative model similar to the figure below. -.. figure:: data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAcQBxAAD/4QDoRXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAAcAAAAcgEyAAIAAAAUAAAAjodpAAQAAAABAAAAogAAAAAAAABxAAAAAQAAAHEAAAABQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzADIwMTk6MDE6MTAgMjE6MDA6MDYAAAOQBAACAAAAFAAAAMygAgAEAAAAAQAAAMmgAwAEAAAAAQAAAHIAAAAAMjAxODoxMjoxMSAxNDozNToxNwD/4RGtaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQuMCI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFNTg4MUU5QkVDMTRFOTExOEI3RkU1RDU4RURGM0Q0RSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyRUM1RUY5ODQ5RkRFODExQTNBM0EyODcyMEY2OTA2NCIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjJFQzVFRjk4NDlGREU4MTFBM0EzQTI4NzIwRjY5MDY0IiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0xMFQyMTowMDowNiswNTozMCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDEtMTBUMjE6MDA6MDYrMDU6MzAiIHhtcDpDcmVhdGVEYXRlPSIyMDE4LTEyLTExVDE0OjM1OjE3KzAxOjAwIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0iRUNJLVJHQi5pY2MiIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIGRjOmZvcm1hdD0iaW1hZ2UvanBlZyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjpvcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MkVDNUVGOTg0OUZERTgxMUEzQTNBMjg3MjBGNjkwNjQiIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RTQ4ODFFOUJFQzE0RTkxMThCN0ZFNUQ1OEVERjNENEUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkVDNUVGOTg0OUZERTgxMUEzQTNBMjg3MjBGNjkwNjQiLz4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiBzdEV2dDp3aGVuPSIyMDE4LTEyLTExVDE0OjM1OjE3KzAxOjAwIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjJFQzVFRjk4NDlGREU4MTFBM0EzQTI4NzIwRjY5MDY0IiBzdEV2dDphY3Rpb249ImNyZWF0ZWQiLz4gPHJkZjpsaSBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0OndoZW49IjIwMTktMDEtMTBUMjA6NTE6MDErMDU6MzAiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6N0QzQ0MyOERFQTE0RTkxMTlFRTJGRkMzQkZENTkzMUUiIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiLz4gPHJkZjpsaSBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0OndoZW49IjIwMTktMDEtMTBUMjA6NTE6MDErMDU6MzAiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6N0UzQ0MyOERFQTE0RTkxMTlFRTJGRkMzQkZENTkzMUUiIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiLz4gPHJkZjpsaSBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0OndoZW49IjIwMTktMDEtMTBUMjE6MDA6MDYrMDU6MzAiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6RTQ4ODFFOUJFQzE0RTkxMThCN0ZFNUQ1OEVERjNENEUiIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNvbnZlcnRlZCIgc3RFdnQ6cGFyYW1ldGVycz0iZnJvbSBpbWFnZS90aWZmIHRvIGltYWdlL2pwZWciLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImRlcml2ZWQiIHN0RXZ0OnBhcmFtZXRlcnM9ImNvbnZlcnRlZCBmcm9tIGltYWdlL3RpZmYgdG8gaW1hZ2UvanBlZyIvPiA8cmRmOmxpIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDUzUgV2luZG93cyIgc3RFdnQ6Y2hhbmdlZD0iLyIgc3RFdnQ6d2hlbj0iMjAxOS0wMS0xMFQyMTowMDowNiswNTozMCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpFNTg4MUU5QkVDMTRFOTExOEI3RkU1RDU4RURGM0Q0RSIgc3RFdnQ6YWN0aW9uPSJzYXZlZCIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/PgD/7QBkUGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAACwcAVoAAxslRxwCAAACAAIcAj4ACDIwMTgxMjExHAI/AAsxNDM1MTcrMDEwMDhCSU0EJQAAAAAAEG8VJXtBp2YwhKsNALMYCvf/4gIsSUNDX1BST0ZJTEUAAQEAAAIcQURCRQIQAABtbnRyUkdCIFhZWiAHzwACABYADAADAA9hY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAACRkZXNjAAABIAAAAGd3dHB0AAABiAAAABRia3B0AAABnAAAABRyVFJDAAABsAAAAA5nVFJDAAABwAAAAA5iVFJDAAAB0AAAAA5yWFlaAAAB4AAAABRnWFlaAAAB9AAAABRiWFlaAAACCAAAABR0ZXh0AAAAAChjKSAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jLgBkZXNjAAAAAAAAAAxFQ0ktUkdCLmljYwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAAD23AABAAAAANM6WFlaIAAAAAAAAAAAAAAAAAAAAABjdXJ2AAAAAAAAAAEBzQAAY3VydgAAAAAAAAABAc0AAGN1cnYAAAAAAAAAAQHNAABYWVogAAAAAAAApngAAFH+AAAA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 +.. figure:: + data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAcQBxAAD/4QDoRXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAAcAAAAcgEyAAIAAAAUAAAAjodpAAQAAAABAAAAogAAAAAAAABxAAAAAQAAAHEAAAABQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzADIwMTk6MDE6MTAgMjE6MDA6MDYAAAOQBAACAAAAFAAAAMygAgAEAAAAAQAAAMmgAwAEAAAAAQAAAHIAAAAAMjAxODoxMjoxMSAxNDozNToxNwD/4RGtaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQuMCI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFNTg4MUU5QkVDMTRFOTExOEI3RkU1RDU4RURGM0Q0RSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyRUM1RUY5ODQ5RkRFODExQTNBM0EyODcyMEY2OTA2NCIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjJFQzVFRjk4NDlGREU4MTFBM0EzQTI4NzIwRjY5MDY0IiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0xMFQyMTowMDowNiswNTozMCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDEtMTBUMjE6MDA6MDYrMDU6MzAiIHhtcDpDcmVhdGVEYXRlPSIyMDE4LTEyLTExVDE0OjM1OjE3KzAxOjAwIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0iRUNJLVJHQi5pY2MiIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIGRjOmZvcm1hdD0iaW1hZ2UvanBlZyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjpvcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MkVDNUVGOTg0OUZERTgxMUEzQTNBMjg3MjBGNjkwNjQiIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RTQ4ODFFOUJFQzE0RTkxMThCN0ZFNUQ1OEVERjNENEUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkVDNUVGOTg0OUZERTgxMUEzQTNBMjg3MjBGNjkwNjQiLz4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiBzdEV2dDp3aGVuPSIyMDE4LTEyLTExVDE0OjM1OjE3KzAxOjAwIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjJFQzVFRjk4NDlGREU4MTFBM0EzQTI4NzIwRjY5MDY0IiBzdEV2dDphY3Rpb249ImNyZWF0ZWQiLz4gPHJkZjpsaSBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0OndoZW49IjIwMTktMDEtMTBUMjA6NTE6MDErMDU6MzAiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6N0QzQ0MyOERFQTE0RTkxMTlFRTJGRkMzQkZENTkzMUUiIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiLz4gPHJkZjpsaSBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0OndoZW49IjIwMTktMDEtMTBUMjA6NTE6MDErMDU6MzAiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6N0UzQ0MyOERFQTE0RTkxMTlFRTJGRkMzQkZENTkzMUUiIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiLz4gPHJkZjpsaSBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0OndoZW49IjIwMTktMDEtMTBUMjE6MDA6MDYrMDU6MzAiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6RTQ4ODFFOUJFQzE0RTkxMThCN0ZFNUQ1OEVERjNENEUiIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNvbnZlcnRlZCIgc3RFdnQ6cGFyYW1ldGVycz0iZnJvbSBpbWFnZS90aWZmIHRvIGltYWdlL2pwZWciLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImRlcml2ZWQiIHN0RXZ0OnBhcmFtZXRlcnM9ImNvbnZlcnRlZCBmcm9tIGltYWdlL3RpZmYgdG8gaW1hZ2UvanBlZyIvPiA8cmRmOmxpIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDUzUgV2luZG93cyIgc3RFdnQ6Y2hhbmdlZD0iLyIgc3RFdnQ6d2hlbj0iMjAxOS0wMS0xMFQyMTowMDowNiswNTozMCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpFNTg4MUU5QkVDMTRFOTExOEI3RkU1RDU4RURGM0Q0RSIgc3RFdnQ6YWN0aW9uPSJzYXZlZCIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/PgD/7QBkUGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAACwcAVoAAxslRxwCAAACAAIcAj4ACDIwMTgxMjExHAI/AAsxNDM1MTcrMDEwMDhCSU0EJQAAAAAAEG8VJXtBp2YwhKsNALMYCvf/4gIsSUNDX1BST0ZJTEUAAQEAAAIcQURCRQIQAABtbnRyUkdCIFhZWiAHzwACABYADAADAA9hY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAACRkZXNjAAABIAAAAGd3dHB0AAABiAAAABRia3B0AAABnAAAABRyVFJDAAABsAAAAA5nVFJDAAABwAAAAA5iVFJDAAAB0AAAAA5yWFlaAAAB4AAAABRnWFlaAAAB9AAAABRiWFlaAAACCAAAABR0ZXh0AAAAAChjKSAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jLgBkZXNjAAAAAAAAAAxFQ0ktUkdCLmljYwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAAD23AABAAAAANM6WFlaIAAAAAAAAAAAAAAAAAAAAABjdXJ2AAAAAAAAAAEBzQAAY3VydgAAAAAAAAABAc0AAGN1cnYAAAAAAAAAAQHNAABYWVogAAAAAAAApngAAFH+AAAA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 :alt: 1-s2.0-S245190221830329X-gr2.jpg 1-s2.0-S245190221830329X-gr2.jpg @@ -531,12 +478,12 @@ individual participants. S_hat=np.mean(S2,axis=0) n=S2.shape[0] CI[i,:]=z*np.power(S_hat/n,.5) - return CI + return CI feature_names=['left_CA1','left_CA3','right_CA1','right_CA3'] sex_covariates=[ 'Female','Male'] - # Creating plots for Female and male + # Creating plots for Female and male for i,sex in enumerate(sex_covariates): #forward model data forward_yhat = pd.read_csv('yhat_forward.txt', sep = ' ', header=None) @@ -544,71 +491,71 @@ individual participants. yhat_forward=yhat_forward[7*i:7*(i+1)] x_forward=[20, 30, 40, 50, 60, 70, 80] - # Find the index of the data exclusively for one sex. Female:0, Male: 1 + # Find the index of the data exclusively for one sex. Female:0, Male: 1 inx=np.where(covariate_normsample.sex==i)[0] x=covariate_normsample.values[inx,1] # actual data y = pd.read_csv('features_normsample.txt', sep = ' ', header=None) y=y.values[inx] - # confidence Interval yhat+ z *(std/n^.5)-->.95 % CI:z=1.96, 99% CI:z=2.58 + # confidence Interval yhat+ z *(std/n^.5)-->.95 % CI:z=1.96, 99% CI:z=2.58 s2= pd.read_csv('ys2_2fold.txt', sep = ' ', header=None) s2=s2.values[inx] CI_95=confidence_interval(s2,x,1.96) CI_99=confidence_interval(s2,x,2.58) - # Creat a trejactroy for each point + # Creat a trejactroy for each point for j,name in enumerate(feature_names): fig=plt.figure() ax=fig.add_subplot(111) ax.plot(x_forward,yhat_forward[:,j], linewidth=4, label='Normative trejactory') - ax.plot(x_forward,CI_95[:,j]+yhat_forward[:,j], linewidth=2,linestyle='--',c='g', label='95% confidence interval') - ax.plot(x_forward,-CI_95[:,j]+yhat_forward[:,j], linewidth=2,linestyle='--',c='g') + ax.plot(x_forward,CI_95[:,j]+yhat_forward[:,j], linewidth=2,linestyle='--',c='g', label='95% confidence interval') + ax.plot(x_forward,-CI_95[:,j]+yhat_forward[:,j], linewidth=2,linestyle='--',c='g') - ax.plot(x_forward,CI_99[:,j]+yhat_forward[:,j], linewidth=1,linestyle='--',c='k', label='99% confidence interval') - ax.plot(x_forward,-CI_99[:,j]+yhat_forward[:,j], linewidth=1,linestyle='--',c='k') + ax.plot(x_forward,CI_99[:,j]+yhat_forward[:,j], linewidth=1,linestyle='--',c='k', label='99% confidence interval') + ax.plot(x_forward,-CI_99[:,j]+yhat_forward[:,j], linewidth=1,linestyle='--',c='k') ax.scatter(x,y[:,j],c='r', label=name) plt.legend(loc='upper left') plt.title('Normative trejectory of' +name+' in '+sex+' cohort') plt.show() plt.close() - + # code by M. Zabihi -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_0.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_0.png -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_1.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_1.png -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_2.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_2.png -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_3.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_3.png -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_4.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_4.png -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_5.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_5.png -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_6.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_6.png -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_7.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_7.png **TASK 6:** Apply the normative model to Nordan’s data and the dementia patients. @@ -626,25 +573,25 @@ individual participants. # create a covariate file for Nordan's as well as the patient's demograhpics covariate_nordan = demographics_nordan[['sex', - 'age']] + 'age']] covariate_nordan.to_csv('covariate_nordan.txt', sep = ' ', - header = False, + header = False, index = False) # create the corresponding feature file - features_nordan = features_nordan[['left_CA1', + features_nordan = features_nordan[['left_CA1', 'left_CA3', 'right_CA1', 'right_CA3']] - features_nordan.to_csv('features_nordan.txt', - sep = ' ', - header = False, + features_nordan.to_csv('features_nordan.txt', + sep = ' ', + header = False, index = False) # apply normative modeling - pcn.normative.estimate(covfile = 'covariate_normsample.txt', + pcn.normative.estimate(covfile = 'covariate_normsample.txt', respfile = 'features_normsample.txt', testcov = 'covariate_nordan.txt', testresp = 'features_nordan.txt', @@ -657,8 +604,22 @@ individual participants. .. parsed-literal:: + inscaler: None + outscaler: None Processing data in features_normsample.txt Estimating model 1 of 4 + + +.. parsed-literal:: + + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:147: RuntimeWarning: overflow encountered in exp + self.sf2 = np.exp(2*theta[self.D]) + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:160: RuntimeWarning: invalid value encountered in multiply + dK = K * squared_dist(x[:, i]/self.ell[i], x[:, i]/self.ell[i]) + + +.. parsed-literal:: + Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -667,8 +628,8 @@ individual participants. Optimization terminated successfully. Current function value: 3781.497401 Iterations: 20 - Function evaluations: 61 - Gradient evaluations: 54 + Function evaluations: 58 + Gradient evaluations: 52 Estimating model 2 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -682,10 +643,22 @@ individual participants. Warning: Estimation of posterior distribution failed Optimization terminated successfully. Current function value: 3201.761309 - Iterations: 39 - Function evaluations: 108 - Gradient evaluations: 108 + Iterations: 48 + Function evaluations: 114 + Gradient evaluations: 114 Estimating model 3 of 4 + + +.. parsed-literal:: + + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:147: RuntimeWarning: overflow encountered in exp + self.sf2 = np.exp(2*theta[self.D]) + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:160: RuntimeWarning: invalid value encountered in multiply + dK = K * squared_dist(x[:, i]/self.ell[i], x[:, i]/self.ell[i]) + + +.. parsed-literal:: + Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -696,9 +669,9 @@ individual participants. Warning: Estimation of posterior distribution failed Optimization terminated successfully. Current function value: 3771.310488 - Iterations: 47 - Function evaluations: 181 - Gradient evaluations: 167 + Iterations: 48 + Function evaluations: 156 + Gradient evaluations: 143 Estimating model 4 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -712,9 +685,9 @@ individual participants. Warning: Estimation of posterior distribution failed Optimization terminated successfully. Current function value: 3200.837262 - Iterations: 40 - Function evaluations: 104 - Gradient evaluations: 104 + Iterations: 42 + Function evaluations: 116 + Gradient evaluations: 116 Evaluating the model ... Writing outputs ... @@ -761,21 +734,21 @@ age 20. Do that for both sexes seperately. lengths = len(forward_yhat[hyppocampal_subfield]) for entry in forward_yhat[hyppocampal_subfield]: if count > 0 and count < 7: - loop_percentage_change_female = calculate_percentage_change(entry, + loop_percentage_change_female = calculate_percentage_change(entry, forward_yhat.iloc[0, hyppocampal_subfield]) percentage_change_female.append(loop_percentage_change_female) - elif count > 7: + elif count > 7: loop_percentage_change_male = calculate_percentage_change(entry, forward_yhat.iloc[9, hyppocampal_subfield]) percentage_change_male.append(loop_percentage_change_male) - count = count + 1 + count = count + 1 - names = ['30 compared to 20 years', - '40 compared to 20 years', - '50 compared to 20 years', - '60 compared to 20 years', + names = ['30 compared to 20 years', + '40 compared to 20 years', + '50 compared to 20 years', + '60 compared to 20 years', '70 compared to 20 years', '80 compared to 20 years'] @@ -803,5 +776,6 @@ age 20. Do that for both sexes seperately. -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_32_1.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_33_1.png + diff --git a/doc/build/html/_sources/pages/other_predictive_models.rst.txt b/doc/build/html/_sources/pages/other_predictive_models.rst.txt index c67b860c..2b841cad 100644 --- a/doc/build/html/_sources/pages/other_predictive_models.rst.txt +++ b/doc/build/html/_sources/pages/other_predictive_models.rst.txt @@ -1,24 +1,23 @@ -.. title:: Predictive modeling tutorial - -Predictive modeling using deviation scores -============================================= - -The Normative Modeling Framework for Computational Psychiatry. Nature Protocols. https://www.nature.com/articles/s41596-022-00696-5. - -Created by `Saige Rutherford `__ - +.. code:: ipython3 -.. image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/predictive-clinical-neuroscience/PCNtoolkit-demo/blob/main/tutorials/BLR_protocol/other_predictive_models.ipynb + ! git clone https://github.com/predictive-clinical-neuroscience/PCNtoolkit-demo.git -.. code:: ipython3 +.. parsed-literal:: - ! git clone https://github.com/predictive-clinical-neuroscience/PCNtoolkit-demo.git + Cloning into 'PCNtoolkit-demo'... + remote: Enumerating objects: 1237, done. + remote: Counting objects: 100% (360/360), done. + remote: Compressing objects: 100% (185/185), done. + remote: Total 1237 (delta 200), reused 306 (delta 172), pack-reused 877 (from 1) + Receiving objects: 100% (1237/1237), 141.45 MiB | 10.83 MiB/s, done. + Resolving deltas: 100% (562/562), done. + Updating files: 100% (70/70), done. .. code:: ipython3 + import os .. code:: ipython3 @@ -42,7 +41,7 @@ Created by `Saige Rutherford `__ import numpy as np from matplotlib import pyplot as plt from scipy import stats, linalg - from sklearn import preprocessing, decomposition, linear_model, metrics + from sklearn import preprocessing, decomposition, linear_model, metrics import warnings .. code:: ipython3 @@ -62,7 +61,7 @@ Created by `Saige Rutherford `__ plt.rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title Load Data ------------------------------ +========= .. code:: ipython3 @@ -85,7 +84,7 @@ Load Data Create Train/Test Splits --------------------------------------- +======================== .. code:: ipython3 @@ -117,7 +116,7 @@ Create Train/Test Splits test_mu_centered_ct = (test_data_ct - train_data_ct.mean(axis=0)) Principal Component Regression (BBS) --------------------------------------- +==================================== .. code:: ipython3 @@ -131,7 +130,7 @@ Principal Component Regression (BBS) .. code:: ipython3 - print(f'First PC explains {pca_model_z.explained_variance_ratio_[0]*100:.2f}% of the total variance.') + print(f'First PC explains {pca_model_z.explained_variance_ratio_[0]*100:.2f}% of the total variance.\nThis is an artifact of zero inflated data') plt.figure(figsize=(10, 7)) plt.bar(range(1, 51), pca_model_z.explained_variance_ratio_[1:51]) plt.title('Deviations model Variance Explained Ratio\nPCs 1-50', fontsize=25) @@ -141,15 +140,16 @@ Principal Component Regression (BBS) .. parsed-literal:: First PC explains 23.41% of the total variance. + This is an artifact of zero inflated data -.. image:: other_predictive_models_files/other_predictive_models_16_1.png +.. image:: other_predictive_models_files/other_predictive_models_17_1.png .. code:: ipython3 - print(f'First PC explains {pca_model_ct.explained_variance_ratio_[0]*100:.2f}% of the total variance.') + print(f'First PC explains {pca_model_ct.explained_variance_ratio_[0]*100:.2f}% of the total variance.\nThis is an artifact of zero inflated data') plt.figure(figsize=(10, 7)) plt.bar(range(1, 51), pca_model_ct.explained_variance_ratio_[1:51]) plt.title('Cortical Thickness model Variance Explained Ratio\nPCs 1-50', fontsize=25) @@ -159,10 +159,11 @@ Principal Component Regression (BBS) .. parsed-literal:: First PC explains 24.28% of the total variance. + This is an artifact of zero inflated data -.. image:: other_predictive_models_files/other_predictive_models_17_1.png +.. image:: other_predictive_models_files/other_predictive_models_18_1.png .. code:: ipython3 @@ -176,7 +177,7 @@ Principal Component Regression (BBS) test_transformed_ct = pca_model_ct.transform(test_data_ct) Fit Linear Regression Model --------------------------------------- +--------------------------- .. code:: ipython3 @@ -184,14 +185,14 @@ Fit Linear Regression Model # we will check that this matches sklearn results later # fit ols model on dimension reduced train data - train_features_z = np.hstack([np.ones((train_transformed_z.shape[0], 1)), + train_features_z = np.hstack([np.ones((train_transformed_z.shape[0], 1)), train_transformed_z]) - train_features_inv_z = linalg.pinv2(train_features_z) + train_features_inv_z = linalg.pinv(train_features_z) train_betas_z = np.dot(train_features_inv_z, train_phen) train_pred_phen_z = np.dot(train_features_z, train_betas_z) # fit ols model on dimension reduced test data - test_features_z = np.hstack([np.ones((test_transformed_z.shape[0], 1)), + test_features_z = np.hstack([np.ones((test_transformed_z.shape[0], 1)), test_transformed_z]) test_pred_phen_z = np.dot(test_features_z, train_betas_z) @@ -201,14 +202,14 @@ Fit Linear Regression Model # we will check that this matches sklearn results later # fit ols model on dimension reduced train data - train_features_ct = np.hstack([np.ones((train_transformed_ct.shape[0], 1)), + train_features_ct = np.hstack([np.ones((train_transformed_ct.shape[0], 1)), train_transformed_ct]) - train_features_inv_ct = linalg.pinv2(train_features_ct) + train_features_inv_ct = linalg.pinv(train_features_ct) train_betas_ct = np.dot(train_features_inv_ct, train_phen) train_pred_phen_ct = np.dot(train_features_ct, train_betas_ct) # fit ols model on dimension reduced test data - test_features_ct = np.hstack([np.ones((test_transformed_ct.shape[0], 1)), + test_features_ct = np.hstack([np.ones((test_transformed_ct.shape[0], 1)), test_transformed_ct]) test_pred_phen_ct = np.dot(test_features_ct, train_betas_ct) @@ -216,34 +217,20 @@ Fit Linear Regression Model # OLS using sklearn - lr_model_z = linear_model.LinearRegression(fit_intercept=True, normalize=False) + lr_model_z = linear_model.LinearRegression(fit_intercept=True) lr_model_z.fit(train_transformed_z, train_phen) train_pred_phen_lr_model_z = lr_model_z.predict(train_transformed_z) test_pred_phen_lr_model_z = lr_model_z.predict(test_transformed_z) - -.. parsed-literal:: - - /usr/local/lib/python3.7/dist-packages/sklearn/linear_model/_base.py:155: FutureWarning: 'normalize' was deprecated in version 1.0 and will be removed in 1.2. Please leave the normalize parameter to its default value to silence this warning. The default behavior of this estimator is to not do any normalization. If normalization is needed please use sklearn.preprocessing.StandardScaler instead. - FutureWarning, - - .. code:: ipython3 # OLS using sklearn - lr_model_ct = linear_model.LinearRegression(fit_intercept=True, normalize=False) + lr_model_ct = linear_model.LinearRegression(fit_intercept=True) lr_model_ct.fit(train_transformed_ct, train_phen) train_pred_phen_lr_model_ct = lr_model_ct.predict(train_transformed_ct) test_pred_phen_lr_model_ct = lr_model_ct.predict(test_transformed_ct) - -.. parsed-literal:: - - /usr/local/lib/python3.7/dist-packages/sklearn/linear_model/_base.py:155: FutureWarning: 'normalize' was deprecated in version 1.0 and will be removed in 1.2. Please leave the normalize parameter to its default value to silence this warning. The default behavior of this estimator is to not do any normalization. If normalization is needed please use sklearn.preprocessing.StandardScaler instead. - FutureWarning, - - .. code:: ipython3 # ensure matrix math predictions and sklearn predictions are accurate to 5 decimals @@ -270,8 +257,8 @@ Fit Linear Regression Model Passed -Mean Squared/Absolute Error of Predictions ------------------------------------------------ +Accuracy of Predictions +----------------------- .. code:: ipython3 @@ -326,18 +313,18 @@ BBS Cross Validation def bbs(X, y, n_components, n_cv_splits, pred_summary_function, verbose=False): assert X.shape[0] == y.shape[0] - + fold_accs_train = [] fold_accs_test = [] np.random.seed(42) shuffled_idxs = np.random.choice(range(X.shape[0]), size=X.shape[0], replace=False) for fold_i, test_idxs in enumerate(np.array_split(shuffled_idxs, n_cv_splits)): - train_mask = np.ones(X.shape[0], np.bool) + train_mask = np.ones(X.shape[0], bool) train_mask[test_idxs] = 0 # create train/text X, y train_X, test_X = X[train_mask, :], X[test_idxs, :] - train_y, test_y = y[train_mask], y[test_idxs] + train_y, test_y = y[train_mask], y[test_idxs] # mean center columns using train data only train_X_mu = train_X.mean(axis=0) @@ -356,7 +343,7 @@ BBS Cross Validation # fit OLS model if verbose: print(f'CV Fold: {fold_i+1:<10} Fitting Linear Regression model...') - lr_model = linear_model.LinearRegression(fit_intercept=True, normalize=False) + lr_model = linear_model.LinearRegression(fit_intercept=True) lr_model.fit(train_X, train_y) train_pred = lr_model.predict(train_X) @@ -364,11 +351,11 @@ BBS Cross Validation fold_accs_train.append(pred_summary_function(train_y, train_pred)) fold_accs_test.append(pred_summary_function(test_y, test_pred)) - + if verbose: - print(f'CV Fold: {fold_i+1:<10} Train MAE: {round(fold_accs_train[-1], 3):<10} Test MAE: {round(fold_accs_test[-1], 3):<10}') + print(f'CV Fold: {fold_i+1:<10} Train Accuracy: {round(fold_accs_train[-1], 3):<10} Test Accuracy: {round(fold_accs_test[-1], 3):<10}') + - plt.figure(figsize=(13, 7)) plt.plot(range(1, len(fold_accs_train)+1), fold_accs_train, linestyle='-', marker='o', color='C0', label='Train CV Performance') plt.plot(range(1, len(fold_accs_test)+1), fold_accs_test, linestyle='-', marker='o', color='C1', label='Test CV Performance') @@ -377,7 +364,7 @@ BBS Cross Validation plt.xlabel('CV Fold') plt.legend(fontsize=20) plt.show() - + return fold_accs_train, fold_accs_test .. code:: ipython3 @@ -389,23 +376,23 @@ BBS Cross Validation CV Fold: 1 Fitting PCA model... CV Fold: 1 Fitting Linear Regression model... - CV Fold: 1 Train MAE: 0.599 Test MAE: 0.619 + CV Fold: 1 Train Accuracy: 0.599 Test Accuracy: 0.619 CV Fold: 2 Fitting PCA model... CV Fold: 2 Fitting Linear Regression model... - CV Fold: 2 Train MAE: 0.572 Test MAE: 0.713 + CV Fold: 2 Train Accuracy: 0.572 Test Accuracy: 0.713 CV Fold: 3 Fitting PCA model... CV Fold: 3 Fitting Linear Regression model... - CV Fold: 3 Train MAE: 0.577 Test MAE: 0.687 + CV Fold: 3 Train Accuracy: 0.577 Test Accuracy: 0.687 CV Fold: 4 Fitting PCA model... CV Fold: 4 Fitting Linear Regression model... - CV Fold: 4 Train MAE: 0.604 Test MAE: 0.608 + CV Fold: 4 Train Accuracy: 0.604 Test Accuracy: 0.608 CV Fold: 5 Fitting PCA model... CV Fold: 5 Fitting Linear Regression model... - CV Fold: 5 Train MAE: 0.581 Test MAE: 0.687 + CV Fold: 5 Train Accuracy: 0.581 Test Accuracy: 0.687 -.. image:: other_predictive_models_files/other_predictive_models_32_3.png +.. image:: other_predictive_models_files/other_predictive_models_33_1.png .. code:: ipython3 @@ -417,41 +404,43 @@ BBS Cross Validation CV Fold: 1 Fitting PCA model... CV Fold: 1 Fitting Linear Regression model... - CV Fold: 1 Train MAE: 0.622 Test MAE: 0.643 + CV Fold: 1 Train Accuracy: 0.622 Test Accuracy: 0.643 CV Fold: 2 Fitting PCA model... CV Fold: 2 Fitting Linear Regression model... - CV Fold: 2 Train MAE: 0.605 Test MAE: 0.723 + CV Fold: 2 Train Accuracy: 0.605 Test Accuracy: 0.723 CV Fold: 3 Fitting PCA model... CV Fold: 3 Fitting Linear Regression model... - CV Fold: 3 Train MAE: 0.604 Test MAE: 0.701 + CV Fold: 3 Train Accuracy: 0.604 Test Accuracy: 0.701 CV Fold: 4 Fitting PCA model... CV Fold: 4 Fitting Linear Regression model... - CV Fold: 4 Train MAE: 0.624 Test MAE: 0.646 + CV Fold: 4 Train Accuracy: 0.624 Test Accuracy: 0.646 CV Fold: 5 Fitting PCA model... CV Fold: 5 Fitting Linear Regression model... - CV Fold: 5 Train MAE: 0.614 Test MAE: 0.722 + CV Fold: 5 Train Accuracy: 0.614 Test Accuracy: 0.722 -.. image:: other_predictive_models_files/other_predictive_models_33_3.png +.. image:: other_predictive_models_files/other_predictive_models_34_1.png Connectome Predictive Modelling --------------------------------------- +=============================== .. code:: ipython3 # correlation train_brain with train_phenotype train_z_pheno_corr_p = [stats.pearsonr(train_data_z[:, i], train_phen) for i in range(train_data_z.shape[1])] # train_pheno_corr_p: (259200, ) + # there are some nan correlations if brain data is poorly cropped (ie: some columns are always 0) .. code:: ipython3 # correlation train_brain with train_phenotype train_ct_pheno_corr_p = [stats.pearsonr(train_data_ct[:, i], train_phen) for i in range(train_data_ct.shape[1])] # train_pheno_corr_p: (259200, ) + # there are some nan correlations if brain data is poorly cropped (ie: some columns are always 0) .. code:: ipython3 - # split into positive and negative correlations + # split into positive and negative correlations # and keep edges with p values below threshold pval_threshold = 0.01 @@ -495,15 +484,13 @@ Connectome Predictive Modelling .. code:: ipython3 - fit_pos_z = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(train_pos_edges_sum_z.reshape(-1, 1), train_phen) - fit_neg_z = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(train_neg_edges_sum_z.reshape(-1, 1), train_phen) - + fit_pos_z = linear_model.LinearRegression(fit_intercept=True).fit(train_pos_edges_sum_z.reshape(-1, 1), train_phen) + fit_neg_z = linear_model.LinearRegression(fit_intercept=True).fit(train_neg_edges_sum_z.reshape(-1, 1), train_phen) .. code:: ipython3 - fit_pos_ct = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(train_pos_edges_sum_ct.reshape(-1, 1), train_phen) - fit_neg_ct = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(train_neg_edges_sum_ct.reshape(-1, 1), train_phen) - + fit_pos_ct = linear_model.LinearRegression(fit_intercept=True).fit(train_pos_edges_sum_ct.reshape(-1, 1), train_phen) + fit_neg_ct = linear_model.LinearRegression(fit_intercept=True).fit(train_neg_edges_sum_ct.reshape(-1, 1), train_phen) .. code:: ipython3 @@ -512,52 +499,48 @@ Connectome Predictive Modelling pos_error_ct = metrics.mean_absolute_error(train_phen, fit_pos_ct.predict(train_pos_edges_sum_ct.reshape(-1, 1))) neg_error_ct = metrics.mean_absolute_error(train_phen, fit_neg_ct.predict(train_neg_edges_sum_ct.reshape(-1, 1))) - print(f'Training Error (MAE) (Positive Z Features Model) = {pos_error_z:.3f}') - print(f'Training Error (MAE) (Negative Z Features Model) = {neg_error_z:.3f}') - print(f'Training Error (MAE) (Positive CT Features Model) = {pos_error_ct:.3f}') - print(f'Training Error (MAE) (Negative CT Features Model) = {neg_error_ct:.3f}') + print(f'Training Error (Positive Z Features Model) = {pos_error_z:.3f}') + print(f'Training Error (Negative Z Features Model) = {neg_error_z:.3f}') + print(f'Training Error (Positive CT Features Model) = {pos_error_ct:.3f}') + print(f'Training Error (Negative CT Features Model) = {neg_error_ct:.3f}') .. parsed-literal:: - Training Error (MAE) (Positive Z Features Model) = 0.631 - Training Error (MAE) (Negative Z Features Model) = 0.666 - Training Error (MAE) (Positive CT Features Model) = 0.662 - Training Error (MAE) (Negative CT Features Model) = 0.665 + Training Error (Positive Z Features Model) = 0.631 + Training Error (Negative Z Features Model) = 0.666 + Training Error (Positive CT Features Model) = 0.662 + Training Error (Negative CT Features Model) = 0.665 .. code:: ipython3 # combine positive/negative edges in one linear regression model - fit_pos_neg_z = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(np.stack((train_pos_edges_sum_z, train_neg_edges_sum_z)).T, train_phen) - - + fit_pos_neg_z = linear_model.LinearRegression(fit_intercept=True).fit(np.stack((train_pos_edges_sum_z, train_neg_edges_sum_z)).T, train_phen) .. code:: ipython3 # combine positive/negative edges in one linear regression model - fit_pos_neg_ct = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(np.stack((train_pos_edges_sum_ct, train_neg_edges_sum_ct)).T, train_phen) - - + fit_pos_neg_ct = linear_model.LinearRegression(fit_intercept=True).fit(np.stack((train_pos_edges_sum_ct, train_neg_edges_sum_ct)).T, train_phen) .. code:: ipython3 pos_neg_error_z = metrics.mean_absolute_error(train_phen, fit_pos_neg_z.predict(np.stack((train_pos_edges_sum_z, train_neg_edges_sum_z)).T)) pos_neg_error_ct = metrics.mean_absolute_error(train_phen, fit_pos_neg_ct.predict(np.stack((train_pos_edges_sum_ct, train_neg_edges_sum_ct)).T)) - print(f'Training Error (MAE) (Positive/Negative Z Features Model) = {pos_neg_error_z:.3f}') - print(f'Training Error (MAE) (Positive/Negative CT Features Model) = {pos_neg_error_ct:.3f}') + print(f'Training Error (Positive/Negative Z Features Model) = {pos_neg_error_z:.3f}') + print(f'Training Error (Positive/Negative CT Features Model) = {pos_neg_error_ct:.3f}') .. parsed-literal:: - Training Error (MAE) (Positive/Negative Z Features Model) = 0.620 - Training Error (MAE) (Positive/Negative CT Features Model) = 0.642 + Training Error (Positive/Negative Z Features Model) = 0.620 + Training Error (Positive/Negative CT Features Model) = 0.642 .. code:: ipython3 - # evaluate out of sample performance + # evaluate out of sample performance test_pos_edges_sum_z = test_data_z[:, keep_edges_pos_z].sum(1) test_neg_edges_sum_z = test_data_z[:, keep_edges_neg_z].sum(1) @@ -572,44 +555,44 @@ Connectome Predictive Modelling neg_test_error_ct = metrics.mean_absolute_error(test_phen, fit_neg_ct.predict(test_neg_edges_sum_ct.reshape(-1, 1))) pos_neg_test_error_ct = metrics.mean_absolute_error(test_phen, fit_pos_neg_ct.predict(np.stack((test_pos_edges_sum_ct, test_neg_edges_sum_ct)).T)) - print(f'Testing Error (MAE) (Positive Z Features Model) = {pos_test_error_z:.3f}') - print(f'Testing Error (MAE) (Negative Z Features Model) = {neg_test_error_z:.3f}') - print(f'Testing Error (MAE) (Positive/Negative Z Features Model) = {pos_neg_test_error_z:.3f}') - print(f'Testing Error (MAE) (Positive CT Features Model) = {pos_test_error_ct:.3f}') - print(f'Testing Error (MAE) (Negative CT Features Model) = {neg_test_error_ct:.3f}') - print(f'Testing Error (MAE) (Positive/Negative CT Features Model) = {pos_neg_test_error_ct:.3f}') + print(f'Testing Error (Positive Z Features Model) = {pos_test_error_z:.3f}') + print(f'Testing Error (Negative Z Features Model) = {neg_test_error_z:.3f}') + print(f'Testing Error (Positive/Negative Z Features Model) = {pos_neg_test_error_z:.3f}') + print(f'Testing Error (Positive CT Features Model) = {pos_test_error_ct:.3f}') + print(f'Testing Error (Negative CT Features Model) = {neg_test_error_ct:.3f}') + print(f'Testing Error (Positive/Negative CT Features Model) = {pos_neg_test_error_ct:.3f}') .. parsed-literal:: - Testing Error (MAE) (Positive Z Features Model) = 0.705 - Testing Error (MAE) (Negative Z Features Model) = 0.696 - Testing Error (MAE) (Positive/Negative Z Features Model) = 0.697 - Testing Error (MAE) (Positive CT Features Model) = 0.710 - Testing Error (MAE) (Negative CT Features Model) = 0.695 - Testing Error (MAE) (Positive/Negative CT Features Model) = 0.701 + Testing Error (Positive Z Features Model) = 0.705 + Testing Error (Negative Z Features Model) = 0.696 + Testing Error (Positive/Negative Z Features Model) = 0.697 + Testing Error (Positive CT Features Model) = 0.710 + Testing Error (Negative CT Features Model) = 0.695 + Testing Error (Positive/Negative CT Features Model) = 0.701 CPM Cross Validation --------------------------------------- +-------------------- .. code:: ipython3 def cpm(X, y, p_threshold, n_cv_splits, pred_summary_function, verbose=False): assert X.shape[0] == y.shape[0] - + fold_accs_train = [] fold_accs_test = [] np.random.seed(42) shuffled_idxs = np.random.choice(range(X.shape[0]), size=X.shape[0], replace=False) for fold_i, test_idxs in enumerate(np.array_split(shuffled_idxs, n_cv_splits)): - train_mask = np.ones(X.shape[0], np.bool) + train_mask = np.ones(X.shape[0], bool) train_mask[test_idxs] = 0 # create train/text X, y train_X, test_X = X[train_mask, :], X[test_idxs, :] - train_y, test_y = y[train_mask], y[test_idxs] - + train_y, test_y = y[train_mask], y[test_idxs] + # create correlation matrix between train_X and train_y if verbose: print(f'CV Fold: {fold_i+1:<10} Computing correlations between train_X and train_y...') @@ -622,18 +605,18 @@ CPM Cross Validation # create masks for edges below p-threshold and split pos/neg correlations keep_edges_pos = (train_corrs > 0) & (train_pvals < p_threshold) keep_edges_neg = (train_corrs < 0) & (train_pvals < p_threshold) - + # sum X entries with significant correlations with y train_pos_edges_sum = train_X[:, keep_edges_pos].sum(1) train_neg_edges_sum = train_X[:, keep_edges_neg].sum(1) test_pos_edges_sum = test_X[:, keep_edges_pos].sum(1) test_neg_edges_sum = test_X[:, keep_edges_neg].sum(1) - + # fit linear regression models based on summed values - fit_pos = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(train_pos_edges_sum.reshape(-1, 1), train_y) - fit_neg = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(train_neg_edges_sum.reshape(-1, 1), train_y) - fit_pos_neg = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(np.stack((train_pos_edges_sum, train_neg_edges_sum)).T, train_y) - + fit_pos = linear_model.LinearRegression(fit_intercept=True).fit(train_pos_edges_sum.reshape(-1, 1), train_y) + fit_neg = linear_model.LinearRegression(fit_intercept=True).fit(train_neg_edges_sum.reshape(-1, 1), train_y) + fit_pos_neg = linear_model.LinearRegression(fit_intercept=True).fit(np.stack((train_pos_edges_sum, train_neg_edges_sum)).T, train_y) + # compute train errors train_pos_error = pred_summary_function(train_y, fit_pos.predict(train_pos_edges_sum.reshape(-1, 1))) train_neg_error = pred_summary_function(train_y, fit_neg.predict(train_neg_edges_sum.reshape(-1, 1))) @@ -646,27 +629,27 @@ CPM Cross Validation fold_accs_train.append((train_pos_error, train_neg_error, train_posneg_error)) fold_accs_test.append((test_pos_error, test_neg_error, test_posneg_error)) - + if verbose: - print(f'CV Fold: {fold_i+1:<10} Train Pos-Edges Model MAE: {round(train_pos_error, 3):<10} Train Neg-Edges Model Accuracy: {round(train_neg_error, 3):<10} Train Pos/Neg-Edges Model Accuracy: {round(train_posneg_error, 3):<10}') - print(f'CV Fold: {fold_i+1:<10} Test Pos-Edges Model MAE: {round(test_pos_error, 3):<10} Test Neg-Edges Model Accuracy: {round(test_neg_error, 3):<10} Test Pos/Neg-Edges Model Accuracy: {round(test_posneg_error, 3):<10}') + print(f'CV Fold: {fold_i+1:<10} Train Pos-Edges Model Accuracy: {round(train_pos_error, 3):<10} Train Neg-Edges Model Accuracy: {round(train_neg_error, 3):<10} Train Pos/Neg-Edges Model Accuracy: {round(train_posneg_error, 3):<10}') + print(f'CV Fold: {fold_i+1:<10} Test Pos-Edges Model Accuracy: {round(test_pos_error, 3):<10} Test Neg-Edges Model Accuracy: {round(test_neg_error, 3):<10} Test Pos/Neg-Edges Model Accuracy: {round(test_posneg_error, 3):<10}') + - plt.figure(figsize=(13, 7)) plt.plot(range(1, len(fold_accs_train)+1), [x[0] for x in fold_accs_train], linestyle='--', marker='o', color='C0', label='Train Pos-Edges Model') plt.plot(range(1, len(fold_accs_train)+1), [x[1] for x in fold_accs_train], linestyle='--', marker='o', color='C1', label='Train Neg-Edges Model') plt.plot(range(1, len(fold_accs_train)+1), [x[2] for x in fold_accs_train], linestyle='--', marker='o', color='C2', label='Train Pos/Neg-Edges Model') - + plt.plot(range(1, len(fold_accs_test)+1), [x[0] for x in fold_accs_test], linestyle='-', marker='o', color='C0', label='Test Pos-Edges Model') plt.plot(range(1, len(fold_accs_test)+1), [x[1] for x in fold_accs_test], linestyle='-', marker='o', color='C1', label='Test Neg-Edges Model') plt.plot(range(1, len(fold_accs_test)+1), [x[2] for x in fold_accs_test], linestyle='-', marker='o', color='C2', label='Test Pos/Neg-Edges Model') - + plt.title(pred_summary_function.__name__, fontsize=20) plt.xticks(range(1, len(fold_accs_test)+1)) plt.xlabel('CV Fold') plt.legend(fontsize=10) plt.show() - + return fold_accs_train, fold_accs_test .. code:: ipython3 @@ -674,32 +657,27 @@ CPM Cross Validation fold_accs_train_z, fold_accs_test_z = cpm(hcp_z, gscores, p_threshold=0.01, n_cv_splits=5, pred_summary_function=metrics.mean_absolute_error, verbose=True) - .. parsed-literal:: CV Fold: 1 Computing correlations between train_X and train_y... - CV Fold: 1 Train Pos-Edges Model MAE: 0.652 Train Neg-Edges Model MAE: 0.673 Train Pos/Neg-Edges Model MAE: 0.644 - CV Fold: 1 Test Pos-Edges Model MAE: 0.636 Test Neg-Edges Model MAE: 0.671 Test Pos/Neg-Edges Model MAE: 0.632 + CV Fold: 1 Train Pos-Edges Model Accuracy: 0.652 Train Neg-Edges Model Accuracy: 0.673 Train Pos/Neg-Edges Model Accuracy: 0.644 + CV Fold: 1 Test Pos-Edges Model Accuracy: 0.636 Test Neg-Edges Model Accuracy: 0.671 Test Pos/Neg-Edges Model Accuracy: 0.632 CV Fold: 2 Computing correlations between train_X and train_y... - CV Fold: 2 Train Pos-Edges Model MAE: 0.648 Train Neg-Edges Model MAE: 0.678 Train Pos/Neg-Edges Model MAE: 0.636 - CV Fold: 2 Test Pos-Edges Model MAE: 0.651 Test Neg-Edges Model MAE: 0.659 Test Pos/Neg-Edges Model MAE: 0.662 + CV Fold: 2 Train Pos-Edges Model Accuracy: 0.648 Train Neg-Edges Model Accuracy: 0.678 Train Pos/Neg-Edges Model Accuracy: 0.636 + CV Fold: 2 Test Pos-Edges Model Accuracy: 0.651 Test Neg-Edges Model Accuracy: 0.659 Test Pos/Neg-Edges Model Accuracy: 0.662 CV Fold: 3 Computing correlations between train_X and train_y... - CV Fold: 3 Train Pos-Edges Model MAE: 0.644 Train Neg-Edges Model MAE: 0.662 Train Pos/Neg-Edges Model MAE: 0.636 - CV Fold: 3 Test Pos-Edges Model MAE: 0.65 Test Neg-Edges Model MAE: 0.708 Test Pos/Neg-Edges Model MAE: 0.646 + CV Fold: 3 Train Pos-Edges Model Accuracy: 0.644 Train Neg-Edges Model Accuracy: 0.662 Train Pos/Neg-Edges Model Accuracy: 0.636 + CV Fold: 3 Test Pos-Edges Model Accuracy: 0.65 Test Neg-Edges Model Accuracy: 0.708 Test Pos/Neg-Edges Model Accuracy: 0.646 CV Fold: 4 Computing correlations between train_X and train_y... - CV Fold: 4 Train Pos-Edges Model MAE: 0.653 Train Neg-Edges Model MAE: 0.676 Train Pos/Neg-Edges Model MAE: 0.648 - CV Fold: 4 Test Pos-Edges Model MAE: 0.626 Test Neg-Edges Model MAE: 0.659 Test Pos/Neg-Edges Model MAE: 0.625 + CV Fold: 4 Train Pos-Edges Model Accuracy: 0.653 Train Neg-Edges Model Accuracy: 0.676 Train Pos/Neg-Edges Model Accuracy: 0.648 + CV Fold: 4 Test Pos-Edges Model Accuracy: 0.626 Test Neg-Edges Model Accuracy: 0.659 Test Pos/Neg-Edges Model Accuracy: 0.625 CV Fold: 5 Computing correlations between train_X and train_y... + CV Fold: 5 Train Pos-Edges Model Accuracy: 0.631 Train Neg-Edges Model Accuracy: 0.666 Train Pos/Neg-Edges Model Accuracy: 0.62 + CV Fold: 5 Test Pos-Edges Model Accuracy: 0.704 Test Neg-Edges Model Accuracy: 0.696 Test Pos/Neg-Edges Model Accuracy: 0.697 -.. parsed-literal:: - - CV Fold: 5 Train Pos-Edges Model MAE: 0.631 Train Neg-Edges Model MAE: 0.666 Train Pos/Neg-Edges Model MAE: 0.62 - CV Fold: 5 Test Pos-Edges Model MAE: 0.704 Test Neg-Edges Model MAE: 0.696 Test Pos/Neg-Edges Model MAE: 0.697 - - -.. image:: other_predictive_models_files/other_predictive_models_50_4.png +.. image:: other_predictive_models_files/other_predictive_models_51_1.png .. code:: ipython3 @@ -710,51 +688,61 @@ CPM Cross Validation .. parsed-literal:: CV Fold: 1 Computing correlations between train_X and train_y... - CV Fold: 1 Train Pos-Edges Model MAE: 0.675 Train Neg-Edges Model MAE: 0.673 Train Pos/Neg-Edges Model MAE: 0.659 - CV Fold: 1 Test Pos-Edges Model MAE: 0.659 Test Neg-Edges Model MAE: 0.67 Test Pos/Neg-Edges Model MAE: 0.653 + CV Fold: 1 Train Pos-Edges Model Accuracy: 0.675 Train Neg-Edges Model Accuracy: 0.673 Train Pos/Neg-Edges Model Accuracy: 0.659 + CV Fold: 1 Test Pos-Edges Model Accuracy: 0.659 Test Neg-Edges Model Accuracy: 0.67 Test Pos/Neg-Edges Model Accuracy: 0.653 CV Fold: 2 Computing correlations between train_X and train_y... - CV Fold: 2 Train Pos-Edges Model MAE: 0.674 Train Neg-Edges Model MAE: 0.678 Train Pos/Neg-Edges Model MAE: 0.636 - CV Fold: 2 Test Pos-Edges Model MAE: 0.661 Test Neg-Edges Model MAE: 0.657 Test Pos/Neg-Edges Model MAE: 0.668 + CV Fold: 2 Train Pos-Edges Model Accuracy: 0.674 Train Neg-Edges Model Accuracy: 0.678 Train Pos/Neg-Edges Model Accuracy: 0.636 + CV Fold: 2 Test Pos-Edges Model Accuracy: 0.661 Test Neg-Edges Model Accuracy: 0.657 Test Pos/Neg-Edges Model Accuracy: 0.668 CV Fold: 3 Computing correlations between train_X and train_y... - CV Fold: 3 Train Pos-Edges Model MAE: 0.659 Train Neg-Edges Model MAE: 0.665 Train Pos/Neg-Edges Model MAE: 0.644 - CV Fold: 3 Test Pos-Edges Model MAE: 0.699 Test Neg-Edges Model MAE: 0.704 Test Pos/Neg-Edges Model MAE: 0.684 + CV Fold: 3 Train Pos-Edges Model Accuracy: 0.659 Train Neg-Edges Model Accuracy: 0.665 Train Pos/Neg-Edges Model Accuracy: 0.644 + CV Fold: 3 Test Pos-Edges Model Accuracy: 0.699 Test Neg-Edges Model Accuracy: 0.704 Test Pos/Neg-Edges Model Accuracy: 0.684 CV Fold: 4 Computing correlations between train_X and train_y... - CV Fold: 4 Train Pos-Edges Model MAE: 0.674 Train Neg-Edges Model MAE: 0.678 Train Pos/Neg-Edges Model MAE: 0.658 - CV Fold: 4 Test Pos-Edges Model MAE: 0.653 Test Neg-Edges Model MAE: 0.656 Test Pos/Neg-Edges Model MAE: 0.638 + CV Fold: 4 Train Pos-Edges Model Accuracy: 0.674 Train Neg-Edges Model Accuracy: 0.678 Train Pos/Neg-Edges Model Accuracy: 0.658 + CV Fold: 4 Test Pos-Edges Model Accuracy: 0.653 Test Neg-Edges Model Accuracy: 0.656 Test Pos/Neg-Edges Model Accuracy: 0.638 CV Fold: 5 Computing correlations between train_X and train_y... - CV Fold: 5 Train Pos-Edges Model MAE: 0.662 Train Neg-Edges Model MAE: 0.666 Train Pos/Neg-Edges Model MAE: 0.642 - CV Fold: 5 Test Pos-Edges Model MAE: 0.709 Test Neg-Edges Model MAE: 0.698 Test Pos/Neg-Edges Model MAE: 0.708 + CV Fold: 5 Train Pos-Edges Model Accuracy: 0.662 Train Neg-Edges Model Accuracy: 0.666 Train Pos/Neg-Edges Model Accuracy: 0.642 + CV Fold: 5 Test Pos-Edges Model Accuracy: 0.709 Test Neg-Edges Model Accuracy: 0.698 Test Pos/Neg-Edges Model Accuracy: 0.708 -.. image:: other_predictive_models_files/other_predictive_models_51_2.png +.. image:: other_predictive_models_files/other_predictive_models_52_1.png Lasso (Linear Regression + L1 Regularization) ------------------------------------------------------ +============================================= .. code:: ipython3 - # LassoCV uses coordinate descent to select hyperparameter alpha + # LassoCV uses coordinate descent to select hyperparameter alpha alpha_grid = np.array([10**a for a in np.arange(-3, 3, 0.25)]) - lassoCV_model_z = linear_model.LassoCV(cv=5, n_alphas=len(alpha_grid), alphas=alpha_grid, fit_intercept=True, normalize=False, random_state=42, verbose=True, n_jobs=5).fit(train_data_z, train_phen) + lassoCV_model_z = linear_model.LassoCV(cv=5, n_alphas=len(alpha_grid), alphas=alpha_grid, fit_intercept=True, random_state=42, verbose=True, n_jobs=5).fit(train_data_z, train_phen) +.. parsed-literal:: + + [Parallel(n_jobs=5)]: Using backend ThreadingBackend with 5 concurrent workers. + .....................................................................................................................[Parallel(n_jobs=5)]: Done 2 out of 5 | elapsed: 0.5s remaining: 0.7s + /usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.07308221069854426, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.15375414865695802, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + ./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.10611096508367268, tolerance: 0.04382929483334259 + model = cd_fast.enet_coordinate_descent_gram( + .[Parallel(n_jobs=5)]: Done 5 out of 5 | elapsed: 0.5s finished + .. code:: ipython3 - # LassoCV uses coordinate descent to select hyperparameter alpha + # LassoCV uses coordinate descent to select hyperparameter alpha alpha_grid = np.array([10**a for a in np.arange(-3, 3, 0.25)]) - lassoCV_model_ct = linear_model.LassoCV(cv=5, n_alphas=len(alpha_grid), alphas=alpha_grid, fit_intercept=True, normalize=False, random_state=42, verbose=True, n_jobs=5).fit(train_data_ct, train_phen) + lassoCV_model_ct = linear_model.LassoCV(cv=5, n_alphas=len(alpha_grid), alphas=alpha_grid, fit_intercept=True, random_state=42, verbose=True, n_jobs=5).fit(train_data_ct, train_phen) .. parsed-literal:: [Parallel(n_jobs=5)]: Using backend ThreadingBackend with 5 concurrent workers. - ...................................................................................................................[Parallel(n_jobs=5)]: Done 2 out of 5 | elapsed: 0.3s remaining: 0.5s - .....[Parallel(n_jobs=5)]: Done 5 out of 5 | elapsed: 0.3s finished - /usr/local/lib/python3.7/dist-packages/sklearn/linear_model/_base.py:155: FutureWarning: 'normalize' was deprecated in version 1.0 and will be removed in 1.2. Please leave the normalize parameter to its default value to silence this warning. The default behavior of this estimator is to not do any normalization. If normalization is needed please use sklearn.preprocessing.StandardScaler instead. - FutureWarning, + ....................................................................................................................[Parallel(n_jobs=5)]: Done 2 out of 5 | elapsed: 0.2s remaining: 0.3s + ....[Parallel(n_jobs=5)]: Done 5 out of 5 | elapsed: 0.3s finished .. code:: ipython3 @@ -770,7 +758,7 @@ Lasso (Linear Regression + L1 Regularization) -.. image:: other_predictive_models_files/other_predictive_models_55_0.png +.. image:: other_predictive_models_files/other_predictive_models_56_0.png .. code:: ipython3 @@ -786,22 +774,18 @@ Lasso (Linear Regression + L1 Regularization) -.. image:: other_predictive_models_files/other_predictive_models_56_0.png +.. image:: other_predictive_models_files/other_predictive_models_57_0.png .. code:: ipython3 # based on cv results above, set alpha=100 - lasso_model_z = linear_model.Lasso(alpha=lassoCV_model_z.alpha_, fit_intercept=True, normalize=False).fit(train_data_z, train_phen) - - + lasso_model_z = linear_model.Lasso(alpha=lassoCV_model_z.alpha_, fit_intercept=True).fit(train_data_z, train_phen) .. code:: ipython3 # based on cv results above, set alpha=100 - lasso_model_ct = linear_model.Lasso(alpha=lassoCV_model_ct.alpha_, fit_intercept=True, normalize=False).fit(train_data_ct, train_phen) - - + lasso_model_ct = linear_model.Lasso(alpha=lassoCV_model_ct.alpha_, fit_intercept=True).fit(train_data_ct, train_phen) .. code:: ipython3 @@ -832,23 +816,23 @@ Lasso (Linear Regression + L1 Regularization) Ridge (Linear Regression + L2 Regularization) --------------------------------------------------------- +============================================= .. code:: ipython3 - # RidgeCV uses generalized cross validation to select hyperparameter alpha + # RidgeCV uses generalized cross validation to select hyperparameter alpha with warnings.catch_warnings(): # ignore matrix decomposition errors warnings.simplefilter("ignore") - ridgeCV_model_z = linear_model.RidgeCV(alphas=(0.1, 1.0, 10.0), fit_intercept=True, normalize=False, cv=5).fit(train_data_z, train_phen) + ridgeCV_model_z = linear_model.RidgeCV(alphas=(0.1, 1.0, 10.0), fit_intercept=True, cv=5).fit(train_data_z, train_phen) .. code:: ipython3 - # RidgeCV uses generalized cross validation to select hyperparameter alpha + # RidgeCV uses generalized cross validation to select hyperparameter alpha with warnings.catch_warnings(): # ignore matrix decomposition errors warnings.simplefilter("ignore") - ridgeCV_model_ct = linear_model.RidgeCV(alphas=(0.1, 1.0, 10.0), fit_intercept=True, normalize=False, cv=5).fit(train_data_ct, train_phen) + ridgeCV_model_ct = linear_model.RidgeCV(alphas=(0.1, 1.0, 10.0), fit_intercept=True, cv=5).fit(train_data_ct, train_phen) .. code:: ipython3 @@ -874,14 +858,11 @@ Ridge (Linear Regression + L2 Regularization) .. code:: ipython3 - ridge_model_z = linear_model.Ridge(alpha=ridge_alpha_z, fit_intercept=True, normalize=False).fit(train_data_z, train_phen) - - + ridge_model_z = linear_model.Ridge(alpha=ridge_alpha_z, fit_intercept=True).fit(train_data_z, train_phen) .. code:: ipython3 - ridge_model_ct = linear_model.Ridge(alpha=ridge_alpha_ct, fit_intercept=True, normalize=False).fit(train_data_ct, train_phen) - + ridge_model_ct = linear_model.Ridge(alpha=ridge_alpha_ct, fit_intercept=True).fit(train_data_ct, train_phen) .. code:: ipython3 @@ -912,21 +893,81 @@ Ridge (Linear Regression + L2 Regularization) Elastic Net (Linear Regression + L1/L2 Regularization) ------------------------------------------------------------- +====================================================== .. code:: ipython3 - # RidgeCV uses generalized cross validation to select hyperparameter alpha + # RidgeCV uses generalized cross validation to select hyperparameter alpha elasticnetCV_model_z = linear_model.ElasticNetCV(l1_ratio=[.1, .5, .7, .9, .95, .99, 1], cv=5, n_alphas=len(alpha_grid), alphas=alpha_grid, random_state=42, verbose=True, n_jobs=5).fit(train_data_z, train_phen) +.. parsed-literal:: -.. code:: ipython3 - - # RidgeCV uses generalized cross validation to select hyperparameter alpha + [Parallel(n_jobs=5)]: Using backend ThreadingBackend with 5 concurrent workers. + ............................................................................................................./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.21318694590257792, tolerance: 0.0423918944559644 + model = cd_fast.enet_coordinate_descent_gram( + ./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.17936527851907158, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + .../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.8618322913218321, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 11.57867990423236, tolerance: 0.0423918944559644 + model = cd_fast.enet_coordinate_descent_gram( + ./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 10.2273489799189, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + ................../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.4036642558553467, tolerance: 0.04401109832998077 + model = cd_fast.enet_coordinate_descent_gram( + .................../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 8.073063075099014, tolerance: 0.04382929483334259 + model = cd_fast.enet_coordinate_descent_gram( + ../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 18.227358858718446, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ............................................/usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 14.883650580549045, tolerance: 0.04401109832998077 + model = cd_fast.enet_coordinate_descent_gram( + ....................................../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.18805636326129616, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ............../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0544661418971657, tolerance: 0.0423918944559644 + model = cd_fast.enet_coordinate_descent_gram( + ........................................................................................................./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.1788130249701112, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + ............/usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.13839227040918445, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ........................................................................................................../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.15009167262110168, tolerance: 0.04382929483334259 + model = cd_fast.enet_coordinate_descent_gram( + ........................................./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.20204581109658193, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + ............................/usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.09891903798924773, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ......................................................................./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.13078279402705562, tolerance: 0.04382929483334259 + model = cd_fast.enet_coordinate_descent_gram( + ../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.18265009272980137, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + ..../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0877297694903234, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ............................................................................................../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.11087317503455552, tolerance: 0.04382929483334259 + model = cd_fast.enet_coordinate_descent_gram( + ...................../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.16051209546739642, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + ..................................../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.07594686106816084, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ............................................................./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.10611096508367268, tolerance: 0.04382929483334259 + model = cd_fast.enet_coordinate_descent_gram( + ...../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.15375414865695802, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + ../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.07308221069854426, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ..[Parallel(n_jobs=5)]: Done 35 out of 35 | elapsed: 6.3s finished + + +.. code:: ipython3 + + # RidgeCV uses generalized cross validation to select hyperparameter alpha elasticnetCV_model_ct = linear_model.ElasticNetCV(l1_ratio=[.1, .5, .7, .9, .95, .99, 1], cv=5, n_alphas=len(alpha_grid), alphas=alpha_grid, random_state=42, verbose=True, n_jobs=5).fit(train_data_ct, train_phen) +.. parsed-literal:: + + [Parallel(n_jobs=5)]: Using backend ThreadingBackend with 5 concurrent workers. + ........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................[Parallel(n_jobs=5)]: Done 35 out of 35 | elapsed: 1.8s finished + .. code:: ipython3 @@ -958,7 +999,7 @@ Elastic Net (Linear Regression + L1/L2 Regularization) -.. image:: other_predictive_models_files/other_predictive_models_72_0.png +.. image:: other_predictive_models_files/other_predictive_models_73_0.png .. code:: ipython3 @@ -975,12 +1016,12 @@ Elastic Net (Linear Regression + L1/L2 Regularization) -.. image:: other_predictive_models_files/other_predictive_models_73_0.png +.. image:: other_predictive_models_files/other_predictive_models_74_0.png .. code:: ipython3 - elasticnet_model_z = linear_model.ElasticNet(alpha=elasticnetCV_model_z.alpha_, l1_ratio=elasticnetCV_model_z.l1_ratio_, fit_intercept=True, normalize=False, random_state=42).fit(train_data_z, train_phen) + elasticnet_model_z = linear_model.ElasticNet(alpha=elasticnetCV_model_z.alpha_, l1_ratio=elasticnetCV_model_z.l1_ratio_, fit_intercept=True, random_state=42).fit(train_data_z, train_phen) train_preds_en_model_z = elasticnet_model_z.predict(train_data_z) test_preds_en_model_z = elasticnet_model_z.predict(test_data_z) @@ -988,7 +1029,7 @@ Elastic Net (Linear Regression + L1/L2 Regularization) train_mae_z = metrics.mean_absolute_error(train_phen, train_preds_en_model_z) test_mae_z = metrics.mean_absolute_error(test_phen, test_preds_en_model_z) - elasticnet_model_ct = linear_model.ElasticNet(alpha=elasticnetCV_model_ct.alpha_, l1_ratio=elasticnetCV_model_ct.l1_ratio_, fit_intercept=True, normalize=False, random_state=42).fit(train_data_ct, train_phen) + elasticnet_model_ct = linear_model.ElasticNet(alpha=elasticnetCV_model_ct.alpha_, l1_ratio=elasticnetCV_model_ct.l1_ratio_, fit_intercept=True, random_state=42).fit(train_data_ct, train_phen) train_preds_en_model_ct = elasticnet_model_ct.predict(train_data_ct) test_preds_en_model_ct = elasticnet_model_ct.predict(test_data_ct) @@ -1008,3 +1049,5 @@ Elastic Net (Linear Regression + L1/L2 Regularization) Test MAE Z model: 0.680 Train MAE CT model: 0.633 Test MAE CT model: 0.692 + + diff --git a/doc/build/html/_static/_sphinx_javascript_frameworks_compat.js b/doc/build/html/_static/_sphinx_javascript_frameworks_compat.js index 8549469d..81415803 100644 --- a/doc/build/html/_static/_sphinx_javascript_frameworks_compat.js +++ b/doc/build/html/_static/_sphinx_javascript_frameworks_compat.js @@ -1,20 +1,9 @@ -/* - * _sphinx_javascript_frameworks_compat.js - * ~~~~~~~~~~ - * - * Compatability shim for jQuery and underscores.js. - * - * WILL BE REMOVED IN Sphinx 6.0 - * xref RemovedInSphinx60Warning +/* Compatability shim for jQuery and underscores.js. * + * Copyright Sphinx contributors + * Released under the two clause BSD licence */ -/** - * select a different prefix for underscore - */ -$u = _.noConflict(); - - /** * small helper function to urldecode strings * diff --git a/doc/build/html/_static/basic.css b/doc/build/html/_static/basic.css index 08896771..7ebbd6d0 100644 --- a/doc/build/html/_static/basic.css +++ b/doc/build/html/_static/basic.css @@ -1,12 +1,5 @@ /* - * basic.css - * ~~~~~~~~~ - * * Sphinx stylesheet -- basic theme. - * - * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * */ /* -- main layout ----------------------------------------------------------- */ @@ -115,15 +108,11 @@ img { /* -- search page ----------------------------------------------------------- */ ul.search { - margin: 10px 0 0 20px; - padding: 0; + margin-top: 10px; } ul.search li { - padding: 5px 0 5px 20px; - background-image: url(file.png); - background-repeat: no-repeat; - background-position: 0 7px; + padding: 5px 0; } ul.search li a { @@ -237,6 +226,10 @@ a.headerlink { visibility: hidden; } +a:visited { + color: #551A8B; +} + h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, @@ -324,17 +317,17 @@ 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; @@ -375,7 +368,6 @@ 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; @@ -385,7 +377,6 @@ div.sidebar::after, aside.sidebar::after, nav.contents::after, aside.topic::after, - div.topic::after, div.admonition::after, blockquote::after { @@ -611,25 +602,6 @@ ul.simple p { margin-bottom: 0; } -/* Docutils 0.17 and older (footnotes & citations) */ -dl.footnote > dt, -dl.citation > dt { - float: left; - margin-right: 0.5em; -} - -dl.footnote > dd, -dl.citation > dd { - margin-bottom: 0em; -} - -dl.footnote > dd:after, -dl.citation > dd:after { - content: ""; - clear: both; -} - -/* Docutils 0.18+ (footnotes & citations) */ aside.footnote > span, div.citation > span { float: left; @@ -654,8 +626,6 @@ div.citation > p:last-of-type:after { clear: both; } -/* Footnotes & citations ends */ - dl.field-list { display: grid; grid-template-columns: fit-content(30%) auto; @@ -668,10 +638,6 @@ dl.field-list > dt { padding-right: 5px; } -dl.field-list > dt:after { - content: ":"; -} - dl.field-list > dd { padding-left: 0.5em; margin-top: 0em; @@ -697,6 +663,16 @@ dd { margin-left: 30px; } +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + dl > dd:last-child, dl > dd:last-child > :last-child { margin-bottom: 0; @@ -765,6 +741,14 @@ abbr, acronym { cursor: help; } +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + /* -- code displays --------------------------------------------------------- */ pre { diff --git a/doc/build/html/_static/css/badge_only.css b/doc/build/html/_static/css/badge_only.css index c718cee4..88ba55b9 100644 --- a/doc/build/html/_static/css/badge_only.css +++ b/doc/build/html/_static/css/badge_only.css @@ -1 +1 @@ -.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 +.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-other-versions .rtd-current-item{font-weight:700}.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}}#flyout-search-form{padding:6px} \ 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 19a446a0..0f14f106 100644 --- a/doc/build/html/_static/css/theme.css +++ b/doc/build/html/_static/css/theme.css @@ -1,4 +1,4 @@ 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 button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,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?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;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.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 p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.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 .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.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.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,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.citation>dd,html.writer-html5 .rst-content dl.citation>dt,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.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,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.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}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 aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.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 aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.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{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;overflow-wrap:normal}.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}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):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(.citation):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(.citation):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(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>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(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>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(.citation):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(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):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(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):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(.citation):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(.citation):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(.citation):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(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):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,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.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 + */@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;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.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 .wy-dropdown>aactive,.wy-side-nav-search .wy-dropdown>afocus,.wy-side-nav-search>a:hover,.wy-side-nav-search>aactive,.wy-side-nav-search>afocus{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,.wy-side-nav-search>a.icon{display:block}.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.switch-menus{position:relative;display:block;margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-side-nav-search>div.switch-menus>div.language-switch,.wy-side-nav-search>div.switch-menus>div.version-switch{display:inline-block;padding:.2em}.wy-side-nav-search>div.switch-menus>div.language-switch select,.wy-side-nav-search>div.switch-menus>div.version-switch select{display:inline-block;margin-right:-2rem;padding-right:2rem;max-width:240px;text-align-last:center;background:none;border:none;border-radius:0;box-shadow:none;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-size:1em;font-weight:400;color:hsla(0,0%,100%,.3);cursor:pointer;appearance:none;-webkit-appearance:none;-moz-appearance:none}.wy-side-nav-search>div.switch-menus>div.language-switch select:active,.wy-side-nav-search>div.switch-menus>div.language-switch select:focus,.wy-side-nav-search>div.switch-menus>div.language-switch select:hover,.wy-side-nav-search>div.switch-menus>div.version-switch select:active,.wy-side-nav-search>div.switch-menus>div.version-switch select:focus,.wy-side-nav-search>div.switch-menus>div.version-switch select:hover{background:hsla(0,0%,100%,.1);color:hsla(0,0%,100%,.5)}.wy-side-nav-search>div.switch-menus>div.language-switch select option,.wy-side-nav-search>div.switch-menus>div.version-switch select option{color:#000}.wy-side-nav-search>div.switch-menus>div.language-switch:has(>select):after,.wy-side-nav-search>div.switch-menus>div.version-switch:has(>select):after{display:inline-block;width:1.5em;height:100%;padding:.1em;content:"\f0d7";font-size:1em;line-height:1.2em;font-family:FontAwesome;text-align:center;pointer-events:none;box-sizing:border-box}.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-other-versions .rtd-current-item{font-weight:700}.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}}#flyout-search-form{padding:6px}.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 p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.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 .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.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.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,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.citation>dd,html.writer-html5 .rst-content dl.citation>dt,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.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,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.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}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 aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.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 aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.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{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;overflow-wrap:normal}.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}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):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(.citation):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(.citation):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(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>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(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>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(.citation):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(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):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(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):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(.citation):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(.citation):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(.citation):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(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):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,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.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 c3db08d1..0398ebb9 100644 --- a/doc/build/html/_static/doctools.js +++ b/doc/build/html/_static/doctools.js @@ -1,15 +1,15 @@ /* - * doctools.js - * ~~~~~~~~~~~ - * * Base JavaScript utilities for all Sphinx HTML 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(); @@ -18,73 +18,11 @@ const _ready = (callback) => { } }; -/** - * highlight a given string on a node by wrapping it in - * span elements with the given class name. - */ -const _highlight = (node, addItems, text, className) => { - if (node.nodeType === Node.TEXT_NODE) { - const val = node.nodeValue; - const parent = node.parentNode; - const pos = val.toLowerCase().indexOf(text); - if ( - pos >= 0 && - !parent.classList.contains(className) && - !parent.classList.contains("nohighlight") - ) { - let span; - - const closestNode = parent.closest("body, svg, foreignObject"); - const isInSVG = closestNode && closestNode.matches("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.classList.add(className); - } - - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - parent.insertBefore( - span, - parent.insertBefore( - document.createTextNode(val.substr(pos + text.length)), - node.nextSibling - ) - ); - node.nodeValue = val.substr(0, pos); - - if (isInSVG) { - const rect = document.createElementNS( - "http://www.w3.org/2000/svg", - "rect" - ); - const bbox = parent.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: parent, target: rect }); - } - } - } else if (node.matches && !node.matches("button, select, textarea")) { - node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); - } -}; -const _highlightText = (thisNode, text, className) => { - let addItems = []; - _highlight(thisNode, addItems, text, className); - addItems.forEach((obj) => - obj.parent.insertAdjacentElement("beforebegin", obj.target) - ); -}; - /** * Small JavaScript module for the documentation. */ const Documentation = { init: () => { - Documentation.highlightSearchWords(); Documentation.initDomainIndexTable(); Documentation.initOnKeyListeners(); }, @@ -126,51 +64,6 @@ const Documentation = { Documentation.LOCALE = catalog.locale; }, - /** - * highlight the search words provided in the url in the text - */ - highlightSearchWords: () => { - const highlight = - new URLSearchParams(window.location.search).get("highlight") || ""; - const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); - if (terms.length === 0) return; // nothing to do - - // There should never be more than one element matching "div.body" - const divBody = document.querySelectorAll("div.body"); - const body = divBody.length ? divBody[0] : document.querySelector("body"); - window.setTimeout(() => { - terms.forEach((term) => _highlightText(body, term, "highlighted")); - }, 10); - - const searchBox = document.getElementById("searchbox"); - if (searchBox === null) return; - searchBox.appendChild( - document - .createRange() - .createContextualFragment( - '" - ) - ); - }, - - /** - * helper function to hide the search marks again - */ - hideSearchWords: () => { - document - .querySelectorAll("#searchbox .highlight-link") - .forEach((el) => el.remove()); - document - .querySelectorAll("span.highlighted") - .forEach((el) => el.classList.remove("highlighted")); - const url = new URL(window.location); - url.searchParams.delete("highlight"); - window.history.replaceState({}, "", url); - }, - /** * helper function to focus on search bar */ @@ -210,15 +103,11 @@ const Documentation = { ) return; - const blacklistedElements = new Set([ - "TEXTAREA", - "INPUT", - "SELECT", - "BUTTON", - ]); document.addEventListener("keydown", (event) => { - if (blacklistedElements.has(document.activeElement.tagName)) return; // bail for input elements - if (event.altKey || event.ctrlKey || event.metaKey) return; // bail with special keys + // 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) { @@ -240,10 +129,6 @@ const Documentation = { event.preventDefault(); } break; - case "Escape": - if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; - Documentation.hideSearchWords(); - event.preventDefault(); } } diff --git a/doc/build/html/_static/documentation_options.js b/doc/build/html/_static/documentation_options.js index 908cd41f..b86f7e34 100644 --- a/doc/build/html/_static/documentation_options.js +++ b/doc/build/html/_static/documentation_options.js @@ -1,5 +1,4 @@ -var DOCUMENTATION_OPTIONS = { - URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), +const DOCUMENTATION_OPTIONS = { VERSION: '0.20', LANGUAGE: 'en', COLLAPSE_INDEX: false, @@ -10,5 +9,5 @@ var DOCUMENTATION_OPTIONS = { SOURCELINK_SUFFIX: '.txt', NAVIGATION_WITH_KEYS: false, SHOW_SEARCH_SUMMARY: true, - ENABLE_SEARCH_SHORTCUTS: false, + ENABLE_SEARCH_SHORTCUTS: true, }; \ No newline at end of file diff --git a/doc/build/html/_static/graphviz.css b/doc/build/html/_static/graphviz.css index 19e7afd3..30f3837b 100644 --- a/doc/build/html/_static/graphviz.css +++ b/doc/build/html/_static/graphviz.css @@ -1,12 +1,5 @@ /* - * graphviz.css - * ~~~~~~~~~~~~ - * * Sphinx stylesheet -- graphviz extension. - * - * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * */ img.graphviz { diff --git a/doc/build/html/_static/language_data.js b/doc/build/html/_static/language_data.js index 2e22b06a..c7fe6c6f 100644 --- a/doc/build/html/_static/language_data.js +++ b/doc/build/html/_static/language_data.js @@ -1,19 +1,12 @@ /* - * language_data.js - * ~~~~~~~~~~~~~~~~ - * * This script contains the language-specific data used by searchtools.js, * namely the list of stopwords, stemmer, scorer and splitter. - * - * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * */ var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; -/* Non-minified version is copied as a separate JS file, is available */ +/* Non-minified version is copied as a separate JS file, if available */ /** * Porter Stemmer diff --git a/doc/build/html/_static/pygments.css b/doc/build/html/_static/pygments.css index 691aeb82..0d49244e 100644 --- a/doc/build/html/_static/pygments.css +++ b/doc/build/html/_static/pygments.css @@ -17,6 +17,7 @@ span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: .highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ .highlight .gd { color: #A00000 } /* Generic.Deleted */ .highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ .highlight .gr { color: #FF0000 } /* Generic.Error */ .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ .highlight .gi { color: #00A000 } /* Generic.Inserted */ diff --git a/doc/build/html/_static/searchtools.js b/doc/build/html/_static/searchtools.js index ac4d5861..2c774d17 100644 --- a/doc/build/html/_static/searchtools.js +++ b/doc/build/html/_static/searchtools.js @@ -1,12 +1,5 @@ /* - * searchtools.js - * ~~~~~~~~~~~~~~~~ - * * Sphinx JavaScript utilities for the full-text search. - * - * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * */ "use strict"; @@ -20,7 +13,7 @@ if (typeof Scorer === "undefined") { // and returns the new score. /* score: result => { - const [docname, title, anchor, descr, score, filename] = result + const [docname, title, anchor, descr, score, filename, kind] = result return score }, */ @@ -47,6 +40,14 @@ if (typeof Scorer === "undefined") { }; } +// Global search result kind enum, used by themes to style search results. +class SearchResultKind { + static get index() { return "index"; } + static get object() { return "object"; } + static get text() { return "text"; } + static get title() { return "title"; } +} + const _removeChildren = (element) => { while (element && element.lastChild) element.removeChild(element.lastChild); }; @@ -57,16 +58,20 @@ const _removeChildren = (element) => { const _escapeRegExp = (string) => string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string -const _displayItem = (item, highlightTerms, searchTerms) => { +const _displayItem = (item, searchTerms, highlightTerms) => { 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 contentRoot = document.documentElement.dataset.content_root; - const [docName, title, anchor, descr] = item; + const [docName, title, anchor, descr, score, _filename, kind] = item; let listItem = document.createElement("li"); + // Add a class representing the item's type: + // can be used by a theme's CSS selector for styling + // See SearchResultKind for the class names. + listItem.classList.add(`kind-${kind}`); let requestUrl; let linkUrl; if (docBuilder === "dirhtml") { @@ -75,29 +80,35 @@ const _displayItem = (item, highlightTerms, searchTerms) => { if (dirname.match(/\/index\/$/)) dirname = dirname.substring(0, dirname.length - 6); else if (dirname === "index/") dirname = ""; - requestUrl = docUrlRoot + dirname; + requestUrl = contentRoot + dirname; linkUrl = requestUrl; } else { // normal html builders - requestUrl = docUrlRoot + docName + docFileSuffix; + requestUrl = contentRoot + docName + docFileSuffix; linkUrl = docName + docLinkSuffix; } - const params = new URLSearchParams(); - params.set("highlight", [...highlightTerms].join(" ")); let linkEl = listItem.appendChild(document.createElement("a")); - linkEl.href = linkUrl + "?" + params.toString() + anchor; + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; linkEl.innerHTML = title; - if (descr) - listItem.appendChild(document.createElement("span")).innerText = + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } else if (showSearchSummary) fetch(requestUrl) .then((responseData) => responseData.text()) .then((data) => { if (data) listItem.appendChild( - Search.makeSearchSummary(data, searchTerms, highlightTerms) + Search.makeSearchSummary(data, searchTerms, anchor) ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); }); Search.output.appendChild(listItem); }; @@ -109,28 +120,46 @@ const _finishSearch = (resultCount) => { "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.` - ); + Search.status.innerText = Documentation.ngettext( + "Search finished, found one page matching the search query.", + "Search finished, found ${resultCount} pages matching the search query.", + resultCount, + ).replace('${resultCount}', resultCount); }; const _displayNextItem = ( results, resultCount, + searchTerms, highlightTerms, - 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(), highlightTerms, searchTerms); + _displayItem(results.pop(), searchTerms, highlightTerms); setTimeout( - () => _displayNextItem(results, resultCount, highlightTerms, searchTerms), + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), 5 ); } // search finished, update title and status message else _finishSearch(resultCount); }; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename, kind]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; /** * Default splitQuery function. Can be overridden in ``sphinx.search`` with a @@ -154,15 +183,26 @@ const Search = { _queued_query: null, _pulse_status: -1, - htmlToText: (htmlString) => { - const htmlElement = document - .createRange() - .createContextualFragment(htmlString); - _removeChildren(htmlElement.querySelectorAll(".headerlink")); + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content const docContent = htmlElement.querySelector('[role="main"]'); - if (docContent !== undefined) return docContent.textContent; + if (docContent) 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." + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." ); return ""; }, @@ -215,6 +255,7 @@ const Search = { searchSummary.classList.add("search-summary"); searchSummary.innerText = ""; const searchList = document.createElement("ul"); + searchList.setAttribute("role", "list"); searchList.classList.add("search"); const out = document.getElementById("search-results"); @@ -235,10 +276,7 @@ const Search = { else Search.deferQuery(query); }, - /** - * execute search (requires search index to be loaded) - */ - query: (query) => { + _parseQuery: (query) => { // stem the search terms and add them to the correct list const stemmer = new Stemmer(); const searchTerms = new Set(); @@ -266,40 +304,98 @@ const Search = { } }); + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + // console.debug("SEARCH: searching for:"); // console.info("required: ", [...searchTerms]); // console.info("excluded: ", [...excludedTerms]); - // array of [docname, title, anchor, descr, score, filename] - let results = []; + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename, kind]. + const normalResults = []; + const nonMainIndexResults = []; + _removeChildren(document.getElementById("search-progress")); + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + SearchResultKind.title, + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + SearchResultKind.index, + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + // lookup as object objectTerms.forEach((term) => - results.push(...Search.performObjectSearch(term, objectTerms)) + normalResults.push(...Search.performObjectSearch(term, objectTerms)) ); // lookup as search terms in fulltext - results.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); // let the scorer override scores with a custom scoring function - if (Scorer.score) results.forEach((item) => (item[4] = Scorer.score(item))); - - // now sort the results by score (in opposite order of appearance, since the - // display function below uses pop() to retrieve items) and then - // alphabetically - results.sort((a, b) => { - const leftScore = a[4]; - const rightScore = b[4]; - if (leftScore === rightScore) { - // same score: sort alphabetically - const leftTitle = a[1].toLowerCase(); - const rightTitle = b[1].toLowerCase(); - if (leftTitle === rightTitle) return 0; - return leftTitle > rightTitle ? -1 : 1; // inverted is intentional - } - return leftScore > rightScore ? 1 : -1; - }); + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; // remove duplicate search results // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept @@ -313,14 +409,19 @@ const Search = { return acc; }, []); - results = results.reverse(); + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); // for debugging //Search.lastresults = results.slice(); // a copy // console.info("search results:", Search.lastresults); // print the results - _displayNextItem(results, results.length, highlightTerms, searchTerms); + _displayNextItem(results, results.length, searchTerms, highlightTerms); }, /** @@ -384,6 +485,7 @@ const Search = { descr, score, filenames[match[0]], + SearchResultKind.object, ]); }; Object.keys(objects).forEach((prefix) => @@ -401,8 +503,8 @@ const Search = { // prepare search const terms = Search._index.terms; const titleTerms = Search._index.titleterms; - const docNames = Search._index.docnames; const filenames = Search._index.filenames; + const docNames = Search._index.docnames; const titles = Search._index.titles; const scoreMap = new Map(); @@ -418,14 +520,18 @@ const Search = { // add support for partial matches if (word.length > 2) { const escapedWord = _escapeRegExp(word); - Object.keys(terms).forEach((term) => { - if (term.match(escapedWord) && !terms[word]) - arr.push({ files: terms[term], score: Scorer.partialTerm }); - }); - Object.keys(titleTerms).forEach((term) => { - if (term.match(escapedWord) && !titleTerms[word]) - arr.push({ files: titleTerms[word], score: Scorer.partialTitle }); - }); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } } // no match but word was a required one @@ -448,9 +554,8 @@ const Search = { // create the mapping files.forEach((file) => { - if (fileMap.has(file) && fileMap.get(file).indexOf(word) === -1) - fileMap.get(file).push(word); - else fileMap.set(file, [word]); + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); }); }); @@ -491,6 +596,7 @@ const Search = { null, score, filenames[file], + SearchResultKind.text, ]); } return results; @@ -499,16 +605,15 @@ const Search = { /** * helper function to return a node containing the * search summary for a given text. keywords is a list - * of stemmed words, highlightWords is the list of normal, unstemmed - * words. the first one is used to find the occurrence, the - * latter for highlighting it. + * of stemmed words. */ - makeSearchSummary: (htmlText, keywords, highlightWords) => { - const text = Search.htmlToText(htmlText).toLowerCase(); + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); if (text === "") return null; + const textLower = text.toLowerCase(); const actualStartPosition = [...keywords] - .map((k) => text.indexOf(k.toLowerCase())) + .map((k) => textLower.indexOf(k.toLowerCase())) .filter((i) => i > -1) .slice(-1)[0]; const startWithContext = Math.max(actualStartPosition - 120, 0); @@ -516,13 +621,9 @@ const Search = { const top = startWithContext === 0 ? "" : "..."; const tail = startWithContext + 240 < text.length ? "..." : ""; - let summary = document.createElement("div"); + let summary = document.createElement("p"); summary.classList.add("context"); - summary.innerText = top + text.substr(startWithContext, 240).trim() + tail; - - highlightWords.forEach((highlightWord) => - _highlightText(summary, highlightWord, "highlighted") - ); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; return summary; }, diff --git a/doc/build/html/_static/sphinx_highlight.js b/doc/build/html/_static/sphinx_highlight.js index aae669d7..8a96c69a 100644 --- a/doc/build/html/_static/sphinx_highlight.js +++ b/doc/build/html/_static/sphinx_highlight.js @@ -29,14 +29,19 @@ const _highlight = (node, addItems, text, className) => { } span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); parent.insertBefore( span, parent.insertBefore( - document.createTextNode(val.substr(pos + text.length)), + rest, node.nextSibling ) ); node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); if (isInSVG) { const rect = document.createElementNS( @@ -140,5 +145,10 @@ const SphinxHighlight = { }, }; -_ready(SphinxHighlight.highlightSearchWords); -_ready(SphinxHighlight.initEscapeListener); +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/doc/build/html/genindex.html b/doc/build/html/genindex.html index 1509fff9..d55d9018 100644 --- a/doc/build/html/genindex.html +++ b/doc/build/html/genindex.html @@ -1,28 +1,24 @@ + + - + Index — Predictive Clinical Neuroscience Toolkit 0.20 documentation - - - + + + - - - - - - - - - + + + + + @@ -62,13 +58,24 @@

Tutorials

Other Useful Stuff

    @@ -225,6 +232,8 @@

    D

    E

      +
    • entrypoint() (in module normative) +
    • estimate() (bayesreg.BLR method)
        diff --git a/doc/build/html/index.html b/doc/build/html/index.html index 680db572..aeb963b5 100644 --- a/doc/build/html/index.html +++ b/doc/build/html/index.html @@ -1,28 +1,25 @@ + + - + - + Predictive Clinical Neuroscience toolkit — Predictive Clinical Neuroscience Toolkit 0.20 documentation - - - + + + - - - - - - - - + + + + + @@ -63,13 +60,24 @@

      Tutorials

      Other Useful Stuff

      Tutorials

        -
      • Gaussian Process Regression
      • -
      • Hierarchical Bayesian Regression
      • +
      • DEMO ON NORMATIVE MODELING
      • +
      • Predictive Clinical Neuroscience Toolkit
      • +
      • Hierarchical Bayesian Regression Normative Modelling and Transfer onto unseen site.
      • Braincharts: transfer
      • -
      • Bayesian Linear Regression
          -
        • Data Preparation
            -
          • Install necessary libraries & grab data files
          • -
          • Prepare covariate data
          • -
          • Prepare brain data
          • -
          • Combine covariate & cortical thickness dataframes
          • -
          • Add variable to model site/scanner effects
          • -
          • Train/test split
          • -
          • Setup output directories
          • +
          • Predictive Clinical Neuroscience Toolkit
          • +
          • The Normative Modeling Framework for Computational Psychiatry Protocol +
          • +
          • Data Preparation
              +
            • Install necessary libraries & grab data files +
            • +
            • Prepare covariate data +
            • +
            • Preprare brain data +
            • +
            • Combine covariate & cortical thickness dataframes +
            • +
            • Add variable to model site/scanner effects +
            • +
            • Train/test split
            • -
            • Algorithm & Modeling
            • -
            • Evaluation & Interpretation

              Other Useful Stuff

                @@ -118,7 +156,7 @@

                Tutorials

                Other Useful Stuff

                  @@ -105,9 +113,9 @@
                  -

                  Frequently Asked Questions

                  +

                  Frequently Asked Questions

                  Most of the questions we recieve are about interpretation of normative modeling outputs.

                  -

                  The PCNtoolkit develoers have written a protocol for how to run a normative modeling analysis which should be helpful to you if you are just getting started.

                  +

                  The PCNtoolkit developers have written a protocol for how to run a normative modeling analysis which should be helpful to you if you are just getting started.

                  Rutherford, S., Kia, S. M., Wolfers, T., … Beckmann, C. F., & Marquand, A. F. (2022). The Normative Modeling Framework for Computational Psychiatry. Nature Protocols. https://www.nature.com/articles/s41596-022-00696-5.

                  Feel free to interact with the normative modeling community and browse the existing questions/answers on Gitter

                  @@ -116,7 +124,7 @@

                  Frequently Asked Questions - +

                  diff --git a/doc/build/html/pages/HBR_NormativeModel_FCONdata_Tutorial.html b/doc/build/html/pages/HBR_NormativeModel_FCONdata_Tutorial.html index ed46b8dc..6364c591 100644 --- a/doc/build/html/pages/HBR_NormativeModel_FCONdata_Tutorial.html +++ b/doc/build/html/pages/HBR_NormativeModel_FCONdata_Tutorial.html @@ -1,33 +1,30 @@ + + - + - + - HBR tutorial — Predictive Clinical Neuroscience Toolkit 0.20 documentation - - - + Predictive Clinical Neuroscience Toolkit — Predictive Clinical Neuroscience Toolkit 0.20 documentation + + + - - - - - - - - + + + + + - - + + @@ -64,25 +61,40 @@

                Tutorials

                  -
                • Gaussian Process Regression
                • -
                • Hierarchical Bayesian Regression
                    -
                  • Step 0: Install necessary libraries & grab data files

                    Other Useful Stuff

                      @@ -106,7 +118,7 @@

                      Tutorials

                      Other Useful Stuff

                        @@ -104,7 +112,7 @@
                        -

                        Acknowledgements

                        +

                        Acknowledgements

                        We gratefully acknowledge funding from the Dutch Organisation for Scientific Research (NWO), via a Vernieuwingsimpuls VIDI fellowship, from the UK Wellcome Trust via a Digital Innovator grant and from the UK Medical Research Council via an Experimental Medicine Challenge Grant.

                        Core developers of the toolbox are:

                          diff --git a/doc/build/html/pages/apply_normative_models.html b/doc/build/html/pages/apply_normative_models.html index 08c0414c..8e8c6545 100644 --- a/doc/build/html/pages/apply_normative_models.html +++ b/doc/build/html/pages/apply_normative_models.html @@ -1,33 +1,30 @@ + + - + - + - Braincharts tutorial — Predictive Clinical Neuroscience Toolkit 0.20 documentation - - - + Using lifespan models to make predictions on new data — Predictive Clinical Neuroscience Toolkit 0.20 documentation + + + - - - - - - - - + + + + + - - + + @@ -64,24 +61,33 @@

                        Tutorials

                          -
                        • Gaussian Process Regression
                        • -
                        • Hierarchical Bayesian Regression
                        • -
                        • Braincharts: transfer

                          Other Useful Stuff

                            @@ -105,7 +111,7 @@

                            Tutorials

                            Other Useful Stuff

                              @@ -105,7 +113,7 @@
                              -

                              How to cite PCNtoolkit

                              +

                              How to cite PCNtoolkit

                              If you use the PCNtoolkit, please consider citing some of the following work:

                              Marquand, A. F., Wolfers, T., Mennes, M., Buitelaar, J., & Beckmann, C. F. (2016). Beyond Lumping and Splitting: A Review of Computational Approaches for Stratifying Psychiatric Disorders. Biological Psychiatry: Cognitive Neuroscience and Neuroimaging. https://doi.org/10.1016/j.bpsc.2016.04.002

                              Marquand, A. F., Rezek, I., Buitelaar, J., & Beckmann, C. F. (2016). Understanding Heterogeneity in Clinical Cohorts Using Normative Models: Beyond Case-Control Studies. Biological Psychiatry. https://doi.org/10.1016/j.biopsych.2015.12.023

                              diff --git a/doc/build/html/pages/glossary.html b/doc/build/html/pages/glossary.html index 0a7ac5d3..82c923ce 100644 --- a/doc/build/html/pages/glossary.html +++ b/doc/build/html/pages/glossary.html @@ -1,28 +1,25 @@ + + - + - + Glossary — Predictive Clinical Neuroscience Toolkit 0.20 documentation - - - + + + - - - - - - - - + + + + + @@ -64,13 +61,24 @@

                            Tutorials

                            Other Useful Stuff

                            -.. code:: ipython3 - cov.groupby(['site']).describe() +Preprare brain data +------------------- -Prepare brain data -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Step 3. +~~~~~~~ Next, format and combine the MRI data using the following commands. The example data contains cortical thickness maps estimated by running @@ -139,7 +448,7 @@ was reduced by using ROIs from the Desikan-Killiany atlas. Including the Euler number as a covariate is also recommended, as this is a proxy metric for data quality. The `Euler number `__ from -each subjects recon-all output folder was extracted into a text file +each subject’s recon-all output folder was extracted into a text file and is merged into the cortical thickness data frame. The Euler number is site-specific, thus, to use the same exclusion threshold across sites it is important to center the site by subtracting the site median from @@ -147,7 +456,7 @@ all subjects at a site. Then take the square root and multiply by negative one and exclude any subjects with a square root above 10. Here is some psuedo-code (run from a terminal in the folder that has all -subjects recon-all output folders) that was used to extract these ROIs: +subject’s recon-all output folders) that was used to extract these ROIs: ``export SUBJECTS_DIR=/path/to/study/freesurfer_data/`` @@ -157,14 +466,14 @@ subjects recon-all output folders) that was used to extract these ROIs: .. code:: ipython3 - hcpya = pd.read_csv('/content/PCNtoolkit-demo/data/HCP1200_aparc_thickness.csv') - ixi = pd.read_csv('/content/PCNtoolkit-demo/data/IXI_aparc_thickness.csv') + hcpya = pd.read_csv(os.path.join(wdir,'data','HCP1200_aparc_thickness.csv')) + ixi = pd.read_csv(os.path.join(wdir,'data','IXI_aparc_thickness.csv')) .. code:: ipython3 brain_all = pd.merge(ixi, hcpya, how='outer') -We extracted the euler number from each subjects recon-all output +We extracted the euler number from each subject’s recon-all output folder into a text file and we now need to format and combine these into our brain dataframe. @@ -173,12 +482,12 @@ recon-all.log for each subject. Run this from the terminal in the folder where your subjects recon-all output folders are located. This assumes that all of your subject IDs start with “sub-” prefix. -:literal:`for i in sub-*; do if [[ -e ${i}/scripts/recon-all.log ]]; then cat ${i}/scripts/recon-all.log | grep -A 1 "Computing euler" > temp_log; lh_en=$(cat temp_log | head -2 | tail -1 | awk -F '=' '{print $2}' | awk -F ',' '{print $1}'); rh_en=$(cat temp_log | head -2 | tail -1 | awk -F '=' '{print $3}'); echo "${i}, ${lh_en}, ${rh_en}" >> euler.csv; echo ${i}; fi; done` +:literal:`for i in sub-\*; do if [[ -e ${i}/scripts/recon-all.log ]]; then cat ${i}/scripts/recon-all.log | grep -A 1 "Computing euler" > temp_log; lh_en=`cat temp_log | head -2 | tail -1 | awk -F '=' '{print $2}' | awk -F ',' '{print $1}'\`; rh_en=`cat temp_log | head -2 | tail -1 | awk -F '=' '{print $3}'\`; echo "${i}, ${lh_en}, ${rh_en}" >> euler.csv; echo ${i}; fi; done` .. code:: ipython3 - hcp_euler = pd.read_csv('/content/PCNtoolkit-demo/data/hcp-ya_euler.csv') - ixi_euler = pd.read_csv('/content/PCNtoolkit-demo/data/ixi_euler.csv') + hcp_euler = pd.read_csv(os.path.join(wdir,'data','hcp-ya_euler.csv')) + ixi_euler = pd.read_csv(os.path.join(wdir,'data','ixi_euler.csv')) .. code:: ipython3 @@ -221,7 +530,7 @@ inclusion is not too strict or too lenient. .. code:: ipython3 - df_euler.groupby(by='site').median() + df_euler.groupby(by='site')[['lh_euler', 'rh_euler', 'avg_euler']].median() @@ -229,9 +538,8 @@ inclusion is not too strict or too lenient. .. raw:: html -
                            -
                            -
                            +
                            +
                            - + +
                            + + +
                            + + + + + +
                            +
                            @@ -363,6 +805,13 @@ inclusion is not too strict or too lenient. df_euler['site_median'] = df_euler['site_median'].replace({'hcp':-43,'ixi':-56}) + +.. parsed-literal:: + + :1: FutureWarning: Downcasting behavior in `replace` is deprecated and will be removed in a future version. To retain the old behavior, explicitly call `result.infer_objects(copy=False)`. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)` + df_euler['site_median'] = df_euler['site_median'].replace({'hcp':-43,'ixi':-56}) + + .. code:: ipython3 df_euler['avg_euler_centered'] = df_euler['avg_euler'] - df_euler['site_median'] @@ -383,18 +832,19 @@ inclusion is not too strict or too lenient. brain_good = brain.query('avg_euler_centered_neg_sqrt < 10') -.. warning:: - **CRITICAL STEP:** If possible, data should be visually inspected to - verify that the data inclusion is not too strict or too lenient. - Subjects above the Euler number threshold should be manually checked to - verify and justify their exclusion due to poor data quality. This is - just one approach for automated QC used by the developers of the - PCNtoolkit. Other approaches such as the ENIGMA QC pipeline or UK - Biobanks QC pipeline are also viable options for automated QC. +**CRITICAL STEP:** If possible, data should be visually inspected to +verify that the data inclusion is not too strict or too lenient. +Subjects above the Euler number threshold should be manually checked to +verify and justify their exclusion due to poor data quality. This is +just one approach for automated QC used by the developers of the +PCNtoolkit. Other approaches such as the ENIGMA QC pipeline or UK +Biobank’s QC pipeline are also viable options for automated QC. Combine covariate & cortical thickness dataframes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------------- +Step 4. +~~~~~~~ The normative modeling function requires the covariate predictors and brain features to be in separate text files. However, it is important to @@ -407,7 +857,7 @@ their own dataframes, using the commands below. .. code:: ipython3 - # make sure to use how="inner" so that we only include subjects that have data in both the covariate and the cortical thickness files + # make sure to use how="inner" so that we only include subjects that have data in both the covariate and the cortical thickness files all_data = pd.merge(brain_good, cov, how='inner') .. code:: ipython3 @@ -433,13 +883,15 @@ their own dataframes, using the commands below. all_data_covariates = all_data[['age','sex','site']] -.. warning:: - **CRITICAL STEP:** ``roi_ids`` is a variable that represents which brain - areas will be modeled and can be used to select subsets of the data - frame if you do not wish to run models for the whole brain. +**CRITICAL STEP:** ``roi_ids`` is a variable that represents which brain +areas will be modeled and can be used to select subsets of the data +frame if you do not wish to run models for the whole brain. Add variable to model site/scanner effects -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------------ + +Step 5. +~~~~~~~ Currently, the different sites are coded in a single column (named ‘site’) and are represented as a string data type. However, the @@ -451,17 +903,689 @@ variables (0=not in this site, 1=present in this site). .. code:: ipython3 - all_data_covariates = pd.get_dummies(all_data_covariates, columns=['site']) + all_data_covariates = pd.get_dummies(all_data_covariates, columns=['site'], dtype=int) .. code:: ipython3 - all_data['Average_Thickness'] = all_data[['lh_MeanThickness_thickness','rh_MeanThickness_thickness']].mean(axis=1) + all_data_covariates.head() + + + + +.. raw:: html + + +
                            +
                            + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                            agesexsite_hcpsite_ixi
                            027.0110
                            127.0210
                            233.0110
                            327.0110
                            435.0210
                            +
                            +
                            + +
                            + + + + + +
                            + + +
                            + + + + + +
                            + +
                            +
                            + +.. code:: ipython3 + + all_data_covariates + + + + +.. raw:: html + + +
                            +
                            + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                            agesexsite_hcpsite_ixi
                            027.000000110
                            127.000000210
                            233.000000110
                            327.000000110
                            435.000000210
                            ...............
                            168747.723477101
                            168850.395619101
                            168942.989733101
                            169046.220397101
                            169141.741273101
                            +

                            1692 rows × 4 columns

                            +
                            +
                            + +
                            + + + + + +
                            + + +
                            + + + + + +
                            + +
                            + + + +
                            + +
                            +
                            + + + + +.. code:: ipython3 + + all_data['Average_Thickness'] = all_data[['lh_MeanThickness_thickness','rh_MeanThickness_thickness']].mean(axis=1) + Train/test split -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------- +Step 6. +~~~~~~~ In this example, we use 80% of the data for training and 20% for testing. Please carefully read the experimental design section on @@ -501,11 +1625,12 @@ Verify that your train & test arrays are the same size Test response size is: (339, 6) -.. warning:: - **CRITICAL STEP:** The model would not learn the site effects if all the - data from one site was only in the test set. Therefore, we stratify the - train/test split using the site variable. +**CRITICAL STEP:** The model would not learn the site effects if all the +data from one site was only in the test set. Therefore, we stratify the +train/test split using the site variable. +Step 7. +~~~~~~~ When the data were split into train and test sets, the row index was not reset. This means that the row index in the train and test data frames @@ -540,9 +1665,11 @@ which sites to evaluate model performance for, as follows: # Create a list with sites names to use in evaluating per-site metrics site_names = ['hcp', 'ixi'] - Setup output directories -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------ + +Step 8. +------- Save each brain region to its own text file (organized in separate directories) using the following commands, because for each response @@ -557,6 +1684,7 @@ variable, Y (e.g., brain region) we fit a separate normative model. X_train.to_csv('cov_tr.txt', sep = '\t', header=False, index = False) + .. code:: ipython3 y_train.to_csv('resp_tr.txt', sep = '\t', header=False, index = False) @@ -580,12 +1708,25 @@ variable, Y (e.g., brain region) we fit a separate normative model. .. code:: ipython3 + # Note: please change the path in the following to wdir (depending on whether you are running on colab or not) + ! for i in `cat /content/PCNtoolkit-demo/data/roi_dir_names`; do if [[ -e resp_tr_${i}.txt ]]; then cd ROI_models; mkdir ${i}; cd ../; cp resp_tr_${i}.txt ROI_models/${i}/resp_tr.txt; cp resp_te_${i}.txt ROI_models/${i}/resp_te.txt; cp cov_tr.txt ROI_models/${i}/cov_tr.txt; cp cov_te.txt ROI_models/${i}/cov_te.txt; fi; done + +.. parsed-literal:: + + mkdir: cannot create directory ‘lh_MeanThickness_thickness’: File exists + mkdir: cannot create directory ‘lh_bankssts_thickness’: File exists + mkdir: cannot create directory ‘lh_caudalanteriorcingulate_thickness’: File exists + mkdir: cannot create directory ‘lh_superiorfrontal_thickness’: File exists + mkdir: cannot create directory ‘rh_MeanThickness_thickness’: File exists + mkdir: cannot create directory ‘rh_superiorfrontal_thickness’: File exists + + .. code:: ipython3 # clean up files - ! rm resp_*.txt + ! rm resp_*.txt .. code:: ipython3 @@ -593,11 +1734,13 @@ variable, Y (e.g., brain region) we fit a separate normative model. ! rm cov_t*.txt Algorithm & Modeling -------------------------------- +==================== Basis expansion using B-Splines -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------- +Step 9. +~~~~~~~ Now, set up a B-spline basis set that allows us to perform nonlinear regression using a linear model, using the following commands. This @@ -615,29 +1758,29 @@ al `__. .. code:: ipython3 # set this path to wherever your ROI_models folder is located (where you copied all of the covariate & response text files to in Step 4) - data_dir = '/content/PCNtoolkit-demo/tutorials/BLR_protocol/ROI_models/' + data_dir = os.path.join(wdir,'tutorials','BLR_protocol','ROI_models') # Create a cubic B-spline basis (used for regression) xmin = 10#16 # xmin & xmax are the boundaries for ages of participants in the dataset xmax = 95#90 B = create_bspline_basis(xmin, xmax) - # create the basis expansion for the covariates for each of the - for roi in roi_ids: + # create the basis expansion for the covariates for each of the + for roi in roi_ids: print('Creating basis expansion for ROI:', roi) roi_dir = os.path.join(data_dir, roi) os.chdir(roi_dir) - # create output dir + # create output dir os.makedirs(os.path.join(roi_dir,'blr'), exist_ok=True) # load train & test covariate data matrices X_tr = np.loadtxt(os.path.join(roi_dir, 'cov_tr.txt')) X_te = np.loadtxt(os.path.join(roi_dir, 'cov_te.txt')) - # add intercept column + # add intercept column X_tr = np.concatenate((X_tr, np.ones((X_tr.shape[0],1))), axis=1) X_te = np.concatenate((X_te, np.ones((X_te.shape[0],1))), axis=1) np.savetxt(os.path.join(roi_dir, 'cov_int_tr.txt'), X_tr) np.savetxt(os.path.join(roi_dir, 'cov_int_te.txt'), X_te) - - # create Bspline basis set + + # create Bspline basis set Phi = np.array([B(i) for i in X_tr[:,0]]) Phis = np.array([B(i) for i in X_te[:,0]]) X_tr = np.concatenate((X_tr, Phi), axis=1) @@ -657,8 +1800,10 @@ al `__. Estimate normative model -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------ +Step 10. +~~~~~~~~ Set up a variable (``data_dir``) that specifies the path to the ROI directories that were created in Step 7. Initiate two empty pandas data @@ -674,6 +1819,8 @@ these data frames will be saved as individual csv files. blr_metrics = pd.DataFrame(columns = ['ROI', 'MSLL', 'EV', 'SMSE', 'RMSE', 'Rho']) blr_site_metrics = pd.DataFrame(columns = ['ROI', 'site', 'MSLL', 'EV', 'SMSE', 'RMSE', 'Rho']) +Step 11. +~~~~~~~~ Estimate the normative models using a for loop to iterate over brain regions. An important consideration is whether to re-scale or @@ -693,70 +1840,69 @@ arguments that are worthy of commenting on: :: - alg = 'blr': specifies we should use Bayesian Linear Regression. - - optimizer = 'powell': use Powell's derivative-free optimization method (faster in this case than L-BFGS) - - savemodel = False: do not write out the final estimated model to disk + - optimizer = 'powell': use Powell's derivative-free optimization method (faster in this case than L-BFGS) + - savemodel = False: do not write out the final estimated model to disk - saveoutput = False: return the outputs directly rather than writing them to disk - standardize = False: Do not standardize the covariates or response variables -.. warning:: - **CRITICAL STEP:** This code fragment will loop through each region of - interest in the ``roi_ids`` list (created in step 4) using Bayesian - Linear Regression and evaluate the model on the independent test set. In - principle, we could estimate the normative models on the whole data - matrix at once (e.g., with the response variables stored in a - ``n_subjects`` by ``n_brain_measures`` NumPy array or a text file - instead of saved out into separate directories). However, running the - models iteratively gives some extra flexibility in that it does not - require that the included subjects are the same for each of the brain - measures. +**CRITICAL STEP:** This code fragment will loop through each region of +interest in the ``roi_ids`` list (created in step 4) using Bayesian +Linear Regression and evaluate the model on the independent test set. In +principle, we could estimate the normative models on the whole data +matrix at once (e.g., with the response variables stored in a +``n_subjects`` by ``n_brain_measures`` NumPy array or a text file +instead of saved out into separate directories). However, running the +models iteratively gives some extra flexibility in that it does not +require that the included subjects are the same for each of the brain +measures. .. code:: ipython3 # Loop through ROIs - for roi in roi_ids: + for roi in roi_ids: print('Running ROI:', roi) roi_dir = os.path.join(data_dir, roi) os.chdir(roi_dir) - - # configure the covariates to use. Change *_bspline_* to *_int_* to + + # configure the covariates to use. Change *_bspline_* to *_int_* to cov_file_tr = os.path.join(roi_dir, 'cov_bspline_tr.txt') cov_file_te = os.path.join(roi_dir, 'cov_bspline_te.txt') - + # load train & test response files resp_file_tr = os.path.join(roi_dir, 'resp_tr.txt') - resp_file_te = os.path.join(roi_dir, 'resp_te.txt') - + resp_file_te = os.path.join(roi_dir, 'resp_te.txt') + # run a basic model - yhat_te, s2_te, nm, Z, metrics_te = estimate(cov_file_tr, - resp_file_tr, - testresp=resp_file_te, - testcov=cov_file_te, - alg = 'blr', - optimizer = 'powell', - savemodel = True, + yhat_te, s2_te, nm, Z, metrics_te = estimate(cov_file_tr, + resp_file_tr, + testresp=resp_file_te, + testcov=cov_file_te, + alg = 'blr', + optimizer = 'powell', + savemodel = True, saveoutput = False, standardize = False) # save metrics blr_metrics.loc[len(blr_metrics)] = [roi, metrics_te['MSLL'][0], metrics_te['EXPV'][0], metrics_te['SMSE'][0], metrics_te['RMSE'][0], metrics_te['Rho'][0]] - + # Compute metrics per site in test set, save to pandas df # load true test data X_te = np.loadtxt(cov_file_te) y_te = np.loadtxt(resp_file_te) y_te = y_te[:, np.newaxis] # make sure it is a 2-d array - + # load training data (required to compute the MSLL) y_tr = np.loadtxt(resp_file_tr) y_tr = y_tr[:, np.newaxis] - - for num, site in enumerate(sites): + + for num, site in enumerate(sites): y_mean_te_site = np.array([[np.mean(y_te[site])]]) y_var_te_site = np.array([[np.var(y_te[site])]]) yhat_mean_te_site = np.array([[np.mean(yhat_te[site])]]) yhat_var_te_site = np.array([[np.var(yhat_te[site])]]) - + metrics_te_site = evaluate(y_te[site], yhat_te[site], s2_te[site], y_mean_te_site, y_var_te_site) - + site_name = site_names[num] blr_site_metrics.loc[len(blr_site_metrics)] = [roi, site_names[num], metrics_te_site['MSLL'][0], metrics_te_site['EXPV'][0], metrics_te_site['SMSE'][0], metrics_te_site['RMSE'][0], metrics_te_site['Rho'][0]] @@ -764,40 +1910,54 @@ arguments that are worthy of commenting on: .. parsed-literal:: Running ROI: lh_MeanThickness_thickness + inscaler: None + outscaler: None Processing data in /content/PCNtoolkit-demo/tutorials/BLR_protocol/ROI_models/lh_MeanThickness_thickness/resp_tr.txt Estimating model 1 of 1 configuring BLR ( order 1 ) Using default hyperparameters - Optimization terminated successfully. - Current function value: -1162.792820 - Iterations: 2 - Function evaluations: 47 - Saving model meta-data... - Evaluating the model ... .. parsed-literal:: - /usr/local/lib/python3.7/dist-packages/pcntoolkit/model/bayesreg.py:187: LinAlgWarning: Ill-conditioned matrix (rcond=1.15485e-18): result may not be accurate. - invAXt = linalg.solve(self.A, X.T, check_finite=False) - /usr/local/lib/python3.7/dist-packages/pcntoolkit/model/bayesreg.py:187: LinAlgWarning: Ill-conditioned matrix (rcond=4.51813e-19): result may not be accurate. + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/bayesreg.py:196: LinAlgWarning: Ill-conditioned matrix (rcond=1.15485e-18): result may not be accurate. invAXt = linalg.solve(self.A, X.T, check_finite=False) .. parsed-literal:: + Optimization terminated successfully. + Current function value: -1162.792820 + Iterations: 2 + Function evaluations: 43 + Saving model meta-data... + Evaluating the model ... Running ROI: rh_MeanThickness_thickness + inscaler: None + outscaler: None Processing data in /content/PCNtoolkit-demo/tutorials/BLR_protocol/ROI_models/rh_MeanThickness_thickness/resp_tr.txt Estimating model 1 of 1 configuring BLR ( order 1 ) Using default hyperparameters + + +.. parsed-literal:: + + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/bayesreg.py:196: LinAlgWarning: Ill-conditioned matrix (rcond=4.51813e-19): result may not be accurate. + invAXt = linalg.solve(self.A, X.T, check_finite=False) + + +.. parsed-literal:: + Optimization terminated successfully. Current function value: -1187.621858 Iterations: 2 - Function evaluations: 47 + Function evaluations: 43 Saving model meta-data... Evaluating the model ... Running ROI: lh_bankssts_thickness + inscaler: None + outscaler: None Processing data in /content/PCNtoolkit-demo/tutorials/BLR_protocol/ROI_models/lh_bankssts_thickness/resp_tr.txt Estimating model 1 of 1 configuring BLR ( order 1 ) @@ -805,10 +1965,12 @@ arguments that are worthy of commenting on: Optimization terminated successfully. Current function value: -578.945257 Iterations: 2 - Function evaluations: 46 + Function evaluations: 42 Saving model meta-data... Evaluating the model ... Running ROI: lh_caudalanteriorcingulate_thickness + inscaler: None + outscaler: None Processing data in /content/PCNtoolkit-demo/tutorials/BLR_protocol/ROI_models/lh_caudalanteriorcingulate_thickness/resp_tr.txt Estimating model 1 of 1 configuring BLR ( order 1 ) @@ -816,10 +1978,12 @@ arguments that are worthy of commenting on: Optimization terminated successfully. Current function value: -235.509099 Iterations: 3 - Function evaluations: 75 + Function evaluations: 69 Saving model meta-data... Evaluating the model ... Running ROI: lh_superiorfrontal_thickness + inscaler: None + outscaler: None Processing data in /content/PCNtoolkit-demo/tutorials/BLR_protocol/ROI_models/lh_superiorfrontal_thickness/resp_tr.txt Estimating model 1 of 1 configuring BLR ( order 1 ) @@ -827,10 +1991,12 @@ arguments that are worthy of commenting on: Optimization terminated successfully. Current function value: -716.547377 Iterations: 3 - Function evaluations: 91 + Function evaluations: 84 Saving model meta-data... Evaluating the model ... Running ROI: rh_superiorfrontal_thickness + inscaler: None + outscaler: None Processing data in /content/PCNtoolkit-demo/tutorials/BLR_protocol/ROI_models/rh_superiorfrontal_thickness/resp_tr.txt Estimating model 1 of 1 configuring BLR ( order 1 ) @@ -838,17 +2004,19 @@ arguments that are worthy of commenting on: Optimization terminated successfully. Current function value: -730.639309 Iterations: 2 - Function evaluations: 45 + Function evaluations: 41 Saving model meta-data... Evaluating the model ... Evaluation & Interpretation ----------------------------------------- +=========================== Describe the normative model performance -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +---------------------------------------- +Step 12. +~~~~~~~~ In step 11, when we looped over each region of interest in the ``roi_ids`` list (created in step 4) and evaluated the normative model @@ -917,7 +2085,7 @@ can organize them into a single file, and merge the deviation scores into the original data file. Visualize normative model outputs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------- Figure 4A viz ~~~~~~~~~~~~~ @@ -937,29 +2105,21 @@ Figure 4A viz plt.figure(dpi=380) fig, axes = joypy.joyplot(blr_site_metrics, column=['EV'], overlap=2.5, by="site", ylim='own', fill=True, figsize=(8,8) , legend=False, xlabels=True, ylabels=True, colormap=lambda x: color_gradient(x, start=(.08, .45, .8),stop=(.8, .34, .44)) - , alpha=0.6, linewidth=.5, linecolor='w', fade=True) + , alpha=0.6, linewidth=.5, linecolor='w', fade=True); plt.title('Test Set Explained Variance', fontsize=18, color='black', alpha=1) plt.xlabel('Explained Variance', fontsize=14, color='black', alpha=1) plt.ylabel('Site', fontsize=14, color='black', alpha=1) - plt.show - + plt.show() .. parsed-literal:: - +
                            - -.. parsed-literal:: - -
                            - - - -.. image:: BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_93_2.png +.. image:: BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_97_1.png The code used to create the visualizations shown in Figure 4 panels B-F, @@ -967,7 +2127,8 @@ can be found in this `notebook `__. Post-Hoc analysis ideas -~~~~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- + The code for running SVM classification and classical case vs. control t-testing on the outputs of normative modeling can be found in this diff --git a/doc/source/pages/BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_15_1.png b/doc/source/pages/BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_15_1.png deleted file mode 100644 index f641b263..00000000 Binary files a/doc/source/pages/BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_15_1.png and /dev/null differ diff --git a/doc/source/pages/BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_16_0.png b/doc/source/pages/BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_16_0.png new file mode 100644 index 00000000..34ac5874 Binary files /dev/null and b/doc/source/pages/BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_16_0.png differ diff --git a/doc/source/pages/BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_93_2.png b/doc/source/pages/BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_93_2.png deleted file mode 100644 index f8752a5e..00000000 Binary files a/doc/source/pages/BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_93_2.png and /dev/null differ diff --git a/doc/source/pages/BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_97_1.png b/doc/source/pages/BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_97_1.png new file mode 100644 index 00000000..adb3c4ae Binary files /dev/null and b/doc/source/pages/BLR_normativemodel_protocol_files/BLR_normativemodel_protocol_97_1.png differ diff --git a/doc/source/pages/FAQs.rst b/doc/source/pages/FAQs.rst index a8879d36..efe98bd0 100644 --- a/doc/source/pages/FAQs.rst +++ b/doc/source/pages/FAQs.rst @@ -5,7 +5,7 @@ Frequently Asked Questions Most of the questions we recieve are about interpretation of normative modeling outputs. -The PCNtoolkit develoers have written a protocol for how to run a normative modeling analysis which should be helpful to you if you are just getting started. +The PCNtoolkit developers have written a protocol for how to run a normative modeling analysis which should be helpful to you if you are just getting started. Rutherford, S., Kia, S. M., Wolfers, T., ... Beckmann, C. F., & Marquand, A. F. (2022). The Normative Modeling Framework for Computational Psychiatry. Nature Protocols. https://www.nature.com/articles/s41596-022-00696-5. diff --git a/doc/source/pages/HBR_NormativeModel_FCONdata_Tutorial.ipynb b/doc/source/pages/HBR_NormativeModel_FCONdata_Tutorial.ipynb new file mode 100644 index 00000000..009e16b6 --- /dev/null +++ b/doc/source/pages/HBR_NormativeModel_FCONdata_Tutorial.ipynb @@ -0,0 +1,628 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4GrH5Af4381L" + }, + "source": [ + "# [Predictive Clinical Neuroscience Toolkit](https://github.com/amarquand/PCNtoolkit)\n", + "# Hierarchical Bayesian Regression Normative Modelling and Transfer onto unseen site.\n", + "\n", + "This notebook will go through basic data preparation (training and testing set, [see Saige's tutorial](https://github.com/predictive-clinical-neuroscience/PCNtoolkit-demo/blob/main/tutorials/BLR_protocol/BLR_normativemodel_protocol.ipynb) on Normative Modelling for more detail), the actual training of the models, and will finally describe how to transfer the trained models onto unseen sites." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fkYKxCSo381O" + }, + "source": [ + "### Created by [Saige Rutherford](https://twitter.com/being_saige)\n", + "### adapted/edited by Andre Marquand and Pierre Berthet" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ptugU56E381P" + }, + "source": [ + "
                            \n", + "\n", + "
                            " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yjRPC3Qq381P" + }, + "source": [ + "## Step 0: Install necessary libraries & grab data files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "lVsYLSIeWiN9" + }, + "outputs": [], + "source": [ + "!pip install pcntoolkit\n", + "!pip install nutpie" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XnQzHXgT381Q" + }, + "source": [ + "For this tutorial we will use data from the [Functional Connectom Project FCON1000](http://fcon_1000.projects.nitrc.org/) to create a multi-site dataset.\n", + "\n", + "The dataset contains some cortical measures (eg thickness), processed by Freesurfer 6.0, and some covariates (eg age, site, gender).\n", + "\n", + "First we import the required package, and create a working directory." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "CBjOrufm381R" + }, + "outputs": [], + "source": [ + "import os\n", + "import pandas as pd\n", + "import pcntoolkit as ptk\n", + "import numpy as np\n", + "import pickle\n", + "from matplotlib import pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "YklC6BFp381S" + }, + "outputs": [], + "source": [ + "processing_dir = \"HBR_demo\" # replace with desired working directory\n", + "if not os.path.isdir(processing_dir):\n", + " os.makedirs(processing_dir)\n", + "os.chdir(processing_dir)\n", + "processing_dir = os.getcwd()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zcNvTEhT381S" + }, + "source": [ + "#### Overview\n", + "Here we get the FCON dataset, remove the ICBM site for later transfer, assign some site id to the different scanner sites and print an overview of the left hemisphere mean raw cortical thickness as a function of age, color coded by the various sites:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "XibIZLaT381T", + "scrolled": true + }, + "outputs": [], + "source": [ + "fcon = pd.read_csv('https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/main/data/fcon1000.csv')\n", + "\n", + "# extract the ICBM site for transfer\n", + "icbm = fcon.loc[fcon['site'] == 'ICBM']\n", + "icbm['sitenum'] = 0\n", + "\n", + "# remove from the training set (also Pittsburgh because it only has 3 samples)\n", + "fcon = fcon.loc[fcon['site'] != 'ICBM']\n", + "fcon = fcon.loc[fcon['site'] != 'Pittsburgh']\n", + "\n", + "sites = fcon['site'].unique()\n", + "fcon['sitenum'] = 0\n", + "\n", + "f, ax = plt.subplots(figsize=(12, 12))\n", + "\n", + "for i,s in enumerate(sites):\n", + " idx = fcon['site'] == s\n", + " fcon['sitenum'].loc[idx] = i\n", + "\n", + " print('site',s, sum(idx))\n", + " ax.scatter(fcon['age'].loc[idx], fcon['lh_MeanThickness_thickness'].loc[idx])\n", + "\n", + "ax.legend(sites)\n", + "ax.set_ylabel('LH mean cortical thickness [mm]')\n", + "ax.set_xlabel('age')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "R41KrcwG381U" + }, + "source": [ + "## Step 1: Prepare training and testing sets" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FNF8I4_S381U" + }, + "source": [ + "Then we randomly split half of the samples (participants) to be either in the training or in the testing samples. We do this for the remaing FCON dataset and for the ICBM data. The transfer function will also require a training and a test sample.\n", + "\n", + "The numbers of samples per sites used for training and for testing are then displayed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "txJK1fHt381V" + }, + "outputs": [], + "source": [ + "tr = np.random.uniform(size=fcon.shape[0]) > 0.5\n", + "te = ~tr\n", + "\n", + "fcon_tr = fcon.loc[tr]\n", + "fcon_te = fcon.loc[te]\n", + "\n", + "tr = np.random.uniform(size=icbm.shape[0]) > 0.5\n", + "te = ~tr\n", + "\n", + "icbm_tr = icbm.loc[tr]\n", + "icbm_te = icbm.loc[te]\n", + "\n", + "print('sample size check')\n", + "for i,s in enumerate(sites):\n", + " idx = fcon_tr['site'] == s\n", + " idxte = fcon_te['site'] == s\n", + " print(i,s, sum(idx), sum(idxte))\n", + "\n", + "fcon_tr.to_csv(processing_dir + '/fcon1000_tr.csv')\n", + "fcon_te.to_csv(processing_dir + '/fcon1000_te.csv')\n", + "icbm_tr.to_csv(processing_dir + '/fcon1000_icbm_tr.csv')\n", + "icbm_te.to_csv(processing_dir + '/fcon1000_icbm_te.csv')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QlTN6JAW381V" + }, + "source": [ + "Otherwise you can just load these pre defined subsets:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "uXBxuLK7381V" + }, + "outputs": [], + "source": [ + "# Optional\n", + "#fcon_tr = pd.read_csv('https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/main/data/fcon1000_tr.csv')\n", + "#fcon_te = pd.read_csv('https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/main/data/fcon1000_te.csv')\n", + "#icbm_tr = pd.read_csv('https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/main/data/fcon1000_icbm_tr.csv')\n", + "#icbm_te = pd.read_csv('https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/main/data/fcon1000_icbm_te.csv')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qwp5XfII381W" + }, + "source": [ + "## Step 2: Configure HBR inputs: covariates, measures and batch effects" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "NnIR5Sqq381W" + }, + "source": [ + "We will here only use the mean cortical thickness for the Right and Left hemisphere: two idps.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "4_EqQFOe381W" + }, + "outputs": [], + "source": [ + "idps = ['rh_MeanThickness_thickness','lh_MeanThickness_thickness']" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wLu-CoUl381W" + }, + "source": [ + "As input to the model, we need covariates (used to describe predictable source of variability (fixed effects), here 'age'), measures (here cortical thickness on two idps), and batch effects (random source of variability, here 'scanner site' and 'sex').\n", + "\n", + "`X` corresponds to the covariate(s)\n", + "\n", + "`Y` to the measure(s)\n", + "\n", + "`batch_effects` to the random effects\n", + "\n", + "We need these values both for the training (`_train`) and for the testing set (`_test`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "W2xf-Zc_381X" + }, + "outputs": [], + "source": [ + "X_train = (fcon_tr['age']/100).to_numpy(dtype=float)\n", + "Y_train = fcon_tr[idps].to_numpy(dtype=float)\n", + "\n", + "# configure batch effects for site and sex\n", + "#batch_effects_train = fcon_tr[['sitenum','sex']].to_numpy(dtype=int)\n", + "\n", + "# or only site\n", + "batch_effects_train = fcon_tr[['sitenum']].to_numpy(dtype=int)\n", + "\n", + "with open('X_train.pkl', 'wb') as file:\n", + " pickle.dump(pd.DataFrame(X_train), file)\n", + "with open('Y_train.pkl', 'wb') as file:\n", + " pickle.dump(pd.DataFrame(Y_train), file)\n", + "with open('trbefile.pkl', 'wb') as file:\n", + " pickle.dump(pd.DataFrame(batch_effects_train), file)\n", + "\n", + "\n", + "X_test = (fcon_te['age']/100).to_numpy(dtype=float)\n", + "Y_test = fcon_te[idps].to_numpy(dtype=float)\n", + "#batch_effects_test = fcon_te[['sitenum','sex']].to_numpy(dtype=int)\n", + "batch_effects_test = fcon_te[['sitenum']].to_numpy(dtype=int)\n", + "\n", + "with open('X_test.pkl', 'wb') as file:\n", + " pickle.dump(pd.DataFrame(X_test), file)\n", + "with open('Y_test.pkl', 'wb') as file:\n", + " pickle.dump(pd.DataFrame(Y_test), file)\n", + "with open('tsbefile.pkl', 'wb') as file:\n", + " pickle.dump(pd.DataFrame(batch_effects_test), file)\n", + "\n", + "# a simple function to quickly load pickle files\n", + "def ldpkl(filename: str):\n", + " with open(filename, 'rb') as f:\n", + " return pickle.load(f)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1poibEqNWiOA" + }, + "outputs": [], + "source": [ + "batch_effects_test" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6WYLCEmV381X" + }, + "source": [ + "## Step 3: Files and Folders grooming" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "I18buNGh381X" + }, + "outputs": [], + "source": [ + "respfile = os.path.join(processing_dir, 'Y_train.pkl') # measurements (eg cortical thickness) of the training samples (columns: the various features/ROIs, rows: observations or subjects)\n", + "covfile = os.path.join(processing_dir, 'X_train.pkl') # covariates (eg age) the training samples (columns: covariates, rows: observations or subjects)\n", + "\n", + "testrespfile_path = os.path.join(processing_dir, 'Y_test.pkl') # measurements for the testing samples\n", + "testcovfile_path = os.path.join(processing_dir, 'X_test.pkl') # covariate file for the testing samples\n", + "\n", + "trbefile = os.path.join(processing_dir, 'trbefile.pkl') # training batch effects file (eg scanner_id, gender) (columns: the various batch effects, rows: observations or subjects)\n", + "tsbefile = os.path.join(processing_dir, 'tsbefile.pkl') # testing batch effects file\n", + "\n", + "output_path = os.path.join(processing_dir, 'Models/') # output path, where the models will be written\n", + "log_dir = os.path.join(processing_dir, 'log/') #\n", + "if not os.path.isdir(output_path):\n", + " os.mkdir(output_path)\n", + "if not os.path.isdir(log_dir):\n", + " os.mkdir(log_dir)\n", + "\n", + "outputsuffix = '_estimate' # a string to name the output files, of use only to you, so adapt it for your needs." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wykFbsV_381Y" + }, + "source": [ + "## Step 4: Estimating the models" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "HvcGW4vK381Y" + }, + "source": [ + "Now we have everything ready to estimate the normative models. The `estimate` function only needs the training and testing sets, each divided in three datasets: covariates, measures and batch effects. We obviously specify `alg=hbr` to use the hierarchical bayesian regression method, well suited for the multi sites datasets. The remaining arguments are basic data management: where the models, logs, and output files will be written and how they will be named." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "fEYt38W9381Y" + }, + "outputs": [], + "source": [ + "ptk.normative.estimate(covfile=covfile,\n", + " respfile=respfile,\n", + " tsbefile=tsbefile,\n", + " trbefile=trbefile,\n", + " inscaler='standardize',\n", + " outscaler='standardize',\n", + " linear_mu='True',\n", + " random_intercept_mu='True',\n", + " centered_intercept_mu='True',\n", + " alg='hbr',\n", + " log_path=log_dir,\n", + " binary=True,\n", + " output_path=output_path,\n", + " testcov= testcovfile_path,\n", + " testresp = testrespfile_path,\n", + " outputsuffix=outputsuffix,\n", + " savemodel=True,\n", + " nuts_sampler='nutpie')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cu4Umis3381Z" + }, + "source": [ + "Here some analyses can be done, there are also some error metrics that could be of interest. This is covered in step 6 and in [Saige's tutorial](https://github.com/predictive-clinical-neuroscience/PCNtoolkit-demo/blob/main/tutorials/BLR_protocol/BLR_normativemodel_protocol.ipynb) on Normative Modelling." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "FyTrHWij381Z" + }, + "source": [ + "## Step 5: Transfering the models to unseen sites" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZRmYyQry381Z" + }, + "source": [ + "Similarly to what was done before for the FCON data, we also need to prepare the ICBM specific data, in order to run the transfer function: training and testing set of covariates, measures and batch effects:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ujwfzth4381Z" + }, + "outputs": [], + "source": [ + "X_adapt = (icbm_tr['age']/100).to_numpy(dtype=float)\n", + "Y_adapt = icbm_tr[idps].to_numpy(dtype=float)\n", + "#batch_effects_adapt = icbm_tr[['sitenum','sex']].to_numpy(dtype=int)\n", + "batch_effects_adapt = icbm_tr[['sitenum']].to_numpy(dtype=int)\n", + "\n", + "with open('X_adaptation.pkl', 'wb') as file:\n", + " pickle.dump(pd.DataFrame(X_adapt), file)\n", + "with open('Y_adaptation.pkl', 'wb') as file:\n", + " pickle.dump(pd.DataFrame(Y_adapt), file)\n", + "with open('adbefile.pkl', 'wb') as file:\n", + " pickle.dump(pd.DataFrame(batch_effects_adapt), file)\n", + "\n", + "# Test data (new dataset)\n", + "X_test_txfr = (icbm_te['age']/100).to_numpy(dtype=float)\n", + "Y_test_txfr = icbm_te[idps].to_numpy(dtype=float)\n", + "#batch_effects_test_txfr = icbm_te[['sitenum','sex']].to_numpy(dtype=int)\n", + "batch_effects_test_txfr = icbm_te[['sitenum']].to_numpy(dtype=int)\n", + "\n", + "with open('X_test_txfr.pkl', 'wb') as file:\n", + " pickle.dump(pd.DataFrame(X_test_txfr), file)\n", + "with open('Y_test_txfr.pkl', 'wb') as file:\n", + " pickle.dump(pd.DataFrame(Y_test_txfr), file)\n", + "with open('txbefile.pkl', 'wb') as file:\n", + " pickle.dump(pd.DataFrame(batch_effects_test_txfr), file)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "wqzmwQ0B381Z" + }, + "outputs": [], + "source": [ + "respfile = os.path.join(processing_dir, 'Y_adaptation.pkl')\n", + "covfile = os.path.join(processing_dir, 'X_adaptation.pkl')\n", + "testrespfile_path = os.path.join(processing_dir, 'Y_test_txfr.pkl')\n", + "testcovfile_path = os.path.join(processing_dir, 'X_test_txfr.pkl')\n", + "trbefile = os.path.join(processing_dir, 'adbefile.pkl')\n", + "tsbefile = os.path.join(processing_dir, 'txbefile.pkl')\n", + "\n", + "log_dir = os.path.join(processing_dir, 'log_transfer/')\n", + "output_path = os.path.join(processing_dir, 'Transfer/')\n", + "model_path = os.path.join(processing_dir, 'Models/') # path to the previously trained models\n", + "outputsuffix = '_transfer' # suffix added to the output files from the transfer function" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "VlP1kyjj381a" + }, + "source": [ + "Here, the difference is that the transfer function needs a model path, which points to the models we just trained, and new site data (training and testing). That is basically the only difference." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "hwpmdprx381a" + }, + "outputs": [], + "source": [ + "yhat, s2, z_scores = ptk.normative.transfer(covfile=covfile,\n", + " respfile=respfile,\n", + " tsbefile=tsbefile,\n", + " trbefile=trbefile,\n", + " inscaler='standardize',\n", + " outscaler='standardize',\n", + " linear_mu='True',\n", + " random_intercept_mu='True',\n", + " centered_intercept_mu='True',\n", + " model_path = model_path,\n", + " alg='hbr',\n", + " log_path=log_dir,\n", + " binary=True,\n", + " output_path=output_path,\n", + " testcov= testcovfile_path,\n", + " testresp = testrespfile_path,\n", + " outputsuffix=outputsuffix,\n", + " savemodel=True,\n", + " nuts_sampler='nutpie')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "nke1zijEWiOB" + }, + "outputs": [], + "source": [ + "output_path" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WeGlgv9kWiOB" + }, + "outputs": [], + "source": [ + "EV = pd.read_pickle('EXPV_estimate.pkl')\n", + "print(EV)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "QBtsSlDG381a" + }, + "source": [ + "And that is it, you now have models that benefited from prior knowledge about different scanner sites to learn on unseen sites." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "INPI9Tnt381a" + }, + "source": [ + "## Step 6: Interpreting model performance" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eMCWLa3b381a" + }, + "source": [ + "Output evaluation metrics definitions:\n", + "* yhat - predictive mean\n", + "* ys2 - predictive variance\n", + "* nm - normative model\n", + "* Z - deviance scores\n", + "* Rho - Pearson correlation between true and predicted responses\n", + "* pRho - parametric p-value for this correlation\n", + "* RMSE - root mean squared error between true/predicted responses\n", + "* SMSE - standardised mean squared error\n", + "* EV - explained variance\n", + "* MSLL - mean standardized log loss\n", + " * See page 23 in http://www.gaussianprocess.org/gpml/chapters/RW2.pdf" + ] + } + ], + "metadata": { + "colab": { + "name": "HBR_NormativeModel_FCONdata_Tutorial.ipynb", + "provenance": [], + "include_colab_link": true + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.9" + }, + "vscode": { + "interpreter": { + "hash": "40d3a090f54c6569ab1632332b64b2c03c39dcf918b08424e98f38b5ae0af88f" + } + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/doc/source/pages/HBR_NormativeModel_FCONdata_Tutorial.rst b/doc/source/pages/HBR_NormativeModel_FCONdata_Tutorial.rst index d4933e38..b4fa2dfb 100644 --- a/doc/source/pages/HBR_NormativeModel_FCONdata_Tutorial.rst +++ b/doc/source/pages/HBR_NormativeModel_FCONdata_Tutorial.rst @@ -1,31 +1,29 @@ -.. title:: HBR tutorial +`Predictive Clinical Neuroscience Toolkit `__ +====================================================================================== -Hierarchical Bayesian Regression +Hierarchical Bayesian Regression Normative Modelling and Transfer onto unseen site. =================================================================================== This notebook will go through basic data preparation (training and -testing set, `see Saige's +testing set, `see Saige’s tutorial `__ on Normative Modelling for more detail), the actual training of the models, and will finally describe how to transfer the trained models onto unseen sites. Created by `Saige Rutherford `__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Adapted/edited by Andre Marquand and Pierre Berthet. - -.. image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/predictive-clinical-neuroscience/PCNtoolkit-demo/blob/main/tutorials/HBR_FCON/HBR_NormativeModel_FCONdata_Tutorial.ipynb - - +adapted/edited by Andre Marquand and Pierre Berthet +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Step 0: Install necessary libraries & grab data files ----------------------------------------------------- .. code:: ipython3 - ! pip install pcntoolkit==0.26 - + !pip install pcntoolkit + !pip install nutpie For this tutorial we will use data from the `Functional Connectom Project FCON1000 `__ to create a @@ -47,7 +45,7 @@ First we import the required package, and create a working directory. .. code:: ipython3 - processing_dir = "HBR_demo/" # replace with a path to your working directory + processing_dir = "HBR_demo" # replace with desired working directory if not os.path.isdir(processing_dir): os.makedirs(processing_dir) os.chdir(processing_dir) @@ -63,27 +61,33 @@ color coded by the various sites: .. code:: ipython3 - fcon = pd.read_csv('https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/main/data/fcon1000.csv') + fcon = pd.read_csv( + "https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/main/data/fcon1000.csv" + ) + + # extract the ICBM site for transfer + icbm = fcon.loc[fcon["site"] == "ICBM"] + icbm["sitenum"] = 0 - icbm = fcon.loc[fcon['site'] == 'ICBM'] - icbm['sitenum'] = 0 - fcon = fcon.loc[fcon['site'] != 'ICBM'] + # remove from the training set (also Pittsburgh because it only has 3 samples) + fcon = fcon.loc[fcon["site"] != "ICBM"] + fcon = fcon.loc[fcon["site"] != "Pittsburgh"] - sites = fcon['site'].unique() - fcon['sitenum'] = 0 + sites = fcon["site"].unique() + fcon["sitenum"] = 0 f, ax = plt.subplots(figsize=(12, 12)) - for i,s in enumerate(sites): - idx = fcon['site'] == s - fcon['sitenum'].loc[idx] = i + for i, s in enumerate(sites): + idx = fcon["site"] == s + fcon["sitenum"].loc[idx] = i - print('site',s, sum(idx)) - ax.scatter(fcon['age'].loc[idx], fcon['lh_MeanThickness_thickness'].loc[idx]) + print("site", s, sum(idx)) + ax.scatter(fcon["age"].loc[idx], fcon["lh_MeanThickness_thickness"].loc[idx]) ax.legend(sites) - ax.set_ylabel('LH mean cortical thickness [mm]') - ax.set_xlabel('age') + ax.set_ylabel("LH mean cortical thickness [mm]") + ax.set_xlabel("age") Step 1: Prepare training and testing sets @@ -111,16 +115,16 @@ then displayed. icbm_tr = icbm.loc[tr] icbm_te = icbm.loc[te] - print('sample size check') - for i,s in enumerate(sites): - idx = fcon_tr['site'] == s - idxte = fcon_te['site'] == s - print(i,s, sum(idx), sum(idxte)) + print("sample size check") + for i, s in enumerate(sites): + idx = fcon_tr["site"] == s + idxte = fcon_te["site"] == s + print(i, s, sum(idx), sum(idxte)) - fcon_tr.to_csv(processing_dir + '/fcon1000_tr.csv') - fcon_te.to_csv(processing_dir + '/fcon1000_te.csv') - icbm_tr.to_csv(processing_dir + '/fcon1000_icbm_tr.csv') - icbm_te.to_csv(processing_dir + '/fcon1000_icbm_te.csv') + fcon_tr.to_csv(processing_dir + "/fcon1000_tr.csv") + fcon_te.to_csv(processing_dir + "/fcon1000_te.csv") + icbm_tr.to_csv(processing_dir + "/fcon1000_icbm_tr.csv") + icbm_te.to_csv(processing_dir + "/fcon1000_icbm_te.csv") Otherwise you can just load these pre defined subsets: @@ -140,7 +144,7 @@ hemisphere: two idps. .. code:: ipython3 - idps = ['rh_MeanThickness_thickness','lh_MeanThickness_thickness'] + idps = ["rh_MeanThickness_thickness", "lh_MeanThickness_thickness"] As input to the model, we need covariates (used to describe predictable source of variability (fixed effects), here ‘age’), measures (here @@ -158,56 +162,79 @@ testing set (``_test``). .. code:: ipython3 - X_train = (fcon_tr['age']/100).to_numpy(dtype=float) + X_train = (fcon_tr["age"] / 100).to_numpy(dtype=float) Y_train = fcon_tr[idps].to_numpy(dtype=float) - batch_effects_train = fcon_tr[['sitenum','sex']].to_numpy(dtype=int) - with open('X_train.pkl', 'wb') as file: + # configure batch effects for site and sex + # batch_effects_train = fcon_tr[['sitenum','sex']].to_numpy(dtype=int) + + # or only site + batch_effects_train = fcon_tr[["sitenum"]].to_numpy(dtype=int) + + with open("X_train.pkl", "wb") as file: pickle.dump(pd.DataFrame(X_train), file) - with open('Y_train.pkl', 'wb') as file: + with open("Y_train.pkl", "wb") as file: pickle.dump(pd.DataFrame(Y_train), file) - with open('trbefile.pkl', 'wb') as file: + with open("trbefile.pkl", "wb") as file: pickle.dump(pd.DataFrame(batch_effects_train), file) - X_test = (fcon_te['age']/100).to_numpy(dtype=float) + X_test = (fcon_te["age"] / 100).to_numpy(dtype=float) Y_test = fcon_te[idps].to_numpy(dtype=float) - batch_effects_test = fcon_te[['sitenum','sex']].to_numpy(dtype=int) + # batch_effects_test = fcon_te[['sitenum','sex']].to_numpy(dtype=int) + batch_effects_test = fcon_te[["sitenum"]].to_numpy(dtype=int) - with open('X_test.pkl', 'wb') as file: + with open("X_test.pkl", "wb") as file: pickle.dump(pd.DataFrame(X_test), file) - with open('Y_test.pkl', 'wb') as file: + with open("Y_test.pkl", "wb") as file: pickle.dump(pd.DataFrame(Y_test), file) - with open('tsbefile.pkl', 'wb') as file: + with open("tsbefile.pkl", "wb") as file: pickle.dump(pd.DataFrame(batch_effects_test), file) + # a simple function to quickly load pickle files def ldpkl(filename: str): - with open(filename, 'rb') as f: + with open(filename, "rb") as f: return pickle.load(f) +.. code:: ipython3 + + batch_effects_test + Step 3: Files and Folders grooming ---------------------------------- .. code:: ipython3 - respfile = os.path.join(processing_dir, 'Y_train.pkl') # measurements (eg cortical thickness) of the training samples (columns: the various features/ROIs, rows: observations or subjects) - covfile = os.path.join(processing_dir, 'X_train.pkl') # covariates (eg age) the training samples (columns: covariates, rows: observations or subjects) + respfile = os.path.join( + processing_dir, "Y_train.pkl" + ) # measurements (eg cortical thickness) of the training samples (columns: the various features/ROIs, rows: observations or subjects) + covfile = os.path.join( + processing_dir, "X_train.pkl" + ) # covariates (eg age) the training samples (columns: covariates, rows: observations or subjects) - testrespfile_path = os.path.join(processing_dir, 'Y_test.pkl') # measurements for the testing samples - testcovfile_path = os.path.join(processing_dir, 'X_test.pkl') # covariate file for the testing samples + testrespfile_path = os.path.join( + processing_dir, "Y_test.pkl" + ) # measurements for the testing samples + testcovfile_path = os.path.join( + processing_dir, "X_test.pkl" + ) # covariate file for the testing samples - trbefile = os.path.join(processing_dir, 'trbefile.pkl') # training batch effects file (eg scanner_id, gender) (columns: the various batch effects, rows: observations or subjects) - tsbefile = os.path.join(processing_dir, 'tsbefile.pkl') # testing batch effects file + trbefile = os.path.join( + processing_dir, "trbefile.pkl" + ) # training batch effects file (eg scanner_id, gender) (columns: the various batch effects, rows: observations or subjects) + tsbefile = os.path.join(processing_dir, "tsbefile.pkl") # testing batch effects file - output_path = os.path.join(processing_dir, 'Models/') # output path, where the models will be written - log_dir = os.path.join(processing_dir, 'log/') # + output_path = os.path.join( + processing_dir, "Models/" + ) # output path, where the models will be written + 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' # a string to name the output files, of use only to you, so adapt it for your needs. + outputsuffix = "_estimate" # a string to name the output files, of use only to you, so adapt it for your needs. Step 4: Estimating the models ----------------------------- @@ -222,16 +249,26 @@ and output files will be written and how they will be named. .. code:: ipython3 - ptk.normative.estimate(covfile=covfile, - respfile=respfile, - tsbefile=tsbefile, - trbefile=trbefile, - alg='hbr', - log_path=log_dir, - binary=True, - output_path=output_path, testcov= testcovfile_path, - testresp = testrespfile_path, - outputsuffix=outputsuffix, savemodel=True) + ptk.normative.estimate( + covfile=covfile, + respfile=respfile, + tsbefile=tsbefile, + trbefile=trbefile, + inscaler="standardize", + outscaler="standardize", + linear_mu="True", + random_intercept_mu="True", + centered_intercept_mu="True", + alg="hbr", + log_path=log_dir, + binary=True, + output_path=output_path, + testcov=testcovfile_path, + testresp=testrespfile_path, + outputsuffix=outputsuffix, + savemodel=True, + nuts_sampler="nutpie", + ) Here some analyses can be done, there are also some error metrics that could be of interest. This is covered in step 6 and in `Saige’s @@ -247,42 +284,49 @@ training and testing set of covariates, measures and batch effects: .. code:: ipython3 - X_adapt = (icbm_tr['age']/100).to_numpy(dtype=float) + X_adapt = (icbm_tr["age"] / 100).to_numpy(dtype=float) Y_adapt = icbm_tr[idps].to_numpy(dtype=float) - batch_effects_adapt = icbm_tr[['sitenum','sex']].to_numpy(dtype=int) + # batch_effects_adapt = icbm_tr[['sitenum','sex']].to_numpy(dtype=int) + batch_effects_adapt = icbm_tr[["sitenum"]].to_numpy(dtype=int) - with open('X_adaptation.pkl', 'wb') as file: + with open("X_adaptation.pkl", "wb") as file: pickle.dump(pd.DataFrame(X_adapt), file) - with open('Y_adaptation.pkl', 'wb') as file: + with open("Y_adaptation.pkl", "wb") as file: pickle.dump(pd.DataFrame(Y_adapt), file) - with open('adbefile.pkl', 'wb') as file: + with open("adbefile.pkl", "wb") as file: pickle.dump(pd.DataFrame(batch_effects_adapt), file) # Test data (new dataset) - X_test_txfr = (icbm_te['age']/100).to_numpy(dtype=float) + X_test_txfr = (icbm_te["age"] / 100).to_numpy(dtype=float) Y_test_txfr = icbm_te[idps].to_numpy(dtype=float) - batch_effects_test_txfr = icbm_te[['sitenum','sex']].to_numpy(dtype=int) + # batch_effects_test_txfr = icbm_te[['sitenum','sex']].to_numpy(dtype=int) + batch_effects_test_txfr = icbm_te[["sitenum"]].to_numpy(dtype=int) - with open('X_test_txfr.pkl', 'wb') as file: + with open("X_test_txfr.pkl", "wb") as file: pickle.dump(pd.DataFrame(X_test_txfr), file) - with open('Y_test_txfr.pkl', 'wb') as file: + with open("Y_test_txfr.pkl", "wb") as file: pickle.dump(pd.DataFrame(Y_test_txfr), file) - with open('txbefile.pkl', 'wb') as file: + with open("txbefile.pkl", "wb") as file: pickle.dump(pd.DataFrame(batch_effects_test_txfr), file) + .. code:: ipython3 - respfile = os.path.join(processing_dir, 'Y_adaptation.pkl') - covfile = os.path.join(processing_dir, 'X_adaptation.pkl') - testrespfile_path = os.path.join(processing_dir, 'Y_test_txfr.pkl') - testcovfile_path = os.path.join(processing_dir, 'X_test_txfr.pkl') - trbefile = os.path.join(processing_dir, 'adbefile.pkl') - tsbefile = os.path.join(processing_dir, 'txbefile.pkl') + respfile = os.path.join(processing_dir, "Y_adaptation.pkl") + covfile = os.path.join(processing_dir, "X_adaptation.pkl") + testrespfile_path = os.path.join(processing_dir, "Y_test_txfr.pkl") + testcovfile_path = os.path.join(processing_dir, "X_test_txfr.pkl") + trbefile = os.path.join(processing_dir, "adbefile.pkl") + tsbefile = os.path.join(processing_dir, "txbefile.pkl") - log_dir = os.path.join(processing_dir, 'log_transfer/') - output_path = os.path.join(processing_dir, 'Transfer/') - model_path = os.path.join(processing_dir, 'Models/') # path to the previously trained models - outputsuffix = '_transfer' # suffix added to the output files from the transfer function + log_dir = os.path.join(processing_dir, "log_transfer/") + output_path = os.path.join(processing_dir, "Transfer/") + model_path = os.path.join( + processing_dir, "Models/" + ) # path to the previously trained models + outputsuffix = ( + "_transfer" # suffix added to the output files from the transfer function + ) Here, the difference is that the transfer function needs a model path, which points to the models we just trained, and new site data (training @@ -290,20 +334,36 @@ and testing). That is basically the only difference. .. code:: ipython3 - yhat, s2, z_scores = ptk.normative.transfer(covfile=covfile, - respfile=respfile, - tsbefile=tsbefile, - trbefile=trbefile, - model_path = model_path, - alg='hbr', - log_path=log_dir, - binary=True, - output_path=output_path, - testcov= testcovfile_path, - testresp = testrespfile_path, - outputsuffix=outputsuffix, - savemodel=True) + yhat, s2, z_scores = ptk.normative.transfer( + covfile=covfile, + respfile=respfile, + tsbefile=tsbefile, + trbefile=trbefile, + inscaler="standardize", + outscaler="standardize", + linear_mu="True", + random_intercept_mu="True", + centered_intercept_mu="True", + model_path=model_path, + alg="hbr", + log_path=log_dir, + binary=True, + output_path=output_path, + testcov=testcovfile_path, + testresp=testrespfile_path, + outputsuffix=outputsuffix, + savemodel=True, + nuts_sampler="nutpie", + ) + +.. code:: ipython3 + output_path + +.. code:: ipython3 + + EV = pd.read_pickle("EXPV_estimate.pkl") + print(EV) And that is it, you now have models that benefited from prior knowledge about different scanner sites to learn on unseen sites. @@ -311,19 +371,12 @@ about different scanner sites to learn on unseen sites. Step 6: Interpreting model performance -------------------------------------- -Output evaluation metrics definitions - -============= ============================================================== -Abbreviation Full name -============= ============================================================== -NM Normative Model -EV / EXPV Explained Variance -MSLL Mean Standardized Log Loss -SMSE Standardized Mean Squared Error -RMSE Root Mean Squared Error between true/predicted responses -Rho Pearson orrelation between true/predicted responses -pRho Parametric p-value for this correlation -Z Z-score or deviation score -yhat predictive mean -ys2 predictive variance -============= ============================================================== +Output evaluation metrics definitions: \* yhat - predictive mean \* ys2 +- predictive variance \* nm - normative model \* Z - deviance scores \* +Rho - Pearson correlation between true and predicted responses \* pRho - +parametric p-value for this correlation \* RMSE - root mean squared +error between true/predicted responses \* SMSE - standardised mean +squared error \* EV - explained variance \* MSLL - mean standardized log +loss \* See page 23 in +http://www.gaussianprocess.org/gpml/chapters/RW2.pdf + diff --git a/doc/source/pages/HBR_NormativeModel_FCONdata_Tutorial_files/HBR_NormativeModel_FCONdata_Tutorial_10_3.png b/doc/source/pages/HBR_NormativeModel_FCONdata_Tutorial_files/HBR_NormativeModel_FCONdata_Tutorial_10_3.png new file mode 100644 index 00000000..1d5d9342 Binary files /dev/null and b/doc/source/pages/HBR_NormativeModel_FCONdata_Tutorial_files/HBR_NormativeModel_FCONdata_Tutorial_10_3.png differ diff --git a/doc/source/pages/HBR_NormativeModel_FCONdata_Tutorial_files/HBR_NormativeModel_FCONdata_Tutorial_9_3.png b/doc/source/pages/HBR_NormativeModel_FCONdata_Tutorial_files/HBR_NormativeModel_FCONdata_Tutorial_9_3.png deleted file mode 100644 index 193f9dbb..00000000 Binary files a/doc/source/pages/HBR_NormativeModel_FCONdata_Tutorial_files/HBR_NormativeModel_FCONdata_Tutorial_9_3.png and /dev/null differ diff --git a/doc/source/pages/apply_normative_models.ipynb b/doc/source/pages/apply_normative_models.ipynb new file mode 100644 index 00000000..cc9c41a5 --- /dev/null +++ b/doc/source/pages/apply_normative_models.ipynb @@ -0,0 +1,793 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "id": "2d8fb4c8-4360-4fdc-b0a2-e1c2e22bd8f9", + "metadata": { + "id": "2d8fb4c8-4360-4fdc-b0a2-e1c2e22bd8f9" + }, + "source": [ + "## Using lifespan models to make predictions on new data\n", + "\n", + "This notebook shows how to apply the coefficients from pre-estimated normative models to new data. This can be done in two different ways: (i) using a new set of data derived from the same sites used to estimate the model and (ii) on a completely different set of sites. In the latter case, we also need to estimate the site effect, which requires some calibration/adaptation data. As an illustrative example, we use a dataset derived from several [OpenNeuro datasets](https://openneuro.org/) and adapt the learned model to make predictions on these data.\n", + "\n", + "First, if necessary, we install PCNtoolkit (note: this tutorial requires at least version 0.27)" + ] + }, + { + "cell_type": "code", + "source": [ + "!pip install pcntoolkit\n", + "!pip install nutpie" + ], + "metadata": { + "id": "ks50sIbMOGzI" + }, + "id": "ks50sIbMOGzI", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ddd7b3cb-b018-4ed4-8b55-15728d8c5411", + "metadata": { + "id": "ddd7b3cb-b018-4ed4-8b55-15728d8c5411" + }, + "outputs": [], + "source": [ + "! git clone https://github.com/predictive-clinical-neuroscience/braincharts.git" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1849f76-b17d-4286-bf57-50ff56e81bf8", + "metadata": { + "id": "b1849f76-b17d-4286-bf57-50ff56e81bf8" + }, + "outputs": [], + "source": [ + "# we need to be in the scripts folder when we import the libraries in the code block below,\n", + "# because there is a function called nm_utils that is in the scripts folder that we need to import\n", + "import os\n", + "wdir = 'braincharts'\n", + "\n", + "os.chdir(wdir) #this path is setup for running on Google Colab. Change it to match your local path if running locally\n", + "root_dir = os.getcwd()" + ] + }, + { + "cell_type": "markdown", + "id": "b2227bc7-e798-470a-99bc-33561ce4511b", + "metadata": { + "id": "b2227bc7-e798-470a-99bc-33561ce4511b" + }, + "source": [ + "Now we import the required libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff661cf2-7d80-46bb-bcfb-1650a93eed3d", + "metadata": { + "id": "ff661cf2-7d80-46bb-bcfb-1650a93eed3d" + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import pickle\n", + "from matplotlib import pyplot as plt\n", + "import seaborn as sns\n", + "\n", + "from pcntoolkit.normative import estimate, predict, evaluate\n", + "from pcntoolkit.util.utils import compute_MSLL, create_design_matrix\n", + "os.chdir(os.path.join(root_dir, 'scripts'))\n", + "from nm_utils import remove_bad_subjects, load_2d\n", + "os.chdir(root_dir)" + ] + }, + { + "cell_type": "markdown", + "id": "78719463-28b2-4849-b970-cfbe2f07d214", + "metadata": { + "id": "78719463-28b2-4849-b970-cfbe2f07d214" + }, + "source": [ + "We need to unzip the models." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b1d4d4b-68ab-4bba-87f5-6062995805d0", + "metadata": { + "id": "3b1d4d4b-68ab-4bba-87f5-6062995805d0" + }, + "outputs": [], + "source": [ + "os.chdir(os.path.join(root_dir, 'models'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4b7b2f4-c514-4d4f-a6b0-9461e1b20831", + "metadata": { + "id": "d4b7b2f4-c514-4d4f-a6b0-9461e1b20831" + }, + "outputs": [], + "source": [ + "# we will use the biggest sample as our training set (approx. N=57000 subjects from 82 sites)\n", + "# for more info on the other pretrained models available in this repository,\n", + "# please refer to the accompanying paper https://elifesciences.org/articles/72904\n", + "! unzip lifespan_57K_82sites.zip" + ] + }, + { + "cell_type": "markdown", + "id": "802b1da6-04cc-4310-af81-f50d38c3e653", + "metadata": { + "id": "802b1da6-04cc-4310-af81-f50d38c3e653" + }, + "source": [ + "Next, we configure some basic variables, like where we want the analysis to be done and which model we want to use.\n", + "\n", + "**Note:** We maintain a list of site ids for each dataset, which describe the site names in the training and test data (`site_ids_tr` and `site_ids_te`), plus also the adaptation data . The training site ids are provided as a text file in the distribution and the test ids are extracted automatically from the pandas dataframe (see below). If you use additional data from the sites (e.g. later waves from ABCD), it may be necessary to adjust the site names to match the names in the training set. See the accompanying paper for more details" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f52e2a19-9b63-4f0f-97c1-387f1a1872a2", + "metadata": { + "id": "f52e2a19-9b63-4f0f-97c1-387f1a1872a2" + }, + "outputs": [], + "source": [ + "# which model do we wish to use?\n", + "model_name = 'lifespan_57K_82sites'\n", + "site_names = 'site_ids_ct_82sites.txt'\n", + "\n", + "\n", + "# where the data files live\n", + "data_dir = os.path.join(root_dir,'docs')\n", + "\n", + "# where the models live\n", + "out_dir = os.path.join(root_dir, 'models', model_name)\n", + "\n", + "# load a set of site ids from this model. This must match the training data\n", + "with open(os.path.join(root_dir,'docs', site_names)) as f:\n", + " site_ids_tr = f.read().splitlines()" + ] + }, + { + "cell_type": "markdown", + "id": "3aab54a5-2579-48d8-a81b-bbd34cea1213", + "metadata": { + "id": "3aab54a5-2579-48d8-a81b-bbd34cea1213" + }, + "source": [ + "### Load test data\n", + "\n", + "**Note:** For the purposes of this tutorial, we make predictions for a multi-site transfer dataset, derived from [OpenNeuro](https://openneuro.org/)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "262d429a-160b-4ba3-9ba4-9acc195bc644", + "metadata": { + "id": "262d429a-160b-4ba3-9ba4-9acc195bc644" + }, + "outputs": [], + "source": [ + "test_data = os.path.join(data_dir, 'OpenNeuroTransfer_ct_te.csv')\n", + "\n", + "df_te = pd.read_csv(test_data)\n", + "\n", + "# extract a list of unique site ids from the test set\n", + "site_ids_te = sorted(set(df_te['site'].to_list()))" + ] + }, + { + "cell_type": "markdown", + "id": "c636509a-8b12-43f1-811c-08cb22640be2", + "metadata": { + "id": "c636509a-8b12-43f1-811c-08cb22640be2" + }, + "source": [ + "### (Optional) Load adaptation data\n", + "\n", + "If the data you wish to make predictions for is not derived from the same scanning sites as those in the trainig set, it is necessary to learn the site effect so that we can account for it in the predictions. In order to do this in an unbiased way, we use a separate dataset, which we refer to as 'adaptation' data. This must contain data for all the same sites as in the test dataset and we assume these are coded in the same way, based on a the 'sitenum' column in the dataframe." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53551023-aff6-4934-ad2d-d77bc63c562d", + "metadata": { + "id": "53551023-aff6-4934-ad2d-d77bc63c562d" + }, + "outputs": [], + "source": [ + "adaptation_data = os.path.join(data_dir, 'OpenNeuroTransfer_ct_ad.csv')\n", + "\n", + "df_ad = pd.read_csv(adaptation_data)\n", + "\n", + "# extract a list of unique site ids from the test set\n", + "site_ids_ad = sorted(set(df_ad['site'].to_list()))\n", + "\n", + "if not all(elem in site_ids_ad for elem in site_ids_te):\n", + " print('Warning: some of the testing sites are not in the adaptation data')" + ] + }, + { + "cell_type": "markdown", + "id": "4f73e30e-c693-44b8-98c6-52b71b577ea8", + "metadata": { + "id": "4f73e30e-c693-44b8-98c6-52b71b577ea8" + }, + "source": [ + "### Configure which models to fit\n", + "\n", + "Now, we configure which imaging derived phenotypes (IDPs) we would like to process. This is just a list of column names in the dataframe we have loaded above.\n", + "\n", + "We could load the whole set (i.e. all phenotypes for which we have models for ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b48e104c-cbac-4ae2-8377-cd3ff80162fd", + "metadata": { + "id": "b48e104c-cbac-4ae2-8377-cd3ff80162fd" + }, + "outputs": [], + "source": [ + "# load the list of idps for left and right hemispheres, plus subcortical regions\n", + "with open(os.path.join(data_dir, 'phenotypes_ct_lh.txt')) as f:\n", + " idp_ids_lh = f.read().splitlines()\n", + "with open(os.path.join(data_dir, 'phenotypes_ct_rh.txt')) as f:\n", + " idp_ids_rh = f.read().splitlines()\n", + "with open(os.path.join(data_dir, 'phenotypes_sc.txt')) as f:\n", + " idp_ids_sc = f.read().splitlines()\n", + "\n", + "# we choose here to process all idps\n", + "idp_ids = idp_ids_lh + idp_ids_rh #+ idp_ids_sc" + ] + }, + { + "cell_type": "markdown", + "id": "280731ad-47d8-43e2-8cb5-4eccfd9f3f81", + "metadata": { + "id": "280731ad-47d8-43e2-8cb5-4eccfd9f3f81" + }, + "source": [ + "... or alternatively, we could just specify a list" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b74d75f-77a5-474a-9c9b-29aab1ce53a2", + "metadata": { + "id": "8b74d75f-77a5-474a-9c9b-29aab1ce53a2" + }, + "outputs": [], + "source": [ + "idp_ids = [ 'Left-Thalamus-Proper', 'Left-Lateral-Ventricle', 'rh_MeanThickness_thickness']" + ] + }, + { + "cell_type": "markdown", + "id": "56ee1f7f-8684-4f1c-b142-a68176407029", + "metadata": { + "id": "56ee1f7f-8684-4f1c-b142-a68176407029" + }, + "source": [ + "### Configure covariates\n", + "\n", + "Now, we configure some parameters to fit the model. First, we choose which columns of the pandas dataframe contain the covariates (age and sex). The site parameters are configured automatically later on by the `configure_design_matrix()` function, when we loop through the IDPs in the list\n", + "\n", + "The supplied coefficients are derived from a 'warped' Bayesian linear regression model, which uses a nonlinear warping function to model non-Gaussianity (`sinarcsinh`) plus a non-linear basis expansion (a cubic b-spline basis set with 5 knot points, which is the default value in the PCNtoolkit package). Since we are sticking with the default value, we do not need to specify any parameters for this, but we do need to specify the limits. We choose to pad the input by a few years either side of the input range. We will also set a couple of options that control the estimation of the model\n", + "\n", + "For further details about the likelihood warping approach, see the accompanying paper and [Fraza et al 2021](https://www.biorxiv.org/content/10.1101/2021.04.05.438429v1)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62312b8e-4972-4238-abf9-87d9bb33cc10", + "metadata": { + "id": "62312b8e-4972-4238-abf9-87d9bb33cc10" + }, + "outputs": [], + "source": [ + "# which data columns do we wish to use as covariates?\n", + "cols_cov = ['age','sex']\n", + "\n", + "# limits for cubic B-spline basis\n", + "xmin = -5\n", + "xmax = 110\n", + "\n", + "# Absolute Z treshold above which a sample is considered to be an outlier (without fitting any model)\n", + "outlier_thresh = 7" + ] + }, + { + "cell_type": "markdown", + "id": "42bc1072-e9ed-4f2a-9fdd-cbd626a61542", + "metadata": { + "id": "42bc1072-e9ed-4f2a-9fdd-cbd626a61542" + }, + "source": [ + "### Make predictions\n", + "\n", + "This will make predictions for each IDP separately. This is done by extracting a column from the dataframe (i.e. specifying the IDP as the response variable) and saving it as a numpy array. Then, we configure the covariates, which is a numpy data array having the number of rows equal to the number of datapoints in the test set. The columns are specified as follows:\n", + "\n", + "- A global intercept (column of ones)\n", + "- The covariate columns (here age and sex, coded as 0=female/1=male)\n", + "- Dummy coded columns for the sites in the training set (one column per site)\n", + "- Columns for the basis expansion (seven columns for the default parameterisation)\n", + "\n", + "Once these are saved as numpy arrays in ascii format (as here) or (alternatively) in pickle format, these are passed as inputs to the `predict()` method in the PCNtoolkit normative modelling framework. These are written in the same format to the location specified by `idp_dir`. At the end of this step, we have a set of predictions and Z-statistics for the test dataset that we can take forward to further analysis.\n", + "\n", + "Note that when we need to make predictions on new data, the procedure is more involved, since we need to prepare, process and store covariates, response variables and site ids for the adaptation data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07b7471b-c334-464f-8273-b409b7acaac2", + "metadata": { + "id": "07b7471b-c334-464f-8273-b409b7acaac2" + }, + "outputs": [], + "source": [ + "for idp_num, idp in enumerate(idp_ids):\n", + " print('Running IDP', idp_num, idp, ':')\n", + " idp_dir = os.path.join(out_dir, idp)\n", + " os.chdir(idp_dir)\n", + "\n", + " # extract and save the response variables for the test set\n", + " y_te = df_te[idp].to_numpy()\n", + "\n", + " # save the variables\n", + " resp_file_te = os.path.join(idp_dir, 'resp_te.txt')\n", + " np.savetxt(resp_file_te, y_te)\n", + "\n", + " # configure and save the design matrix\n", + " cov_file_te = os.path.join(idp_dir, 'cov_bspline_te.txt')\n", + " X_te = create_design_matrix(df_te[cols_cov],\n", + " site_ids = df_te['site'],\n", + " all_sites = site_ids_tr,\n", + " basis = 'bspline',\n", + " xmin = xmin,\n", + " xmax = xmax)\n", + " np.savetxt(cov_file_te, X_te)\n", + "\n", + " # check whether all sites in the test set are represented in the training set\n", + " if all(elem in site_ids_tr for elem in site_ids_te):\n", + " print('All sites are present in the training data')\n", + "\n", + " # just make predictions\n", + " yhat_te, s2_te, Z = predict(cov_file_te,\n", + " alg='blr',\n", + " respfile=resp_file_te,\n", + " model_path=os.path.join(idp_dir,'Models'))\n", + " else:\n", + " print('Some sites missing from the training data. Adapting model')\n", + "\n", + " # save the covariates for the adaptation data\n", + " X_ad = create_design_matrix(df_ad[cols_cov],\n", + " site_ids = df_ad['site'],\n", + " all_sites = site_ids_tr,\n", + " basis = 'bspline',\n", + " xmin = xmin,\n", + " xmax = xmax)\n", + " cov_file_ad = os.path.join(idp_dir, 'cov_bspline_ad.txt')\n", + " np.savetxt(cov_file_ad, X_ad)\n", + "\n", + " # save the responses for the adaptation data\n", + " resp_file_ad = os.path.join(idp_dir, 'resp_ad.txt')\n", + " y_ad = df_ad[idp].to_numpy()\n", + " np.savetxt(resp_file_ad, y_ad)\n", + "\n", + " # save the site ids for the adaptation data\n", + " sitenum_file_ad = os.path.join(idp_dir, 'sitenum_ad.txt')\n", + " site_num_ad = df_ad['sitenum'].to_numpy(dtype=int)\n", + " np.savetxt(sitenum_file_ad, site_num_ad)\n", + "\n", + " # save the site ids for the test data\n", + " sitenum_file_te = os.path.join(idp_dir, 'sitenum_te.txt')\n", + " site_num_te = df_te['sitenum'].to_numpy(dtype=int)\n", + " np.savetxt(sitenum_file_te, site_num_te)\n", + "\n", + " yhat_te, s2_te, Z = predict(cov_file_te,\n", + " alg = 'blr',\n", + " respfile = resp_file_te,\n", + " model_path = os.path.join(idp_dir,'Models'),\n", + " adaptrespfile = resp_file_ad,\n", + " adaptcovfile = cov_file_ad,\n", + " adaptvargroupfile = sitenum_file_ad,\n", + " testvargroupfile = sitenum_file_te)" + ] + }, + { + "cell_type": "markdown", + "id": "75210821-ccb8-4bd2-82f3-641708811b21", + "metadata": { + "id": "75210821-ccb8-4bd2-82f3-641708811b21" + }, + "source": [ + "### Preparing dummy data for plotting\n", + "\n", + "Now, we plot the centiles of variation estimated by the normative model.\n", + "\n", + "We do this by making use of a set of dummy covariates that span the whole range of the input space (for age) for a fixed value of the other covariates (e.g. sex) so that we can make predictions for these dummy data points, then plot them. We configure these dummy predictions using the same procedure as we used for the real data. We can use the same dummy data for all the IDPs we wish to plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d0743d8-28ca-4a14-8ef0-99bf40434b5b", + "metadata": { + "id": "2d0743d8-28ca-4a14-8ef0-99bf40434b5b" + }, + "outputs": [], + "source": [ + "# which sex do we want to plot?\n", + "sex = 1 # 1 = male 0 = female\n", + "if sex == 1:\n", + " clr = 'blue';\n", + "else:\n", + " clr = 'red'\n", + "\n", + "# create dummy data for visualisation\n", + "print('configuring dummy data ...')\n", + "xx = np.arange(xmin, xmax, 0.5)\n", + "X0_dummy = np.zeros((len(xx), 2))\n", + "X0_dummy[:,0] = xx\n", + "X0_dummy[:,1] = sex\n", + "\n", + "# create the design matrix\n", + "X_dummy = create_design_matrix(X0_dummy, xmin=xmin, xmax=xmax, site_ids=None, all_sites=site_ids_tr)\n", + "\n", + "# save the dummy covariates\n", + "cov_file_dummy = os.path.join(out_dir,'cov_bspline_dummy_mean.txt')\n", + "np.savetxt(cov_file_dummy, X_dummy)" + ] + }, + { + "cell_type": "markdown", + "id": "126323a3-2270-4796-97c4-94629730ddf7", + "metadata": { + "id": "126323a3-2270-4796-97c4-94629730ddf7" + }, + "source": [ + "### Plotting the normative models\n", + "\n", + "Now we loop through the IDPs, plotting each one separately. The outputs of this step are a set of quantitative regression metrics for each IDP and a set of centile curves which we plot the test data against.\n", + "\n", + "This part of the code is relatively complex because we need to keep track of many quantities for the plotting. We also need to remember whether the data need to be warped or not. By default in PCNtoolkit, predictions in the form of `yhat, s2` are always in the warped (Gaussian) space. If we want predictions in the input (non-Gaussian) space, then we need to warp them with the inverse of the estimated warping function. This can be done using the function `nm.blr.warp.warp_predictions()`.\n", + "\n", + "**Note:** it is necessary to update the intercept for each of the sites. For purposes of visualisation, here we do this by adjusting the median of the data to match the dummy predictions, but note that all the quantitative metrics are estimated using the predictions that are adjusted properly using a learned offset (or adjusted using a hold-out adaptation set, as above). Note also that for the calibration data we require at least two data points of the same sex in each site to be able to estimate the variance. Of course, in a real example, you would want many more than just two since we need to get a reliable estimate of the variance for each site." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cdd68cc6-212b-4149-b86a-24e842078e1a", + "metadata": { + "id": "cdd68cc6-212b-4149-b86a-24e842078e1a" + }, + "outputs": [], + "source": [ + "sns.set(style='whitegrid')\n", + "\n", + "for idp_num, idp in enumerate(idp_ids):\n", + " print('Running IDP', idp_num, idp, ':')\n", + " idp_dir = os.path.join(out_dir, idp)\n", + " os.chdir(idp_dir)\n", + "\n", + " # load the true data points\n", + " yhat_te = load_2d(os.path.join(idp_dir, 'yhat_predict.txt'))\n", + " s2_te = load_2d(os.path.join(idp_dir, 'ys2_predict.txt'))\n", + " y_te = load_2d(os.path.join(idp_dir, 'resp_te.txt'))\n", + "\n", + " # set up the covariates for the dummy data\n", + " print('Making predictions with dummy covariates (for visualisation)')\n", + " yhat, s2 = predict(cov_file_dummy,\n", + " alg = 'blr',\n", + " respfile = None,\n", + " model_path = os.path.join(idp_dir,'Models'),\n", + " outputsuffix = '_dummy')\n", + "\n", + " # load the normative model\n", + " with open(os.path.join(idp_dir,'Models', 'NM_0_0_estimate.pkl'), 'rb') as handle:\n", + " nm = pickle.load(handle)\n", + "\n", + " # get the warp and warp parameters\n", + " W = nm.blr.warp\n", + " warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1]\n", + "\n", + " # first, we warp predictions for the true data and compute evaluation metrics\n", + " med_te = W.warp_predictions(np.squeeze(yhat_te), np.squeeze(s2_te), warp_param)[0]\n", + " med_te = med_te[:, np.newaxis]\n", + " print('metrics:', evaluate(y_te, med_te))\n", + "\n", + " # then, we warp dummy predictions to create the plots\n", + " med, pr_int = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2), warp_param)\n", + "\n", + " # extract the different variance components to visualise\n", + " beta, junk1, junk2 = nm.blr._parse_hyps(nm.blr.hyp, X_dummy)\n", + " s2n = 1/beta # variation (aleatoric uncertainty)\n", + " s2s = s2-s2n # modelling uncertainty (epistemic uncertainty)\n", + "\n", + " # plot the data points\n", + " y_te_rescaled_all = np.zeros_like(y_te)\n", + " for sid, site in enumerate(site_ids_te):\n", + " # plot the true test data points\n", + " if all(elem in site_ids_tr for elem in site_ids_te):\n", + " # all data in the test set are present in the training set\n", + "\n", + " # first, we select the data points belonging to this particular site\n", + " idx = np.where(np.bitwise_and(X_te[:,2] == sex, X_te[:,sid+len(cols_cov)+1] !=0))[0]\n", + " if len(idx) == 0:\n", + " print('No data for site', sid, site, 'skipping...')\n", + " continue\n", + "\n", + " # then directly adjust the data\n", + " idx_dummy = np.bitwise_and(X_dummy[:,1] > X_te[idx,1].min(), X_dummy[:,1] < X_te[idx,1].max())\n", + " y_te_rescaled = y_te[idx] - np.median(y_te[idx]) + np.median(med[idx_dummy])\n", + " else:\n", + " # we need to adjust the data based on the adaptation dataset\n", + "\n", + " # first, select the data point belonging to this particular site\n", + " idx = np.where(np.bitwise_and(X_te[:,2] == sex, (df_te['site'] == site).to_numpy()))[0]\n", + "\n", + " # load the adaptation data\n", + " y_ad = load_2d(os.path.join(idp_dir, 'resp_ad.txt'))\n", + " X_ad = load_2d(os.path.join(idp_dir, 'cov_bspline_ad.txt'))\n", + " idx_a = np.where(np.bitwise_and(X_ad[:,2] == sex, (df_ad['site'] == site).to_numpy()))[0]\n", + " if len(idx) < 2 or len(idx_a) < 2:\n", + " print('Insufficent data for site', sid, site, 'skipping...')\n", + " continue\n", + "\n", + " # adjust and rescale the data\n", + " y_te_rescaled, s2_rescaled = nm.blr.predict_and_adjust(nm.blr.hyp,\n", + " X_ad[idx_a,:],\n", + " np.squeeze(y_ad[idx_a]),\n", + " Xs=None,\n", + " ys=np.squeeze(y_te[idx]))\n", + " # plot the (adjusted) data points\n", + " plt.scatter(X_te[idx,1], y_te_rescaled, s=4, color=clr, alpha = 0.1)\n", + "\n", + " # plot the median of the dummy data\n", + " plt.plot(xx, med, clr)\n", + "\n", + " # fill the gaps in between the centiles\n", + " junk, pr_int25 = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2), warp_param, percentiles=[0.25,0.75])\n", + " junk, pr_int95 = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2), warp_param, percentiles=[0.05,0.95])\n", + " junk, pr_int99 = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2), warp_param, percentiles=[0.01,0.99])\n", + " plt.fill_between(xx, pr_int25[:,0], pr_int25[:,1], alpha = 0.1,color=clr)\n", + " plt.fill_between(xx, pr_int95[:,0], pr_int95[:,1], alpha = 0.1,color=clr)\n", + " plt.fill_between(xx, pr_int99[:,0], pr_int99[:,1], alpha = 0.1,color=clr)\n", + "\n", + " # make the width of each centile proportional to the epistemic uncertainty\n", + " junk, pr_int25l = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2-0.5*s2s), warp_param, percentiles=[0.25,0.75])\n", + " junk, pr_int95l = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2-0.5*s2s), warp_param, percentiles=[0.05,0.95])\n", + " junk, pr_int99l = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2-0.5*s2s), warp_param, percentiles=[0.01,0.99])\n", + " junk, pr_int25u = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2+0.5*s2s), warp_param, percentiles=[0.25,0.75])\n", + " junk, pr_int95u = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2+0.5*s2s), warp_param, percentiles=[0.05,0.95])\n", + " junk, pr_int99u = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2+0.5*s2s), warp_param, percentiles=[0.01,0.99])\n", + " plt.fill_between(xx, pr_int25l[:,0], pr_int25u[:,0], alpha = 0.3,color=clr)\n", + " plt.fill_between(xx, pr_int95l[:,0], pr_int95u[:,0], alpha = 0.3,color=clr)\n", + " plt.fill_between(xx, pr_int99l[:,0], pr_int99u[:,0], alpha = 0.3,color=clr)\n", + " plt.fill_between(xx, pr_int25l[:,1], pr_int25u[:,1], alpha = 0.3,color=clr)\n", + " plt.fill_between(xx, pr_int95l[:,1], pr_int95u[:,1], alpha = 0.3,color=clr)\n", + " plt.fill_between(xx, pr_int99l[:,1], pr_int99u[:,1], alpha = 0.3,color=clr)\n", + "\n", + " # plot actual centile lines\n", + " plt.plot(xx, pr_int25[:,0],color=clr, linewidth=0.5)\n", + " plt.plot(xx, pr_int25[:,1],color=clr, linewidth=0.5)\n", + " plt.plot(xx, pr_int95[:,0],color=clr, linewidth=0.5)\n", + " plt.plot(xx, pr_int95[:,1],color=clr, linewidth=0.5)\n", + " plt.plot(xx, pr_int99[:,0],color=clr, linewidth=0.5)\n", + " plt.plot(xx, pr_int99[:,1],color=clr, linewidth=0.5)\n", + "\n", + " plt.xlabel('Age')\n", + " plt.ylabel(idp)\n", + " plt.title(idp)\n", + " plt.xlim((0,90))\n", + " plt.savefig(os.path.join(idp_dir, 'centiles_' + str(sex)), bbox_inches='tight')\n", + " plt.show()\n", + "\n", + "os.chdir(out_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "135dbebd-f563-4a2a-9f44-f96757fb4b0b", + "metadata": { + "id": "135dbebd-f563-4a2a-9f44-f96757fb4b0b" + }, + "outputs": [], + "source": [ + "# explore an example output folder of a single model (one ROI)\n", + "# think about what each of these output files represents.\n", + "# Hint: look at the variable names and comments in the code block above\n", + "! ls rh_MeanThickness_thickness/" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe1cac10-01f1-42fd-a4b7-cf08ce0d64be", + "metadata": { + "id": "fe1cac10-01f1-42fd-a4b7-cf08ce0d64be" + }, + "outputs": [], + "source": [ + "# check that the number of deviation scores matches the number of subjects in the test set\n", + "# there should be one deviation score per subject (one line per subject), so we can\n", + "# verify by counting the line numbers in the Z_predict.txt file\n", + "! cat rh_MeanThickness_thickness/Z_predict.txt | wc" + ] + }, + { + "cell_type": "markdown", + "id": "88d2dbc0-e82f-4af5-91eb-dc8aa60f6ba7", + "metadata": { + "id": "88d2dbc0-e82f-4af5-91eb-dc8aa60f6ba7" + }, + "source": [ + "The deviation scores are output as a text file in separate folders. We want to summarize the deviation scores across all models estimates so we can organize them into a single file, and merge the deviation scores into the original data file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3fb0ced-ed44-487c-86b8-07b9fc04d64e", + "metadata": { + "id": "e3fb0ced-ed44-487c-86b8-07b9fc04d64e" + }, + "outputs": [], + "source": [ + "! mkdir deviation_scores" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "571f549e-9edd-4f8b-a6b3-d76cd23609f0", + "metadata": { + "id": "571f549e-9edd-4f8b-a6b3-d76cd23609f0" + }, + "outputs": [], + "source": [ + "! for i in *; do if [[ -e ${i}/Z_predict.txt ]]; then cp ${i}/Z_predict.txt deviation_scores/${i}_Z_predict.txt; fi; done" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f63da6c-91e8-4852-91a7-a4e8bc9d9f31", + "metadata": { + "id": "9f63da6c-91e8-4852-91a7-a4e8bc9d9f31" + }, + "outputs": [], + "source": [ + "z_dir = os.path.join(root_dir, 'models', model_name, 'deviation_scores')\n", + "\n", + "filelist = [name for name in os.listdir(z_dir)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8791195b-09a9-4251-8fd7-35e80a028d2f", + "metadata": { + "id": "8791195b-09a9-4251-8fd7-35e80a028d2f" + }, + "outputs": [], + "source": [ + "os.chdir(z_dir)\n", + "Z_df = pd.concat([pd.read_csv(item, names=[item[:-4]]) for item in filelist], axis=1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1054959-dd17-4c1b-b1db-56cf849ecae2", + "metadata": { + "id": "f1054959-dd17-4c1b-b1db-56cf849ecae2" + }, + "outputs": [], + "source": [ + "df_te.reset_index(inplace=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ab00be4-d9c8-49aa-b407-f69946ca2d6c", + "metadata": { + "id": "6ab00be4-d9c8-49aa-b407-f69946ca2d6c" + }, + "outputs": [], + "source": [ + "Z_df['sub_id'] = df_te['sub_id']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f6185b6-d9d7-4651-bbab-2cfba6c46963", + "metadata": { + "id": "9f6185b6-d9d7-4651-bbab-2cfba6c46963" + }, + "outputs": [], + "source": [ + "df_te_Z = pd.merge(df_te, Z_df, on='sub_id', how='inner')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae932714-60c3-4a36-8b72-cd9086a25761", + "metadata": { + "id": "ae932714-60c3-4a36-8b72-cd9086a25761" + }, + "outputs": [], + "source": [ + "df_te_Z.to_csv('OpenNeuroTransfer_deviation_scores.csv', index=False)" + ] + } + ], + "metadata": { + "colab": { + "name": "apply_normative_models.ipynb", + "provenance": [], + "include_colab_link": true + }, + "kernelspec": { + "display_name": "braincharts", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.7" + }, + "vscode": { + "interpreter": { + "hash": "f65f66140ab2d9a57fedc58a3b7e1d01f34d12111107cec87dc46b07c8179a15" + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/doc/source/pages/apply_normative_models.rst b/doc/source/pages/apply_normative_models.rst index 9a55d8b4..1dabeb43 100644 --- a/doc/source/pages/apply_normative_models.rst +++ b/doc/source/pages/apply_normative_models.rst @@ -1,17 +1,5 @@ -.. title:: Braincharts tutorial - -Braincharts: transfer -=================================== - -Code for transfering the models from `Charting Brain Growth and Aging at High Spatial Precision. `__ - -.. image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/predictive-clinical-neuroscience/braincharts/blob/master/scripts/apply_normative_models_ct.ipynb - - -.. figure:: ./brainchart_fig1.png - :height: 400px - :align: center +Using lifespan models to make predictions on new data +----------------------------------------------------- This notebook shows how to apply the coefficients from pre-estimated normative models to new data. This can be done in two different ways: @@ -24,34 +12,26 @@ datasets `__ and adapt the learned model to make predictions on these data. First, if necessary, we install PCNtoolkit (note: this tutorial requires -at least version 0.20) +at least version 0.27) .. code:: ipython3 - !pip install pcntoolkit==0.26 + !pip install pcntoolkit + !pip install nutpie .. code:: ipython3 ! git clone https://github.com/predictive-clinical-neuroscience/braincharts.git - -.. parsed-literal:: - - Cloning into 'braincharts'... - remote: Enumerating objects: 1444, done. - remote: Counting objects: 100% (1444/1444), done. - remote: Compressing objects: 100% (1365/1365), done. - remote: Total 1444 (delta 153), reused 1342 (delta 75), pack-reused 0 - Receiving objects: 100% (1444/1444), 57.99 MiB | 34.87 MiB/s, done. - Resolving deltas: 100% (153/153), done. - - .. code:: ipython3 # we need to be in the scripts folder when we import the libraries in the code block below, # because there is a function called nm_utils that is in the scripts folder that we need to import import os - os.chdir('/content/braincharts/scripts/') #this path is setup for running on Google Colab. Change it to match your local path if running locally + wdir = 'braincharts' + + os.chdir(wdir) #this path is setup for running on Google Colab. Change it to match your local path if running locally + root_dir = os.getcwd() Now we import the required libraries @@ -65,45 +45,34 @@ Now we import the required libraries from pcntoolkit.normative import estimate, predict, evaluate from pcntoolkit.util.utils import compute_MSLL, create_design_matrix + os.chdir(os.path.join(root_dir, 'scripts')) from nm_utils import remove_bad_subjects, load_2d + os.chdir(root_dir) We need to unzip the models. .. code:: ipython3 - os.chdir('/content/braincharts/models/') - -.. code:: ipython3 - - ls - - -.. parsed-literal:: - - lifespan_12K_57sites_mqc2_train.zip lifespan_29K_82sites_train.zip - lifespan_12K_59sites_mqc_train.zip lifespan_57K_82sites.zip - lifespan_23K_57sites_mqc2.zip README.md - + os.chdir(os.path.join(root_dir, 'models')) .. code:: ipython3 # we will use the biggest sample as our training set (approx. N=57000 subjects from 82 sites) - # for more info on the other pretrained models available in this repository, + # for more info on the other pretrained models available in this repository, # please refer to the accompanying paper https://elifesciences.org/articles/72904 ! unzip lifespan_57K_82sites.zip Next, we configure some basic variables, like where we want the analysis to be done and which model we want to use. -.. note:: - We maintain a list of site ids for each dataset, which - describe the site names in the training and test data (``site_ids_tr`` - and ``site_ids_te``), plus also the adaptation data . The training site - ids are provided as a text file in the distribution and the test ids are - extracted automatically from the pandas dataframe (see below). If you - use additional data from the sites (e.g. later waves from ABCD), it may - be necessary to adjust the site names to match the names in the training - set. See the accompanying paper for more details +**Note:** We maintain a list of site ids for each dataset, which +describe the site names in the training and test data (``site_ids_tr`` +and ``site_ids_te``), plus also the adaptation data . The training site +ids are provided as a text file in the distribution and the test ids are +extracted automatically from the pandas dataframe (see below). If you +use additional data from the sites (e.g. later waves from ABCD), it may +be necessary to adjust the site names to match the names in the training +set. See the accompanying paper for more details .. code:: ipython3 @@ -111,91 +80,35 @@ to be done and which model we want to use. model_name = 'lifespan_57K_82sites' site_names = 'site_ids_ct_82sites.txt' - # where the analysis takes place - root_dir = '/content/braincharts' + + # where the data files live + data_dir = os.path.join(root_dir,'docs') + + # where the models live out_dir = os.path.join(root_dir, 'models', model_name) # load a set of site ids from this model. This must match the training data with open(os.path.join(root_dir,'docs', site_names)) as f: site_ids_tr = f.read().splitlines() -Download test dataset ------------------------------------------------------ - -As mentioned above, to demonstrate this tool we will use a test dataset -derived from the FCON 1000 dataset. We provide a prepackaged -training/test split of these data in the required format (also after -removing sites with only a few data points), -`here `__. -you can get these data by running the following commmands: - -.. code:: ipython3 - - os.chdir(root_dir) - !wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/braincharts/master/docs/OpenNeuroTransfer_ct_te.csv - !wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/braincharts/master/docs/OpenNeuroTransfer_ct_tr.csv - - -.. parsed-literal:: - - --2022-02-17 15:01:31-- https://raw.githubusercontent.com/predictive-clinical-neuroscience/braincharts/master/docs/OpenNeuroTransfer_ct_te.csv - Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ... - Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected. - HTTP request sent, awaiting response... 200 OK - Length: 628752 (614K) [text/plain] - Saving to: ‘OpenNeuroTransfer_te.csv’ - - OpenNeuroTransfer_t 100%[===================>] 614.02K --.-KB/s in 0.03s - - 2022-02-17 15:01:31 (22.0 MB/s) - ‘OpenNeuroTransfer_te.csv’ saved [628752/628752] - - --2022-02-17 15:01:31-- https://raw.githubusercontent.com/predictive-clinical-neuroscience/braincharts/master/docs/OpenNeuroTransfer_ct_tr.csv - Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.109.133, 185.199.110.133, 185.199.108.133, ... - Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.109.133|:443... connected. - HTTP request sent, awaiting response... 200 OK - Length: 163753 (160K) [text/plain] - Saving to: ‘OpenNeuroTransfer_tr.csv’ - - OpenNeuroTransfer_c 100%[===================>] 159.92K --.-KB/s in 0.03s - - 2022-02-17 15:01:32 (6.08 MB/s) - ‘OpenNeuroTransfer_ct_tr.csv’ saved [163753/163753] - - - Load test data ------------------------------------------------------ - -Now we load the test data and remove some subjects that may have poor -scan quality. This asssesment is based on the Freesurfer Euler -characteristic as described in the papers below. +~~~~~~~~~~~~~~ -.. note:: - For the purposes of this tutorial, we make predictions for all - sites in the FCON 1000 dataset, but two of them were also included in - the training data (named ‘Baltimore’ and ‘NewYork_a’). In this case, - this will only slightly bias the accuracy, but in order to replicate the - results in the paper, it would be necessary to additionally remove these - sites from the test dataframe. - -**References** - `Kia et al -2021 `__ -- `Rosen et al -2018 `__ +**Note:** For the purposes of this tutorial, we make predictions for a +multi-site transfer dataset, derived from +`OpenNeuro `__. .. code:: ipython3 - test_data = os.path.join(root_dir, 'OpenNeuroTransfer_ct_te.csv') + test_data = os.path.join(data_dir, 'OpenNeuroTransfer_ct_te.csv') df_te = pd.read_csv(test_data) - # remove some bad subjects, this requires having a column called "avg_en" that corresponds to the average Euler number extracted from Freesurfer - # df_te, bad_sub = remove_bad_subjects(df_te, df_te) - # extract a list of unique site ids from the test set site_ids_te = sorted(set(df_te['site'].to_list())) (Optional) Load adaptation data ------------------------------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If the data you wish to make predictions for is not derived from the same scanning sites as those in the trainig set, it is necessary to @@ -207,13 +120,10 @@ same way, based on a the ‘sitenum’ column in the dataframe. .. code:: ipython3 - adaptation_data = os.path.join(root_dir, 'OpenNeuroTransfer_ct_tr.csv') + adaptation_data = os.path.join(data_dir, 'OpenNeuroTransfer_ct_ad.csv') df_ad = pd.read_csv(adaptation_data) - # remove some bad subjects, this requires having a column called "avg_en" that corresponds to the average Euler number extracted from Freesurfer - # df_ad, bad_sub = remove_bad_subjects(df_ad, df_ad) - # extract a list of unique site ids from the test set site_ids_ad = sorted(set(df_ad['site'].to_list())) @@ -221,7 +131,7 @@ same way, based on a the ‘sitenum’ column in the dataframe. print('Warning: some of the testing sites are not in the adaptation data') Configure which models to fit ------------------------------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now, we configure which imaging derived phenotypes (IDPs) we would like to process. This is just a list of column names in the dataframe we have @@ -233,15 +143,15 @@ models for … .. code:: ipython3 # load the list of idps for left and right hemispheres, plus subcortical regions - with open(os.path.join(root_dir,'docs','phenotypes_ct_lh.txt')) as f: + with open(os.path.join(data_dir, 'phenotypes_ct_lh.txt')) as f: idp_ids_lh = f.read().splitlines() - with open(os.path.join(root_dir,'docs','phenotypes_ct_rh.txt')) as f: + with open(os.path.join(data_dir, 'phenotypes_ct_rh.txt')) as f: idp_ids_rh = f.read().splitlines() - with open(os.path.join(root_dir,'docs','phenotypes_sc.txt')) as f: + with open(os.path.join(data_dir, 'phenotypes_sc.txt')) as f: idp_ids_sc = f.read().splitlines() # we choose here to process all idps - idp_ids = idp_ids_lh + idp_ids_rh + idp_ids_sc + idp_ids = idp_ids_lh + idp_ids_rh #+ idp_ids_sc … or alternatively, we could just specify a list @@ -250,7 +160,7 @@ models for … idp_ids = [ 'Left-Thalamus-Proper', 'Left-Lateral-Ventricle', 'rh_MeanThickness_thickness'] Configure covariates ------------------------------------------------------ +~~~~~~~~~~~~~~~~~~~~ Now, we configure some parameters to fit the model. First, we choose which columns of the pandas dataframe contain the covariates (age and @@ -274,18 +184,18 @@ accompanying paper and `Fraza et al .. code:: ipython3 - # which data columns do we wish to use as covariates? + # which data columns do we wish to use as covariates? cols_cov = ['age','sex'] - # limits for cubic B-spline basis - xmin = -5 + # limits for cubic B-spline basis + xmin = -5 xmax = 110 # Absolute Z treshold above which a sample is considered to be an outlier (without fitting any model) outlier_thresh = 7 Make predictions ------------------------------------------------------ +~~~~~~~~~~~~~~~~ This will make predictions for each IDP separately. This is done by extracting a column from the dataframe (i.e. specifying the IDP as the @@ -294,12 +204,12 @@ the covariates, which is a numpy data array having the number of rows equal to the number of datapoints in the test set. The columns are specified as follows: -- A global intercept (column of ones) -- The covariate columns (here age and sex, coded as 0=female/1=male) -- Dummy coded columns for the sites in the training set (one column per - site) -- Columns for the basis expansion (seven columns for the default - parameterisation) +- A global intercept (column of ones) +- The covariate columns (here age and sex, coded as 0=female/1=male) +- Dummy coded columns for the sites in the training set (one column per + site) +- Columns for the basis expansion (seven columns for the default + parameterisation) Once these are saved as numpy arrays in ascii format (as here) or (alternatively) in pickle format, these are passed as inputs to the @@ -309,74 +219,74 @@ These are written in the same format to the location specified by Z-statistics for the test dataset that we can take forward to further analysis. -When we need to make predictions on new data, the procedure is +Note that when we need to make predictions on new data, the procedure is more involved, since we need to prepare, process and store covariates, response variables and site ids for the adaptation data. .. code:: ipython3 - for idp_num, idp in enumerate(idp_ids): + for idp_num, idp in enumerate(idp_ids): print('Running IDP', idp_num, idp, ':') idp_dir = os.path.join(out_dir, idp) os.chdir(idp_dir) - + # extract and save the response variables for the test set y_te = df_te[idp].to_numpy() - + # save the variables - resp_file_te = os.path.join(idp_dir, 'resp_te.txt') + resp_file_te = os.path.join(idp_dir, 'resp_te.txt') np.savetxt(resp_file_te, y_te) - + # configure and save the design matrix cov_file_te = os.path.join(idp_dir, 'cov_bspline_te.txt') - X_te = create_design_matrix(df_te[cols_cov], + X_te = create_design_matrix(df_te[cols_cov], site_ids = df_te['site'], all_sites = site_ids_tr, - basis = 'bspline', - xmin = xmin, + basis = 'bspline', + xmin = xmin, xmax = xmax) np.savetxt(cov_file_te, X_te) - + # check whether all sites in the test set are represented in the training set if all(elem in site_ids_tr for elem in site_ids_te): print('All sites are present in the training data') - + # just make predictions - yhat_te, s2_te, Z = predict(cov_file_te, - alg='blr', - respfile=resp_file_te, + yhat_te, s2_te, Z = predict(cov_file_te, + alg='blr', + respfile=resp_file_te, model_path=os.path.join(idp_dir,'Models')) else: print('Some sites missing from the training data. Adapting model') - + # save the covariates for the adaptation data - X_ad = create_design_matrix(df_ad[cols_cov], + X_ad = create_design_matrix(df_ad[cols_cov], site_ids = df_ad['site'], all_sites = site_ids_tr, - basis = 'bspline', - xmin = xmin, + basis = 'bspline', + xmin = xmin, xmax = xmax) - cov_file_ad = os.path.join(idp_dir, 'cov_bspline_ad.txt') + cov_file_ad = os.path.join(idp_dir, 'cov_bspline_ad.txt') np.savetxt(cov_file_ad, X_ad) - + # save the responses for the adaptation data - resp_file_ad = os.path.join(idp_dir, 'resp_ad.txt') + resp_file_ad = os.path.join(idp_dir, 'resp_ad.txt') y_ad = df_ad[idp].to_numpy() np.savetxt(resp_file_ad, y_ad) - + # save the site ids for the adaptation data - sitenum_file_ad = os.path.join(idp_dir, 'sitenum_ad.txt') + sitenum_file_ad = os.path.join(idp_dir, 'sitenum_ad.txt') site_num_ad = df_ad['sitenum'].to_numpy(dtype=int) np.savetxt(sitenum_file_ad, site_num_ad) - - # save the site ids for the test data + + # save the site ids for the test data sitenum_file_te = os.path.join(idp_dir, 'sitenum_te.txt') site_num_te = df_te['sitenum'].to_numpy(dtype=int) np.savetxt(sitenum_file_te, site_num_te) - - yhat_te, s2_te, Z = predict(cov_file_te, - alg = 'blr', - respfile = resp_file_te, + + yhat_te, s2_te, Z = predict(cov_file_te, + alg = 'blr', + respfile = resp_file_te, model_path = os.path.join(idp_dir,'Models'), adaptrespfile = resp_file_ad, adaptcovfile = cov_file_ad, @@ -409,16 +319,8 @@ response variables and site ids for the adaptation data. Writing outputs ... -Evaluate the performance ------------------------------------------------------ - -.. figure:: ./brainchart_fig3.png - :height: 400px - :align: center - - Preparing dummy data for plotting ------------------------------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now, we plot the centiles of variation estimated by the normative model. @@ -431,9 +333,9 @@ dummy data for all the IDPs we wish to plot .. code:: ipython3 - # which sex do we want to plot? + # which sex do we want to plot? sex = 1 # 1 = male 0 = female - if sex == 1: + if sex == 1: clr = 'blue'; else: clr = 'red' @@ -459,7 +361,7 @@ dummy data for all the IDPs we wish to plot Plotting the normative models ------------------------------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now we loop through the IDPs, plotting each one separately. The outputs of this step are a set of quantitative regression metrics for each IDP @@ -474,83 +376,82 @@ space, then we need to warp them with the inverse of the estimated warping function. This can be done using the function ``nm.blr.warp.warp_predictions()``. -.. note:: - It is necessary to update the intercept for each of the sites. - For purposes of visualisation, here we do this by adjusting the median - of the data to match the dummy predictions, but note that all the - quantitative metrics are estimated using the predictions that are - adjusted properly using a learned offset (or adjusted using a hold-out - adaptation set, as above). Note also that for the calibration data we - require at least two data points of the same sex in each site to be able - to estimate the variance. Of course, in a real example, you would want - many more than just two since we need to get a reliable estimate of the - variance for each site. +**Note:** it is necessary to update the intercept for each of the sites. +For purposes of visualisation, here we do this by adjusting the median +of the data to match the dummy predictions, but note that all the +quantitative metrics are estimated using the predictions that are +adjusted properly using a learned offset (or adjusted using a hold-out +adaptation set, as above). Note also that for the calibration data we +require at least two data points of the same sex in each site to be able +to estimate the variance. Of course, in a real example, you would want +many more than just two since we need to get a reliable estimate of the +variance for each site. .. code:: ipython3 sns.set(style='whitegrid') - for idp_num, idp in enumerate(idp_ids): + for idp_num, idp in enumerate(idp_ids): print('Running IDP', idp_num, idp, ':') idp_dir = os.path.join(out_dir, idp) os.chdir(idp_dir) - + # load the true data points yhat_te = load_2d(os.path.join(idp_dir, 'yhat_predict.txt')) s2_te = load_2d(os.path.join(idp_dir, 'ys2_predict.txt')) y_te = load_2d(os.path.join(idp_dir, 'resp_te.txt')) - + # set up the covariates for the dummy data print('Making predictions with dummy covariates (for visualisation)') - yhat, s2 = predict(cov_file_dummy, - alg = 'blr', - respfile = None, - model_path = os.path.join(idp_dir,'Models'), + yhat, s2 = predict(cov_file_dummy, + alg = 'blr', + respfile = None, + model_path = os.path.join(idp_dir,'Models'), outputsuffix = '_dummy') - + # load the normative model with open(os.path.join(idp_dir,'Models', 'NM_0_0_estimate.pkl'), 'rb') as handle: - nm = pickle.load(handle) - + nm = pickle.load(handle) + # get the warp and warp parameters W = nm.blr.warp - warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] - + warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] + # first, we warp predictions for the true data and compute evaluation metrics med_te = W.warp_predictions(np.squeeze(yhat_te), np.squeeze(s2_te), warp_param)[0] med_te = med_te[:, np.newaxis] print('metrics:', evaluate(y_te, med_te)) - + # then, we warp dummy predictions to create the plots med, pr_int = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2), warp_param) - + # extract the different variance components to visualise beta, junk1, junk2 = nm.blr._parse_hyps(nm.blr.hyp, X_dummy) s2n = 1/beta # variation (aleatoric uncertainty) s2s = s2-s2n # modelling uncertainty (epistemic uncertainty) - + # plot the data points y_te_rescaled_all = np.zeros_like(y_te) for sid, site in enumerate(site_ids_te): - # plot the true test data points + # plot the true test data points if all(elem in site_ids_tr for elem in site_ids_te): # all data in the test set are present in the training set - + # first, we select the data points belonging to this particular site idx = np.where(np.bitwise_and(X_te[:,2] == sex, X_te[:,sid+len(cols_cov)+1] !=0))[0] if len(idx) == 0: print('No data for site', sid, site, 'skipping...') continue - + # then directly adjust the data idx_dummy = np.bitwise_and(X_dummy[:,1] > X_te[idx,1].min(), X_dummy[:,1] < X_te[idx,1].max()) y_te_rescaled = y_te[idx] - np.median(y_te[idx]) + np.median(med[idx_dummy]) else: - # we need to adjust the data based on the adaptation dataset - + # we need to adjust the data based on the adaptation dataset + # first, select the data point belonging to this particular site idx = np.where(np.bitwise_and(X_te[:,2] == sex, (df_te['site'] == site).to_numpy()))[0] - + # load the adaptation data y_ad = load_2d(os.path.join(idp_dir, 'resp_ad.txt')) X_ad = load_2d(os.path.join(idp_dir, 'cov_bspline_ad.txt')) @@ -558,19 +459,19 @@ warping function. This can be done using the function if len(idx) < 2 or len(idx_a) < 2: print('Insufficent data for site', sid, site, 'skipping...') continue - + # adjust and rescale the data - y_te_rescaled, s2_rescaled = nm.blr.predict_and_adjust(nm.blr.hyp, - X_ad[idx_a,:], - np.squeeze(y_ad[idx_a]), - Xs=None, + y_te_rescaled, s2_rescaled = nm.blr.predict_and_adjust(nm.blr.hyp, + X_ad[idx_a,:], + np.squeeze(y_ad[idx_a]), + Xs=None, ys=np.squeeze(y_te[idx])) # plot the (adjusted) data points plt.scatter(X_te[idx,1], y_te_rescaled, s=4, color=clr, alpha = 0.1) - + # plot the median of the dummy data plt.plot(xx, med, clr) - + # fill the gaps in between the centiles junk, pr_int25 = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2), warp_param, percentiles=[0.25,0.75]) junk, pr_int95 = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2), warp_param, percentiles=[0.05,0.95]) @@ -578,14 +479,14 @@ warping function. This can be done using the function plt.fill_between(xx, pr_int25[:,0], pr_int25[:,1], alpha = 0.1,color=clr) plt.fill_between(xx, pr_int95[:,0], pr_int95[:,1], alpha = 0.1,color=clr) plt.fill_between(xx, pr_int99[:,0], pr_int99[:,1], alpha = 0.1,color=clr) - + # make the width of each centile proportional to the epistemic uncertainty junk, pr_int25l = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2-0.5*s2s), warp_param, percentiles=[0.25,0.75]) junk, pr_int95l = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2-0.5*s2s), warp_param, percentiles=[0.05,0.95]) junk, pr_int99l = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2-0.5*s2s), warp_param, percentiles=[0.01,0.99]) junk, pr_int25u = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2+0.5*s2s), warp_param, percentiles=[0.25,0.75]) junk, pr_int95u = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2+0.5*s2s), warp_param, percentiles=[0.05,0.95]) - junk, pr_int99u = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2+0.5*s2s), warp_param, percentiles=[0.01,0.99]) + junk, pr_int99u = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2+0.5*s2s), warp_param, percentiles=[0.01,0.99]) plt.fill_between(xx, pr_int25l[:,0], pr_int25u[:,0], alpha = 0.3,color=clr) plt.fill_between(xx, pr_int95l[:,0], pr_int95u[:,0], alpha = 0.3,color=clr) plt.fill_between(xx, pr_int99l[:,0], pr_int99u[:,0], alpha = 0.3,color=clr) @@ -600,14 +501,14 @@ warping function. This can be done using the function plt.plot(xx, pr_int95[:,1],color=clr, linewidth=0.5) plt.plot(xx, pr_int99[:,0],color=clr, linewidth=0.5) plt.plot(xx, pr_int99[:,1],color=clr, linewidth=0.5) - + plt.xlabel('Age') - plt.ylabel(idp) + plt.ylabel(idp) plt.title(idp) plt.xlim((0,90)) plt.savefig(os.path.join(idp_dir, 'centiles_' + str(sex)), bbox_inches='tight') plt.show() - + os.chdir(out_dir) @@ -622,7 +523,7 @@ warping function. This can be done using the function -.. image:: apply_normative_models_files/apply_normative_models_29_1.png +.. image:: apply_normative_models_files/apply_normative_models_ct_27_1.png .. parsed-literal:: @@ -636,7 +537,7 @@ warping function. This can be done using the function -.. image:: apply_normative_models_files/apply_normative_models_29_3.png +.. image:: apply_normative_models_files/apply_normative_models_ct_27_3.png .. parsed-literal:: @@ -650,24 +551,23 @@ warping function. This can be done using the function -.. image:: apply_normative_models_files/apply_normative_models_29_5.png +.. image:: apply_normative_models_files/apply_normative_models_ct_27_5.png .. code:: ipython3 # explore an example output folder of a single model (one ROI) - # think about what each of these output files represents. + # think about what each of these output files represents. # Hint: look at the variable names and comments in the code block above ! ls rh_MeanThickness_thickness/ .. parsed-literal:: - centiles_1.png MSLL_predict.txt RMSE_predict.txt yhat_predict.txt - cov_bspline_ad.txt pRho_predict.txt sitenum_ad.txt ys2_dummy.pkl - cov_bspline_te.txt resp_ad.txt sitenum_te.txt ys2_predict.txt - EXPV_predict.txt resp_te.txt SMSE_predict.txt Z_predict.txt - Models Rho_predict.txt yhat_dummy.pkl + centiles_1.png Models Rho_predict.txt SMSE_predict.txt ys2_predict.txt + cov_bspline_ad.txt pRho_predict.txt RMSE_predict.txt yhat_dummy.pkl Z_predict.txt + cov_bspline_te.txt resp_ad.txt sitenum_ad.txt yhat_predict.txt + EXPV_predict.txt resp_te.txt sitenum_te.txt ys2_dummy.pkl .. code:: ipython3 @@ -698,7 +598,8 @@ into the original data file. .. code:: ipython3 - z_dir = '/content/braincharts/models/lifespan_57K_82sites/deviation_scores/' + z_dir = os.path.join(root_dir, 'models', model_name, 'deviation_scores') + filelist = [name for name in os.listdir(z_dir)] .. code:: ipython3 diff --git a/doc/source/pages/apply_normative_models_files/apply_normative_models_29_1.png b/doc/source/pages/apply_normative_models_files/apply_normative_models_29_1.png deleted file mode 100644 index 0249f536..00000000 Binary files a/doc/source/pages/apply_normative_models_files/apply_normative_models_29_1.png and /dev/null differ diff --git a/doc/source/pages/apply_normative_models_files/apply_normative_models_29_3.png b/doc/source/pages/apply_normative_models_files/apply_normative_models_29_3.png deleted file mode 100644 index 37a06931..00000000 Binary files a/doc/source/pages/apply_normative_models_files/apply_normative_models_29_3.png and /dev/null differ diff --git a/doc/source/pages/apply_normative_models_files/apply_normative_models_29_5.png b/doc/source/pages/apply_normative_models_files/apply_normative_models_29_5.png deleted file mode 100644 index 97bb0ba9..00000000 Binary files a/doc/source/pages/apply_normative_models_files/apply_normative_models_29_5.png and /dev/null differ diff --git a/doc/source/pages/apply_normative_models_files/apply_normative_models_ct_27_1.png b/doc/source/pages/apply_normative_models_files/apply_normative_models_ct_27_1.png new file mode 100644 index 00000000..2258d6a2 Binary files /dev/null and b/doc/source/pages/apply_normative_models_files/apply_normative_models_ct_27_1.png differ diff --git a/doc/source/pages/apply_normative_models_files/apply_normative_models_ct_27_3.png b/doc/source/pages/apply_normative_models_files/apply_normative_models_ct_27_3.png new file mode 100644 index 00000000..ed4ca34a Binary files /dev/null and b/doc/source/pages/apply_normative_models_files/apply_normative_models_ct_27_3.png differ diff --git a/doc/source/pages/apply_normative_models_files/apply_normative_models_ct_27_5.png b/doc/source/pages/apply_normative_models_files/apply_normative_models_ct_27_5.png new file mode 100644 index 00000000..38931baf Binary files /dev/null and b/doc/source/pages/apply_normative_models_files/apply_normative_models_ct_27_5.png differ diff --git a/doc/source/pages/installation.rst b/doc/source/pages/installation.rst index ce1551ce..18e50880 100644 --- a/doc/source/pages/installation.rst +++ b/doc/source/pages/installation.rst @@ -20,13 +20,19 @@ Basic installation (on a local machine) source activate -4. Install required conda packages +4. Install torch using the torch instructions. + +.. code-block:: bash + + # Command found on the torch website: https://pytorch.org/get-started/locally/ + +5. Install required conda packages .. code-block:: bash - conda install pip pandas scipy + conda install numba nutpie -c conda-forge -5. Install PCNtoolkit (plus dependencies) +6. Install PCNtoolkit (plus dependencies) .. code-block:: bash @@ -60,7 +66,8 @@ Alternative installation (on a shared resource) .. code-block:: bash - conda install -y pandas scipy + # Command found on the torch website: https://pytorch.org/get-started/locally/ + conda install numba nutpie -c conda-forge 5. Install pip dependencies diff --git a/doc/source/pages/normative_modelling_walkthrough.ipynb b/doc/source/pages/normative_modelling_walkthrough.ipynb new file mode 100644 index 00000000..6d8e4fdd --- /dev/null +++ b/doc/source/pages/normative_modelling_walkthrough.ipynb @@ -0,0 +1,648 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hC8rsih7PHa_" + }, + "source": [ + "# **DEMO ON NORMATIVE MODELING**\n", + "\n", + "\n", + "Created by\n", + "\n", + "Mariam Zabihi [@m_zabihi](https://twitter.com/m_zabihi)\n", + "\n", + "Saige Rutherford [@being_saige](https://twitter.com/being_saige)\n", + "\n", + "Thomas Wolfers [@ThomasWolfers](https://twitter.com/ThomasWolfers)\n", + "_______________________________________________________________________________" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "irl08XE1AG9v" + }, + "source": [ + "## **Background Story**\n", + "\n", + "Morten and Ingrid are concerned about the health of their father, Nordan. He recently turned 65 years. A few months ago he could not find his way home. Together, they visit a neurologist/psychiatrist to conduct a number of cognitive tests. However, those tests were inconclusive. While Nordan has a relatively low IQ it could not explain his trouble returning home.\n", + "\n", + "Recently, the family heard about a new screening technique called normative modeling with which one can place individuals in reference to a population norm on for instance measures such as brain volume. Nordan would like to undertake this procedure to better know what is going on and to potentially find targets for treatment. Therefore, the family booked an appointment with you, the normative modeling specialist. To find out what is going on you compare Nordan's hyppocampus to the norm and to a group of persons with Dementia disorders, who have a similar IQ, age as well as the same sex as Nordan.\n", + "\n", + "Do your best to get as far as you can. However, you do not need to feel bad if you cannot complete everything during the tutorial.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "udo6yANOCpvp" + }, + "source": [ + "## **Task 0:** Load data and install PCNtoolkit" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "40K_mZuBZB82" + }, + "outputs": [], + "source": [ + "!pip install pcntoolkit\n", + "!pip install nutpie" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EHDKe2ohCxP4" + }, + "source": [ + "**Option 1:** Connect your Google Drive account, and load data from Google Drive. Having Google Drive connected will allow you to save any files created back to your Drive folder. This step will require you to download the csv files from [Github](https://github.com/predictive-clinical-neuroscience/PCNtoolkit-demo/tree/main/data) to your computer, and then make a folder in your Google Drive account and upload the csv files to this folder." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0SMVyxNZqmlv" + }, + "outputs": [], + "source": [ + "from google.colab import drive\n", + "drive.mount('/content/drive')\n", + "\n", + "#change dir to data on your google drive\n", + "import os\n", + "os.chdir('drive/My Drive/name-of-folder-where-you-uploaded-csv-files-from-Github/') #Change this path to match the path to your data in Google Drive\n", + "\n", + "# code by T. Wolfers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Bst55nPJDHKb" + }, + "source": [ + "**Option 2:** Import the files directly from Github, and skip adding them to Google Drive." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zuuSkJwPDRrv" + }, + "outputs": [], + "source": [ + "!wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_demographics.csv\n", + "!wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_demographics_nordan.csv\n", + "!wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_features.csv\n", + "!wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_features_nordan.csv\n", + "\n", + "# code by S. Rutherford" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kvSiRjysuGkV" + }, + "source": [ + "## **TASK 1:** Format input data" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "N2Bon1mJAVjJ" + }, + "source": [ + "You have four files. The features and demographics file for the normsample and two files of the same name for Nordan your test sample. As one of your coworkers has done the preporcessing and quality control there are more subjects in the demographics file than in the features file of the norm sample. Please select the overlap of participants between those two files.\n", + "\n", + "\n", + "*Question for your understanding:*\n", + "\n", + "1) Why do we have to select the overlap between participants in terms of featrues and demographics?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_RSfxGWku6fU" + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "# read in the files.\n", + "norm_demographics = pd.read_csv('camcan_demographics.csv',\n", + " sep= \",\",\n", + " index_col = 0)\n", + "norm_features = pd.read_csv('camcan_features.csv',\n", + " sep=\",\",\n", + " index_col = 0)\n", + "\n", + "# check columns through print [there are other better options]\n", + "print(norm_demographics)\n", + "print(norm_features)\n", + "\n", + "# find overlap in terms of participants between norm_sample_features and\n", + "# norm_sample_demographics\n", + "\n", + "norm_demographics_features = pd.concat([norm_demographics, norm_features],\n", + " axis = 1,\n", + " join = 'inner') # inner checks overlap\n", + " # outer combines\n", + "print(norm_demographics_features)\n", + "\n", + "# code by T. Wolfers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fUufLg4lQWdn" + }, + "source": [ + "## **TASK 2:** Prepare the covariate_normsample and testresponse_normsample file." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "g1i1qp7AAh1Q" + }, + "source": [ + "As mentioned in the introductory presentation those files need a specific format and the entries need to be seperated by spaces. Use whatever method you know to prepare those files based on the data provided in TASK 1. Save those files in .txt format in your drive. Also get rid of the column names and participant IDs.\n", + "\n", + "Given that we only have limited time in this practical we have to make a selection for the features based on your prior knowledge. With the information in mind that Nordan does not remember his way home, which subfield of the hyppocampus is probably a good target for the investigations?\n", + "Select a maximum of four hyppocampal regions as features.\n", + "\n", + "NOTE: Normative modeling is a screening tool we just make this selection due to time constraints, in reality we build these models on millions of putative biomarkers that are not restricted to brain imaging.\n", + "\n", + "\n", + "*Qestions for your understanding:*\n", + "\n", + "2) What is the requirement for the features in terms of variable properties (e.g. dicotomous or continous)? 3) What is the requirement for the covariates in terms of these properties? 4) What are the requirements for both together? 5) How does this depent on the algorithm used?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "lzt6llxyRPyY" + }, + "outputs": [], + "source": [ + "# perpare covariate_normsample for sex and age\n", + "covariate_normsample = norm_demographics_features[['sex',\n", + " 'age']]\n", + "\n", + "covariate_normsample.to_csv('covariate_normsample.txt',\n", + " sep = ' ',\n", + " header = False,\n", + " index = False)\n", + "\n", + "# perpare features_normsample for relevant hyppocampal subfields\n", + "features_normsample = norm_demographics_features[['left_CA1',\n", + " 'left_CA3',\n", + " 'right_CA1',\n", + " 'right_CA3']]\n", + "\n", + "features_normsample.to_csv('features_normsample.txt',\n", + " sep = ' ',\n", + " header = False,\n", + " index = False)\n", + "\n", + "# code by T. Wolfers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "irR4FAIvQ8ds" + }, + "source": [ + "## **TASK 3:** Estimate normative model\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XV61hQUoA1Kd" + }, + "source": [ + "Once you have prepared and saved all the necessary files. Look at the pcntoolkit for running normative modeling. Select an appropritate method set up the toolkit and run your analyses using 2-fold cross validation in the normsample. Change the output suffix from estimate to '_2fold'.\n", + "\n", + "HINT: You primarily need the estimate function.\n", + "\n", + "SUGGESTION: While this process is running you can go to the next TASK 4, you will have no doubt when it is correctly running.\n", + "\n", + "*Question for your understaning:*\n", + "\n", + "6) What does cvfolds mean and why do we use it? 7) What is the output of the estimate function and what does it mean?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yRTusEg6SRNL" + }, + "outputs": [], + "source": [ + "import pcntoolkit as pcn\n", + "\n", + "# run normative modeling using 2-fold cross-validation\n", + "\n", + "pcn.normative.estimate(covfile = 'covariate_normsample.txt',\n", + " respfile = 'features_normsample.txt',\n", + " cvfolds = 2,\n", + " alg = 'gpr',\n", + " outputsuffix = '_2fold')\n", + "\n", + "# code by T. Wolfers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Nonuk7d_SNM6" + }, + "source": [ + "## **TASK 4:** Estimate the forward model of the normative model\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fmn4TD_tBE70" + }, + "source": [ + "In order to visulize the normative trajectories you first need to run the forward model. To this end you need to set up an appropriate covariate_forwardmodel file that covers the age range appropriately for both sexes. Save this file as .txt . Then you can input the files you made in TASK 1 as well as the file you made now and run the forward model using the appropriate specifications.\n", + "\n", + "*Question for your understaning:*\n", + "\n", + "8) What is yhat and ys2? 9) Why does the output of the forward model does not inlcude the Z-scores?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "22U-knkWSPsZ" + }, + "outputs": [], + "source": [ + "# create covariate_forwardmodel.txt file\n", + "covariate_forwardmodel = {'sex': [0, 0, 0, 0, 0, 0, 0,\n", + " 1, 1, 1, 1, 1, 1, 1],\n", + " 'age': [20, 30, 40, 50, 60, 70, 80,\n", + " 20, 30, 40, 50, 60, 70, 80]}\n", + "covariate_forwardmodel = pd.DataFrame(data=covariate_forwardmodel)\n", + "\n", + "covariate_forwardmodel.to_csv('covariate_forwardmodel.txt',\n", + " sep = ' ',\n", + " header = False,\n", + " index = False)\n", + "\n", + "# estimate forward model\n", + "pcn.normative.estimate(covfile = 'covariate_normsample.txt',\n", + " respfile = 'features_normsample.txt',\n", + " testcov = 'covariate_forwardmodel.txt',\n", + " cvfolds = None,\n", + " alg = 'gpr',\n", + " outputsuffix = '_forward')\n", + "\n", + "# code by T. Wolfers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wxeZlXshQ7eS" + }, + "source": [ + "## **TASK 5:** Visualize forward model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BVodlChrBg25" + }, + "source": [ + "Visualize the forward model of the normative model similar to the figure below.\n", + "\n", + "![1-s2.0-S245190221830329X-gr2.jpg](data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAcQBxAAD/4QDoRXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAAcAAAAcgEyAAIAAAAUAAAAjodpAAQAAAABAAAAogAAAAAAAABxAAAAAQAAAHEAAAABQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzADIwMTk6MDE6MTAgMjE6MDA6MDYAAAOQBAACAAAAFAAAAMygAgAEAAAAAQAAAMmgAwAEAAAAAQAAAHIAAAAAMjAxODoxMjoxMSAxNDozNToxNwD/4RGtaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQuMCI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFNTg4MUU5QkVDMTRFOTExOEI3RkU1RDU4RURGM0Q0RSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyRUM1RUY5ODQ5RkRFODExQTNBM0EyODcyMEY2OTA2NCIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjJFQzVFRjk4NDlGREU4MTFBM0EzQTI4NzIwRjY5MDY0IiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0xMFQyMTowMDowNiswNTozMCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDEtMTBUMjE6MDA6MDYrMDU6MzAiIHhtcDpDcmVhdGVEYXRlPSIyMDE4LTEyLTExVDE0OjM1OjE3KzAxOjAwIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0iRUNJLVJHQi5pY2MiIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIGRjOmZvcm1hdD0iaW1hZ2UvanBlZyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjpvcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MkVDNUVGOTg0OUZERTgxMUEzQTNBMjg3MjBGNjkwNjQiIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RTQ4ODFFOUJFQzE0RTkxMThCN0ZFNUQ1OEVERjNENEUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkVDNUVGOTg0OUZERTgxMUEzQTNBMjg3MjBGNjkwNjQiLz4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiBzdEV2dDp3aGVuPSIyMDE4LTEyLTExVDE0OjM1OjE3KzAxOjAwIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjJFQzVFRjk4NDlGREU4MTFBM0EzQTI4NzIwRjY5MDY0IiBzdEV2dDphY3Rpb249ImNyZWF0ZWQiLz4gPHJkZjpsaSBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0OndoZW49IjIwMTktMDEtMTBUMjA6NTE6MDErMDU6MzAiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6N0QzQ0MyOERFQTE0RTkxMTlFRTJGRkMzQkZENTkzMUUiIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiLz4gPHJkZjpsaSBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0OndoZW49IjIwMTktMDEtMTBUMjA6NTE6MDErMDU6MzAiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6N0UzQ0MyOERFQTE0RTkxMTlFRTJGRkMzQkZENTkzMUUiIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiLz4gPHJkZjpsaSBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0OndoZW49IjIwMTktMDEtMTBUMjE6MDA6MDYrMDU6MzAiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6RTQ4ODFFOUJFQzE0RTkxMThCN0ZFNUQ1OEVERjNENEUiIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNvbnZlcnRlZCIgc3RFdnQ6cGFyYW1ldGVycz0iZnJvbSBpbWFnZS90aWZmIHRvIGltYWdlL2pwZWciLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImRlcml2ZWQiIHN0RXZ0OnBhcmFtZXRlcnM9ImNvbnZlcnRlZCBmcm9tIGltYWdlL3RpZmYgdG8gaW1hZ2UvanBlZyIvPiA8cmRmOmxpIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDUzUgV2luZG93cyIgc3RFdnQ6Y2hhbmdlZD0iLyIgc3RFdnQ6d2hlbj0iMjAxOS0wMS0xMFQyMTowMDowNiswNTozMCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpFNTg4MUU5QkVDMTRFOTExOEI3RkU1RDU4RURGM0Q0RSIgc3RFdnQ6YWN0aW9uPSJzYXZlZCIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/PgD/7QBkUGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAACwcAVoAAxslRxwCAAACAAIcAj4ACDIwMTgxMjExHAI/AAsxNDM1MTcrMDEwMDhCSU0EJQAAAAAAEG8VJXtBp2YwhKsNALMYCvf/4gIsSUNDX1BST0ZJTEUAAQEAAAIcQURCRQIQAABtbnRyUkdCIFhZWiAHzwACABYADAADAA9hY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAACRkZXNjAAABIAAAAGd3dHB0AAABiAAAABRia3B0AAABnAAAABRyVFJDAAABsAAAAA5nVFJDAAABwAAAAA5iVFJDAAAB0AAAAA5yWFlaAAAB4AAAABRnWFlaAAAB9AAAABRiWFlaAAACCAAAABR0ZXh0AAAAAChjKSAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jLgBkZXNjAAAAAAAAAAxFQ0ktUkdCLmljYwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAAD23AABAAAAANM6WFlaIAAAAAAAAAAAAAAAAAAAAABjdXJ2AAAAAAAAAAEBzQAAY3VydgAAAAAAAAABAc0AAGN1cnYAAAAAAAAAAQHNAABYWVogAAAAAAAApngAAFH+AAAA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)\n", + "\n", + "HINT: First create a function that calculates the confidence intervals and then plot yhat, y2 of the forward model. Finally, plot the data of individual participants." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "ii0H9GDwv-ha" + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "# confidence interval calculation at x_forward\n", + "def confidence_interval(s2,x,z):\n", + " CI=np.zeros((len(x_forward),4))\n", + " for i,xdot in enumerate(x_forward):\n", + " ci_inx=np.isin(x,xdot)\n", + " S2=s2[ci_inx]\n", + " S_hat=np.mean(S2,axis=0)\n", + " n=S2.shape[0]\n", + " CI[i,:]=z*np.power(S_hat/n,.5)\n", + " return CI\n", + "\n", + "\n", + "feature_names=['left_CA1','left_CA3','right_CA1','right_CA3']\n", + "sex_covariates=[ 'Female','Male']\n", + "# Creating plots for Female and male\n", + "for i,sex in enumerate(sex_covariates):\n", + "#forward model data\n", + " forward_yhat = pd.read_csv('yhat_forward.txt', sep = ' ', header=None)\n", + " yhat_forward=forward_yhat.values\n", + " yhat_forward=yhat_forward[7*i:7*(i+1)]\n", + " x_forward=[20, 30, 40, 50, 60, 70, 80]\n", + "\n", + "# Find the index of the data exclusively for one sex. Female:0, Male: 1\n", + " inx=np.where(covariate_normsample.sex==i)[0]\n", + " x=covariate_normsample.values[inx,1]\n", + "# actual data\n", + " y = pd.read_csv('features_normsample.txt', sep = ' ', header=None)\n", + " y=y.values[inx]\n", + "# confidence Interval yhat+ z *(std/n^.5)-->.95 % CI:z=1.96, 99% CI:z=2.58\n", + " s2= pd.read_csv('ys2_2fold.txt', sep = ' ', header=None)\n", + " s2=s2.values[inx]\n", + "\n", + " CI_95=confidence_interval(s2,x,1.96)\n", + " CI_99=confidence_interval(s2,x,2.58)\n", + "\n", + "# Creat a trejactroy for each point\n", + " for j,name in enumerate(feature_names):\n", + " fig=plt.figure()\n", + " ax=fig.add_subplot(111)\n", + " ax.plot(x_forward,yhat_forward[:,j], linewidth=4, label='Normative trejactory')\n", + "\n", + "\n", + " ax.plot(x_forward,CI_95[:,j]+yhat_forward[:,j], linewidth=2,linestyle='--',c='g', label='95% confidence interval')\n", + " ax.plot(x_forward,-CI_95[:,j]+yhat_forward[:,j], linewidth=2,linestyle='--',c='g')\n", + "\n", + " ax.plot(x_forward,CI_99[:,j]+yhat_forward[:,j], linewidth=1,linestyle='--',c='k', label='99% confidence interval')\n", + " ax.plot(x_forward,-CI_99[:,j]+yhat_forward[:,j], linewidth=1,linestyle='--',c='k')\n", + "\n", + " ax.scatter(x,y[:,j],c='r', label=name)\n", + " plt.legend(loc='upper left')\n", + " plt.title('Normative trejectory of' +name+' in '+sex+' cohort')\n", + " plt.show()\n", + " plt.close()\n", + "\n", + "# code by M. Zabihi" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "yM4z1BtyWwiF" + }, + "source": [ + "## **TASK 6:** Apply the normative model to Nordan's data and the dementia patients." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "eVTYxKjvWBvm" + }, + "outputs": [], + "source": [ + "# read in Nordan's as well as the patient's demographics and features\n", + "demographics_nordan = pd.read_csv('camcan_demographics_nordan.csv',\n", + " sep= \",\",\n", + " index_col = 0)\n", + "features_nordan = pd.read_csv('camcan_features_nordan.csv',\n", + " sep=\",\",\n", + " index_col = 0)\n", + "\n", + "# create a covariate file for Nordan's as well as the patient's demograhpics\n", + "covariate_nordan = demographics_nordan[['sex',\n", + " 'age']]\n", + "covariate_nordan.to_csv('covariate_nordan.txt',\n", + " sep = ' ',\n", + " header = False,\n", + " index = False)\n", + "\n", + "# create the corresponding feature file\n", + "features_nordan = features_nordan[['left_CA1',\n", + " 'left_CA3',\n", + " 'right_CA1',\n", + " 'right_CA3']]\n", + "\n", + "features_nordan.to_csv('features_nordan.txt',\n", + " sep = ' ',\n", + " header = False,\n", + " index = False)\n", + "\n", + "# apply normative modeling\n", + "pcn.normative.estimate(covfile = 'covariate_normsample.txt',\n", + " respfile = 'features_normsample.txt',\n", + " testcov = 'covariate_nordan.txt',\n", + " testresp = 'features_nordan.txt',\n", + " cvfolds = None,\n", + " alg = 'gpr',\n", + " outputsuffix = '_nordan')\n", + "\n", + "# code by T. Wolfers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "LFnHCy0XVVwl" + }, + "source": [ + "## **TASK 7:** In which hyppocampal subfield(s) does Nordan deviate extremely?\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "jUhmPAOZB0kp" + }, + "source": [ + "No coding necessary just create a presentation which includes recommendations to Nordan and his family.\n", + "Use i) |Z| > 3.6 ii) |Z| > 1.96 as definitions for extreme normative deviations." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AqQhxN9pEFGC" + }, + "source": [ + "## **TASK 8 (OPTIONAL):** Implement a function that calculates percentage change." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "weASKkZNBMW5" + }, + "source": [ + "Percentage change = $\\frac{x1 - x2}{|x2|}*100$" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "0vIt9fd7EmJx" + }, + "outputs": [], + "source": [ + "# function that calculates percentage change\n", + "def calculate_percentage_change(x1, x2):\n", + " percentage_change = ((x1 - x2) / abs(x2)) * 100\n", + " return percentage_change\n", + "\n", + "# code by T. Wolfers" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1Mypo4xrT7ID" + }, + "source": [ + "## **TASK 9 (OPTIONAL):** Visualize percent change\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1I1Kwv5iBUJj" + }, + "source": [ + "Plot the prercentage change in Yhat of the forward model in reference to age 20. Do that for both sexes seperately." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1DoJid7R1DBX", + "scrolled": true + }, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "forward_yhat = pd.read_csv('yhat_forward.txt', sep = ' ', header=None)\n", + "\n", + "# You can indicate here which hypocampal subfield you like to visualize\n", + "hyppocampal_subfield = 0\n", + "\n", + "percentage_change_female = []\n", + "percentage_change_male = []\n", + "count = 0\n", + "lengths = len(forward_yhat[hyppocampal_subfield])\n", + "for entry in forward_yhat[hyppocampal_subfield]:\n", + " if count > 0 and count < 7:\n", + " loop_percentage_change_female = calculate_percentage_change(entry,\n", + " forward_yhat.iloc[0,\n", + " hyppocampal_subfield])\n", + " percentage_change_female.append(loop_percentage_change_female)\n", + " elif count > 7:\n", + " loop_percentage_change_male = calculate_percentage_change(entry,\n", + " forward_yhat.iloc[9,\n", + " hyppocampal_subfield])\n", + " percentage_change_male.append(loop_percentage_change_male)\n", + " count = count + 1\n", + "\n", + "names = ['30 compared to 20 years',\n", + " '40 compared to 20 years',\n", + " '50 compared to 20 years',\n", + " '60 compared to 20 years',\n", + " '70 compared to 20 years',\n", + " '80 compared to 20 years']\n", + "\n", + "# females\n", + "plt.subplot(121)\n", + "plt.bar(names, percentage_change_female)\n", + "plt.xticks(rotation=90)\n", + "plt.ylim(-20, 2)\n", + "\n", + "# males\n", + "plt.subplot(122)\n", + "plt.bar(names, percentage_change_male)\n", + "plt.xticks(rotation=90)\n", + "plt.ylim(-20, 2)\n", + "\n", + "# code by T. Wolfers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "D-OZ7GBfZB87" + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "colab": { + "name": "normative_modelling_walkthrough.ipynb", + "provenance": [], + "toc_visible": true, + "include_colab_link": true + }, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.13" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/doc/source/pages/normative_modelling_walkthrough.rst b/doc/source/pages/normative_modelling_walkthrough.rst index f63ec8fa..5bbc326e 100644 --- a/doc/source/pages/normative_modelling_walkthrough.rst +++ b/doc/source/pages/normative_modelling_walkthrough.rst @@ -1,6 +1,4 @@ -.. title:: GPR tutorial - -Gaussian Process Regression +**DEMO ON NORMATIVE MODELING** ============================== Created by @@ -10,14 +8,9 @@ Mariam Zabihi `@m_zabihi `__ Saige Rutherford `@being_saige `__ Thomas Wolfers `@ThomasWolfers `__ -\______________________________________________________________________________\_ - - -.. image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/predictive-clinical-neuroscience/PCNtoolkit-demo/blob/main/tutorials/CPC_2020/normative_modelling_walkthrough.ipynb - +\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_ -Background Story +**Background Story** -------------------- Morten and Ingrid are concerned about the health of their father, @@ -40,13 +33,13 @@ IQ, age as well as the same sex as Nordan. Do your best to get as far as you can. However, you do not need to feel bad if you cannot complete everything during the tutorial. -**Task 0:** Load data and install the pcntoolkit ------------------------------------------------- +**Task 0:** Load data and install PCNtoolkit +-------------------------------------------- .. code:: ipython3 - #install normative modeling - ! pip install pcntoolkit==0.26 + !pip install pcntoolkit + !pip install nutpie **Option 1:** Connect your Google Drive account, and load data from @@ -77,56 +70,8 @@ them to Google Drive. !wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_demographics_nordan.csv !wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_features.csv !wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_features_nordan.csv - - # code by S. Rutherford - - -.. parsed-literal:: - - --2022-02-17 15:03:58-- https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_demographics.csv - Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.110.133, 185.199.108.133, 185.199.111.133, ... - Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.110.133|:443... connected. - HTTP request sent, awaiting response... 200 OK - Length: 17484 (17K) [text/plain] - Saving to: ‘camcan_demographics.csv’ - - camcan_demographics 100%[===================>] 17.07K --.-KB/s in 0.001s - - 2022-02-17 15:03:58 (12.9 MB/s) - ‘camcan_demographics.csv’ saved [17484/17484] - - --2022-02-17 15:03:58-- https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_demographics_nordan.csv - Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ... - Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected. - HTTP request sent, awaiting response... 200 OK - Length: 332 [text/plain] - Saving to: ‘camcan_demographics_nordan.csv’ - - camcan_demographics 100%[===================>] 332 --.-KB/s in 0s - - 2022-02-17 15:03:58 (15.5 MB/s) - ‘camcan_demographics_nordan.csv’ saved [332/332] - - --2022-02-17 15:03:58-- https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_features.csv - Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ... - Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected. - HTTP request sent, awaiting response... 200 OK - Length: 188944 (185K) [text/plain] - Saving to: ‘camcan_features.csv’ - - camcan_features.csv 100%[===================>] 184.52K --.-KB/s in 0.05s - - 2022-02-17 15:03:58 (3.88 MB/s) - ‘camcan_features.csv’ saved [188944/188944] - - --2022-02-17 15:03:58-- https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/master/tutorials/CPC_2020/data/camcan_features_nordan.csv - Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ... - Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|185.199.108.133|:443... connected. - HTTP request sent, awaiting response... 200 OK - Length: 1695 (1.7K) [text/plain] - Saving to: ‘camcan_features_nordan.csv’ - - camcan_features_nor 100%[===================>] 1.66K --.-KB/s in 0s - - 2022-02-17 15:03:59 (25.3 MB/s) - ‘camcan_features_nordan.csv’ saved [1695/1695] + # code by S. Rutherford **TASK 1:** Format input data @@ -160,7 +105,7 @@ between those two files. print(norm_demographics) print(norm_features) - # find overlap in terms of participants between norm_sample_features and + # find overlap in terms of participants between norm_sample_features and # norm_sample_demographics norm_demographics_features = pd.concat([norm_demographics, norm_features], @@ -172,54 +117,6 @@ between those two files. # code by T. Wolfers -.. parsed-literal:: - - age sex_name sex IQ_random - paricipants - CC110033 24 MALE 1 73 - CC110037 18 MALE 1 103 - CC110045 24 FEMALE 0 124 - CC110056 22 FEMALE 0 124 - CC110062 20 MALE 1 126 - ... ... ... ... ... - CC722542 79 MALE 1 116 - CC722651 79 FEMALE 0 128 - CC722891 84 FEMALE 0 129 - CC723197 80 FEMALE 0 96 - CC723395 86 FEMALE 0 145 - - [707 rows x 4 columns] - left_Hippocampal_tail ... right_Whole_hippocampus - participants ... - CC110033 482.768229 ... 3531.764896 - CC110037 595.269259 ... 3835.426137 - CC110045 655.847194 ... 3681.494304 - CC110056 561.345626 ... 3461.373764 - CC110062 756.521166 ... 4782.407821 - ... ... ... ... - CC722542 467.896808 ... 3284.108783 - CC722651 406.326167 ... 3210.272905 - CC722891 393.430481 ... 2423.675065 - CC723197 475.929914 ... 3043.146264 - CC723395 444.301617 ... 2988.001288 - - [651 rows x 26 columns] - age sex_name sex ... right_fimbria right_HATA right_Whole_hippocampus - CC110033 24 MALE 1 ... 87.127463 73.589184 3531.764896 - CC110037 18 MALE 1 ... 99.657823 60.920924 3835.426137 - CC110045 24 FEMALE 0 ... 69.436808 59.323542 3681.494304 - CC110056 22 FEMALE 0 ... 60.505521 51.726283 3461.373764 - CC110062 20 MALE 1 ... 92.215816 85.484454 4782.407821 - ... ... ... ... ... ... ... ... - CC722542 79 MALE 1 ... 46.144212 43.966509 3284.108783 - CC722651 79 FEMALE 0 ... 68.730322 59.699644 3210.272905 - CC722891 84 FEMALE 0 ... 27.913196 38.629828 2423.675065 - CC723197 80 FEMALE 0 ... 51.893458 65.474967 3043.146264 - CC723395 86 FEMALE 0 ... 68.335159 62.081225 2988.001288 - - [650 rows x 30 columns] - - **TASK 2:** Prepare the covariate_normsample and testresponse_normsample file. ------------------------------------------------------------------------------ @@ -252,22 +149,22 @@ putative biomarkers that are not restricted to brain imaging. # perpare covariate_normsample for sex and age covariate_normsample = norm_demographics_features[['sex', - 'age']] + 'age']] covariate_normsample.to_csv('covariate_normsample.txt', sep = ' ', - header = False, + header = False, index = False) # perpare features_normsample for relevant hyppocampal subfields - features_normsample = norm_demographics_features[['left_CA1', + features_normsample = norm_demographics_features[['left_CA1', 'left_CA3', 'right_CA1', 'right_CA3']] - features_normsample.to_csv('features_normsample.txt', - sep = ' ', - header = False, + features_normsample.to_csv('features_normsample.txt', + sep = ' ', + header = False, index = False) # code by T. Wolfers @@ -278,7 +175,7 @@ putative biomarkers that are not restricted to brain imaging. Once you have prepared and saved all the necessary files. Look at the pcntoolkit for running normative modeling. Select an appropritate method set up the toolkit and run your analyses using 2-fold cross validation -in the normsample. Change the output suffix from estimate to ’_2fold’. +in the normsample. Change the output suffix from estimate to ’\_2fold’. HINT: You primarily need the estimate function. @@ -296,7 +193,7 @@ you will have no doubt when it is correctly running. # run normative modeling using 2-fold cross-validation - pcn.normative.estimate(covfile = 'covariate_normsample.txt', + pcn.normative.estimate(covfile = 'covariate_normsample.txt', respfile = 'features_normsample.txt', cvfolds = 2, alg = 'gpr', @@ -307,55 +204,66 @@ you will have no doubt when it is correctly running. .. parsed-literal:: + inscaler: None + outscaler: None Processing data in features_normsample.txt Estimating model 1 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed - Warning: Estimation of posterior distribution failed + + +.. parsed-literal:: + + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:147: RuntimeWarning: overflow encountered in exp + self.sf2 = np.exp(2*theta[self.D]) + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:160: RuntimeWarning: invalid value encountered in multiply + dK = K * squared_dist(x[:, i]/self.ell[i], x[:, i]/self.ell[i]) + + +.. parsed-literal:: + Optimization terminated successfully. - Current function value: 1856.502251 - Iterations: 40 - Function evaluations: 99 - Gradient evaluations: 99 + Current function value: 1925.145213 + Iterations: 30 + Function evaluations: 75 + Gradient evaluations: 69 Estimating model 2 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed - Optimization terminated successfully. - Current function value: 1596.239263 - Iterations: 42 - Function evaluations: 93 - Gradient evaluations: 93 - Estimating model 3 of 4 - Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed - Optimization terminated successfully. - Current function value: 1862.316698 - Iterations: 47 - Function evaluations: 104 - Gradient evaluations: 104 - Estimating model 4 of 4 - Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Optimization terminated successfully. - Current function value: 1587.950935 - Iterations: 30 - Function evaluations: 64 - Gradient evaluations: 64 - Estimating model 1 of 4 + Current function value: 1627.864114 + Iterations: 41 + Function evaluations: 102 + Gradient evaluations: 102 + Estimating model 3 of 4 + + +.. parsed-literal:: + + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:147: RuntimeWarning: overflow encountered in exp + self.sf2 = np.exp(2*theta[self.D]) + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:160: RuntimeWarning: invalid value encountered in multiply + dK = K * squared_dist(x[:, i]/self.ell[i], x[:, i]/self.ell[i]) + + +.. parsed-literal:: + Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Optimization terminated successfully. - Current function value: 1916.461484 - Iterations: 44 - Function evaluations: 94 - Gradient evaluations: 87 - Estimating model 2 of 4 + Current function value: 1922.205071 + Iterations: 30 + Function evaluations: 73 + Gradient evaluations: 67 + Estimating model 4 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -365,36 +273,48 @@ you will have no doubt when it is correctly running. Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Optimization terminated successfully. - Current function value: 1611.661888 - Iterations: 34 - Function evaluations: 85 - Gradient evaluations: 85 - Estimating model 3 of 4 - Warning: Estimation of posterior distribution failed + Current function value: 1621.445961 + Iterations: 78 + Function evaluations: 181 + Gradient evaluations: 181 + Estimating model 1 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Optimization terminated successfully. - Current function value: 1912.665851 - Iterations: 61 - Function evaluations: 133 - Gradient evaluations: 126 - Estimating model 4 of 4 + Current function value: 1844.061877 + Iterations: 36 + Function evaluations: 81 + Gradient evaluations: 81 + Estimating model 2 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed + Optimization terminated successfully. + Current function value: 1580.315780 + Iterations: 37 + Function evaluations: 79 + Gradient evaluations: 79 + Estimating model 3 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed + Optimization terminated successfully. + Current function value: 1851.005493 + Iterations: 32 + Function evaluations: 68 + Gradient evaluations: 68 + Estimating model 4 of 4 + Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Optimization terminated successfully. - Current function value: 1619.045647 - Iterations: 43 - Function evaluations: 110 - Gradient evaluations: 105 + Current function value: 1584.089863 + Iterations: 39 + Function evaluations: 91 + Gradient evaluations: 91 Evaluating the model ... Writing outputs ... @@ -423,13 +343,13 @@ model using the appropriate specifications. 20, 30, 40, 50, 60, 70, 80]} covariate_forwardmodel = pd.DataFrame(data=covariate_forwardmodel) - covariate_forwardmodel.to_csv('covariate_forwardmodel.txt', - sep = ' ', - header = False, + covariate_forwardmodel.to_csv('covariate_forwardmodel.txt', + sep = ' ', + header = False, index = False) # estimate forward model - pcn.normative.estimate(covfile = 'covariate_normsample.txt', + pcn.normative.estimate(covfile = 'covariate_normsample.txt', respfile = 'features_normsample.txt', testcov = 'covariate_forwardmodel.txt', cvfolds = None, @@ -441,8 +361,22 @@ model using the appropriate specifications. .. parsed-literal:: + inscaler: None + outscaler: None Processing data in features_normsample.txt Estimating model 1 of 4 + + +.. parsed-literal:: + + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:147: RuntimeWarning: overflow encountered in exp + self.sf2 = np.exp(2*theta[self.D]) + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:160: RuntimeWarning: invalid value encountered in multiply + dK = K * squared_dist(x[:, i]/self.ell[i], x[:, i]/self.ell[i]) + + +.. parsed-literal:: + Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -451,8 +385,8 @@ model using the appropriate specifications. Optimization terminated successfully. Current function value: 3781.497401 Iterations: 20 - Function evaluations: 61 - Gradient evaluations: 54 + Function evaluations: 58 + Gradient evaluations: 52 Estimating model 2 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -466,10 +400,22 @@ model using the appropriate specifications. Warning: Estimation of posterior distribution failed Optimization terminated successfully. Current function value: 3201.761309 - Iterations: 39 - Function evaluations: 108 - Gradient evaluations: 108 + Iterations: 48 + Function evaluations: 114 + Gradient evaluations: 114 Estimating model 3 of 4 + + +.. parsed-literal:: + + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:147: RuntimeWarning: overflow encountered in exp + self.sf2 = np.exp(2*theta[self.D]) + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:160: RuntimeWarning: invalid value encountered in multiply + dK = K * squared_dist(x[:, i]/self.ell[i], x[:, i]/self.ell[i]) + + +.. parsed-literal:: + Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -480,9 +426,9 @@ model using the appropriate specifications. Warning: Estimation of posterior distribution failed Optimization terminated successfully. Current function value: 3771.310488 - Iterations: 47 - Function evaluations: 181 - Gradient evaluations: 167 + Iterations: 48 + Function evaluations: 156 + Gradient evaluations: 143 Estimating model 4 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -496,9 +442,9 @@ model using the appropriate specifications. Warning: Estimation of posterior distribution failed Optimization terminated successfully. Current function value: 3200.837262 - Iterations: 40 - Function evaluations: 104 - Gradient evaluations: 104 + Iterations: 42 + Function evaluations: 116 + Gradient evaluations: 116 Writing outputs ... @@ -508,7 +454,8 @@ model using the appropriate specifications. Visualize the forward model of the normative model similar to the figure below. -.. figure:: data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAcQBxAAD/4QDoRXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAAcAAAAcgEyAAIAAAAUAAAAjodpAAQAAAABAAAAogAAAAAAAABxAAAAAQAAAHEAAAABQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzADIwMTk6MDE6MTAgMjE6MDA6MDYAAAOQBAACAAAAFAAAAMygAgAEAAAAAQAAAMmgAwAEAAAAAQAAAHIAAAAAMjAxODoxMjoxMSAxNDozNToxNwD/4RGtaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQuMCI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFNTg4MUU5QkVDMTRFOTExOEI3RkU1RDU4RURGM0Q0RSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyRUM1RUY5ODQ5RkRFODExQTNBM0EyODcyMEY2OTA2NCIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjJFQzVFRjk4NDlGREU4MTFBM0EzQTI4NzIwRjY5MDY0IiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0xMFQyMTowMDowNiswNTozMCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDEtMTBUMjE6MDA6MDYrMDU6MzAiIHhtcDpDcmVhdGVEYXRlPSIyMDE4LTEyLTExVDE0OjM1OjE3KzAxOjAwIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0iRUNJLVJHQi5pY2MiIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIGRjOmZvcm1hdD0iaW1hZ2UvanBlZyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjpvcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MkVDNUVGOTg0OUZERTgxMUEzQTNBMjg3MjBGNjkwNjQiIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RTQ4ODFFOUJFQzE0RTkxMThCN0ZFNUQ1OEVERjNENEUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkVDNUVGOTg0OUZERTgxMUEzQTNBMjg3MjBGNjkwNjQiLz4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiBzdEV2dDp3aGVuPSIyMDE4LTEyLTExVDE0OjM1OjE3KzAxOjAwIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjJFQzVFRjk4NDlGREU4MTFBM0EzQTI4NzIwRjY5MDY0IiBzdEV2dDphY3Rpb249ImNyZWF0ZWQiLz4gPHJkZjpsaSBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0OndoZW49IjIwMTktMDEtMTBUMjA6NTE6MDErMDU6MzAiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6N0QzQ0MyOERFQTE0RTkxMTlFRTJGRkMzQkZENTkzMUUiIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiLz4gPHJkZjpsaSBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0OndoZW49IjIwMTktMDEtMTBUMjA6NTE6MDErMDU6MzAiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6N0UzQ0MyOERFQTE0RTkxMTlFRTJGRkMzQkZENTkzMUUiIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiLz4gPHJkZjpsaSBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0OndoZW49IjIwMTktMDEtMTBUMjE6MDA6MDYrMDU6MzAiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6RTQ4ODFFOUJFQzE0RTkxMThCN0ZFNUQ1OEVERjNENEUiIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNvbnZlcnRlZCIgc3RFdnQ6cGFyYW1ldGVycz0iZnJvbSBpbWFnZS90aWZmIHRvIGltYWdlL2pwZWciLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImRlcml2ZWQiIHN0RXZ0OnBhcmFtZXRlcnM9ImNvbnZlcnRlZCBmcm9tIGltYWdlL3RpZmYgdG8gaW1hZ2UvanBlZyIvPiA8cmRmOmxpIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDUzUgV2luZG93cyIgc3RFdnQ6Y2hhbmdlZD0iLyIgc3RFdnQ6d2hlbj0iMjAxOS0wMS0xMFQyMTowMDowNiswNTozMCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpFNTg4MUU5QkVDMTRFOTExOEI3RkU1RDU4RURGM0Q0RSIgc3RFdnQ6YWN0aW9uPSJzYXZlZCIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/PgD/7QBkUGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAACwcAVoAAxslRxwCAAACAAIcAj4ACDIwMTgxMjExHAI/AAsxNDM1MTcrMDEwMDhCSU0EJQAAAAAAEG8VJXtBp2YwhKsNALMYCvf/4gIsSUNDX1BST0ZJTEUAAQEAAAIcQURCRQIQAABtbnRyUkdCIFhZWiAHzwACABYADAADAA9hY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAACRkZXNjAAABIAAAAGd3dHB0AAABiAAAABRia3B0AAABnAAAABRyVFJDAAABsAAAAA5nVFJDAAABwAAAAA5iVFJDAAAB0AAAAA5yWFlaAAAB4AAAABRnWFlaAAAB9AAAABRiWFlaAAACCAAAABR0ZXh0AAAAAChjKSAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jLgBkZXNjAAAAAAAAAAxFQ0ktUkdCLmljYwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAAD23AABAAAAANM6WFlaIAAAAAAAAAAAAAAAAAAAAABjdXJ2AAAAAAAAAAEBzQAAY3VydgAAAAAAAAABAc0AAGN1cnYAAAAAAAAAAQHNAABYWVogAAAAAAAApngAAFH+AAAA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 +.. figure:: + data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAcQBxAAD/4QDoRXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAAcAAAAcgEyAAIAAAAUAAAAjodpAAQAAAABAAAAogAAAAAAAABxAAAAAQAAAHEAAAABQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzADIwMTk6MDE6MTAgMjE6MDA6MDYAAAOQBAACAAAAFAAAAMygAgAEAAAAAQAAAMmgAwAEAAAAAQAAAHIAAAAAMjAxODoxMjoxMSAxNDozNToxNwD/4RGtaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQuMCI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFNTg4MUU5QkVDMTRFOTExOEI3RkU1RDU4RURGM0Q0RSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyRUM1RUY5ODQ5RkRFODExQTNBM0EyODcyMEY2OTA2NCIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjJFQzVFRjk4NDlGREU4MTFBM0EzQTI4NzIwRjY5MDY0IiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wMS0xMFQyMTowMDowNiswNTozMCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTktMDEtMTBUMjE6MDA6MDYrMDU6MzAiIHhtcDpDcmVhdGVEYXRlPSIyMDE4LTEyLTExVDE0OjM1OjE3KzAxOjAwIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0iRUNJLVJHQi5pY2MiIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIGRjOmZvcm1hdD0iaW1hZ2UvanBlZyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjpvcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MkVDNUVGOTg0OUZERTgxMUEzQTNBMjg3MjBGNjkwNjQiIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RTQ4ODFFOUJFQzE0RTkxMThCN0ZFNUQ1OEVERjNENEUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MkVDNUVGOTg0OUZERTgxMUEzQTNBMjg3MjBGNjkwNjQiLz4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpIiBzdEV2dDp3aGVuPSIyMDE4LTEyLTExVDE0OjM1OjE3KzAxOjAwIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjJFQzVFRjk4NDlGREU4MTFBM0EzQTI4NzIwRjY5MDY0IiBzdEV2dDphY3Rpb249ImNyZWF0ZWQiLz4gPHJkZjpsaSBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0OndoZW49IjIwMTktMDEtMTBUMjA6NTE6MDErMDU6MzAiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6N0QzQ0MyOERFQTE0RTkxMTlFRTJGRkMzQkZENTkzMUUiIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiLz4gPHJkZjpsaSBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0OndoZW49IjIwMTktMDEtMTBUMjA6NTE6MDErMDU6MzAiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6N0UzQ0MyOERFQTE0RTkxMTlFRTJGRkMzQkZENTkzMUUiIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiLz4gPHJkZjpsaSBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ1M1IFdpbmRvd3MiIHN0RXZ0OmNoYW5nZWQ9Ii8iIHN0RXZ0OndoZW49IjIwMTktMDEtMTBUMjE6MDA6MDYrMDU6MzAiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6RTQ4ODFFOUJFQzE0RTkxMThCN0ZFNUQ1OEVERjNENEUiIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNvbnZlcnRlZCIgc3RFdnQ6cGFyYW1ldGVycz0iZnJvbSBpbWFnZS90aWZmIHRvIGltYWdlL2pwZWciLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImRlcml2ZWQiIHN0RXZ0OnBhcmFtZXRlcnM9ImNvbnZlcnRlZCBmcm9tIGltYWdlL3RpZmYgdG8gaW1hZ2UvanBlZyIvPiA8cmRmOmxpIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDUzUgV2luZG93cyIgc3RFdnQ6Y2hhbmdlZD0iLyIgc3RFdnQ6d2hlbj0iMjAxOS0wMS0xMFQyMTowMDowNiswNTozMCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpFNTg4MUU5QkVDMTRFOTExOEI3RkU1RDU4RURGM0Q0RSIgc3RFdnQ6YWN0aW9uPSJzYXZlZCIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPD94cGFja2V0IGVuZD0idyI/PgD/7QBkUGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAACwcAVoAAxslRxwCAAACAAIcAj4ACDIwMTgxMjExHAI/AAsxNDM1MTcrMDEwMDhCSU0EJQAAAAAAEG8VJXtBp2YwhKsNALMYCvf/4gIsSUNDX1BST0ZJTEUAAQEAAAIcQURCRQIQAABtbnRyUkdCIFhZWiAHzwACABYADAADAA9hY3NwQVBQTAAAAABub25lAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUFEQkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApjcHJ0AAAA/AAAACRkZXNjAAABIAAAAGd3dHB0AAABiAAAABRia3B0AAABnAAAABRyVFJDAAABsAAAAA5nVFJDAAABwAAAAA5iVFJDAAAB0AAAAA5yWFlaAAAB4AAAABRnWFlaAAAB9AAAABRiWFlaAAACCAAAABR0ZXh0AAAAAChjKSAxOTk5IEFkb2JlIFN5c3RlbXMgSW5jLgBkZXNjAAAAAAAAAAxFQ0ktUkdCLmljYwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAAD23AABAAAAANM6WFlaIAAAAAAAAAAAAAAAAAAAAABjdXJ2AAAAAAAAAAEBzQAAY3VydgAAAAAAAAABAc0AAGN1cnYAAAAAAAAAAQHNAABYWVogAAAAAAAApngAAFH+AAAAAFhZWiAAAAAAAAAtlAAAmiAAABFdWFlaIAAAAAAAACLJAAAT4gAAwdD/wAARCAByAMkDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9sAQwADAgICAgIDAgICAwMDAwQGBAQEBAQIBgYFBgkICgoJCAkJCgwPDAoLDgsJCQ0RDQ4PEBAREAoMEhMSEBMPEBAQ/9sAQwEDAwMEAwQIBAQIEAsJCxAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ/90ABAAa/9oADAMBAAIRAxEAPwD9KKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD//0P0ooAKACgAoAOvSgA74/CgAoATI9aAI2ubdDhp4wfQsKAEN1bH/AJbKfpQAC7tu9wg+poAVbm3f7k8bfRwaAJeoyOlAB060AFABQAUAFABQAUAFABQAUAFABQAUAf/R/SigAoAKADvjHPpQBheNNFHinw7e+Hk1e4sJLxAont8tIuGDfdBGQcEEZHB6ik9iZarQs6Fp91pGjWWj/aJbn7HAlubm6O6SXaANxAP16mjoNKyL/kO/+tuHPsuEH6c/rTGRQpYXG4wtFP5b7GO4SFWHY5zg0CumWFVV4RAO2BxQMcBnj1oAzbLxFoWpand6NY6tbT3thj7TAkm54s/3h7dPai6Emm7GXceOtBi8c2/w/mgum1K5tjcq/k5iwBkjd1zheuMZIGeaTeocyvY6A2trnIgRT6qAp/Mc0xgLZBzHJKnuJCf50AL5U6/cuc/76A/yIoAN10vVY3+jFf8AGgBPPK/6y2lX3wGH6c0AKLq3Jx5qqTxtb5T+RoAl6jI70AJmgBaADmgDmvHsvjmPRUb4fQ2cuom4TeLkgAQ87sZ4z936DOMmkyZX6HRQed5KG4CibaDIE+7ux82PbNMrdD6ACgD/0v0ooABzxQBG06r8iZdv7q9PxNAFe7mS1tJr3UJ1htreNpZNucKijJJPXoD0x+NAm7K5R8JeKNA8X6QuteG7jz7RpGj3GMoQy9QQ3I6g/iKW4JqWxtUxh0PTn0oA5nwf4D0DwEupPpM1yV1K5+1Tm5m3heuAOOnJ5OW5GScUlZCUbECfFHwdJ4suPBg1Ei8toBM0hx5B6HYHBOW5HQY5PcUlLUSkr2NseI9GbBW73g9CIXZT+IWqHc8x8HSeC/AnxFvtH0jRNTxqsRuJdavHPlpn5/KBZRhcgck5LADoAahPUzi+WTZ6amteGpphKmraa0wHlhxPGXx6A5zV3RppuZPifTPGup61oN74V8R29lpltcF9ShZcm5jJXhTg54DDHy/ezkkYpMUtdjqc8c9fWmUNWRHd40dWaMgOoOSpPTPpQA6gAHPSgBCFPDDII/OgCP7Jb5ysYT/c+Q/oRmgBPJlX7ly3sHG4f0P60AGbpesccg9m2n8jmgA+0Kv+shkT/gOcfiKAHpNFLxHKrewPNAD6ACgAwfQ0Af/T/SV5FjALH73RR1NAEfzzErI/ljqUB+bB9T6f5zQBMiqgwi7evSgCO6ktobaaW8aNbeNGaYyY2hAPm3Z4xjOc0MH5nCaV8SfhzBa3WjeBPJvp7G2kuodO060ZRKRyQmFC5LYBOMjIJqbroRzx+yWvAfjDxd410qS+vPCn/CPyRzGIC9MjFhgHKoQhI98gfWhNsak2a+sj+ydHvNc1rVtRuILKBriSK1AiyFBJChMN0HdjVA9DA0Txd8NtS0nR9buYreyfWXaKzTUV3TPIrFSNzZ7/AMWccgUk11C8VqzV0Xxl4P1zTdT8S+FhHqH9nLJFMba32zPsUtsXIBIPYdDnr1waBeO6LXgXxb/wm/hy38Qrpd1p3ns6CG4HzfKcbgeMg+vH0prUcXc3x7cdse31oHYa8Uco2yRq+eMMuf50aBocr43k8P8AhLw7eeJD4TjvXtgp8q0gVZGy2MlgMgDue1JtImTsjEg1PxFe+ItBh0nwxq1vomq2Auri7e+l32rkE7CrsVXHyjBHORjoanUWrZcsfAdp4EvvEHjPS9cv5Jb5XurxLqJbhW25Y7UTY2euBn246U7W1FyqOpk2/jH4i+ONA0jxB8PbfThbyXpjvEvE8tzEp5ADFht9SrE8j0NF2F3LY1LT4h2XjQeI/DFhZ61p95pyvZTXSW3meVI25Q6bSWOCpI47Ci9xqV7oxvhZeeL/AA7qV34R8U6rb3ej2kZNjqN05iuLiQsDt2SMWwMtnIBG0DODSjfqKN07M9QhvbO4lkgt7uGWSLHmIkgZkyMjIHTIq7mhNQAUAHU0AMeGKT/WRhvTcOlADfs4H+qlkT2DZH5NkUAIBdKRkxyj3+U/4fyoArfb73/nwm/OgD//1P0ljhRCXGS54Lnkn+lAHN2HgLTtO8c6h48jv717vULdbeSB5MwooC9B1/hHBJHXFK1tSeVLUj8e+OJPB+m/aNN0K6127WaOKS1tclolbPzOQp2jOMA4ySKG7bDlLlWg3xLrOnaboP8Aa/jzVF0vTZwsTWse4kmQcxuwG9uM5ChRgHOaL9xN2Wpn+GfhJ4L0DxPb+NfC3nWyG18qG2hlzAysv3+cscqcgFiOhFJR1ugjBbo7ODU9NuL2bTbfULWS7tQDPAkwMkYYZBZRyAR3IqrjumYN54V1258eWfimPxVcxaVBbNBLpQX5JWIbk84zyCTjOVFKzE03K/Q43WPDeva58cdOfVNEkufDGl2fnWrtABbwy7Ccg9AxkC8Hngcbealp3Ia9/XY7qO78B+DFe1jv9D0RbiRpmjM8Vv5jnGWwSOTge4xVaI091GBqHx3+FlvBcHS/GGn6zdxI7Ja6Y5u3eQA4X9wr4yeOelF+wr9jh/CXxT8Wf8JDfeJ/HVn4os9LktwkWl2nhfUruOMjbiQSRwFccMc7j1wQMVMXrqQnJO7Op1b456eumi68M+CfGGqSuyYSbw7qFlCUJ+95s0IQ9eADkk57VTfYpy00NGD43eB/lXUl1/SpSuSmoeHdQgAP1aAKw9xRe473RzmoftCeHIPH+maJDr2iW2hTQM13d6jObSRJMHAUTbOny4ABzk9MUr6ibfMdWPjB4Fn8Tab4X03WoNTn1RN0c9hKlxbp1ADOrEAnaenpzT5lsNyWx2caRRr5cSKiDI2qAAPwHSnsUvI8w8Ya74j1H4paF4L8LXlzpQi/4mGoz/Z90N1GAPl3DqAoZecDLAZ4qXe+hm9ZaHY2XjXwzrfiXUvBdvcNNqWnRiS5heI7CMgEBjwcblz7mnctNN2KWk/DDwnouu6n4i06K8hu9WbdJ5V08aL3OzYVxk9ck+2BxRy2FypGjDte8n07S/FgluLTBmtZmjnMWem4DEgz7tSBGX4p8fL4Hn0228RLbTNqk5t7c2+9HZsgElCGUD5hnLj71O9tBOVnY3hrkMPF9Y39p6mSAuo9y8e5cfUjHegq5atNRsL8FrG9gnA4PlSBsexx0phdFigYUALtb+6fyoA//9X9Iby9trKMSXMgBc7UCgs7t6Ko5P4UBcqCLUtS5uHaxtu0KMPOYf7Tg/J/uqc/7XagWrLtta29lCtvaQrDGuQFUfn9T65/WiwWK2saHo/iGybTda0+G8tmYP5Uq7lBHQ/Uf1NFrjdnozg9U+K9t4N8cXPhzxi+jaJoMdojadNJdL59w2FwscP3m6t8qpxjnqKlPUhN3scbqFt4m1/xoPGXwV8K6vZPcsz6jqGtRtYWl4u5CEVJv3+0lfmYQ4PykdKVtbktNvQPiH4p+PFhrGm+GLPWNLttV1hS0Nto+meYEXkc3VyxViCMnEKgDBJINOTYSlJaGZqfw/j1f4teHfCWtavr2tGGzF5q66zezXNrK23cwjQFYgMZXIjUZ7k4ATvzBd8x6JqPwl+B+oeGJhp/h7w1pFgzAnUtMgtoGUhu8wX14O7PXFN2ZckmrnR634f1eLwQNA+H+qR6dd29vFBZ3Ep3AIuAckA8lRjODyc+9O1loHLp7pxVj8MfH2v+JdRn+Inidp9Mm0wWASxmMa3BKgFinRcMCxOBubHGMgyotkRhJ7s9J8MeHrDwnoNn4f01pmtrKMxo0z7nbJzyfxJ7D6dKvyNErIu/2hZfbjp326D7WF84wCUeaE7NtPOM9+lA7q5zlvofjJ/HOoahqWs2l14ZmtVjt9OaIFlkwuScjpkMc5OcgYGAalLUnladx+q/DD4b62HXVvAXh673kEtLpsRbPY525BHqD3qrD5UzzLV/AGn+Adf03TNE1Dx0sfiC82rJpWoGKKxThVDjDCXaG6v0VTz6xZJ6EWUWds3gr4maSfM8P/F2e828GDxDpFvdIQO2+3Fu47ckt+NWXY4zx/dfGrRLCTXIPDvhPSp45Ee/1fTdTQPdoAAkbrcxRkDOODI+eMGpehEubc66D42+HLeGObxXoniLw2kiiT7Tf6Y8lqQeQ32q3MsODkEEuOO9O5SlbcveDrb4X3uv6j418Fa3peoXurjbPNZaglwp5ydoViFOQMigEknc6y80vS9RaB9Q061uTbOJoPOhV/KccBl3AkHHTGPzFFirIs9ucc5z/WmBVu9L0y+IkvLCCZx913jBYfRuopWCxW/sTyebDVb+3x1Uy+cv0xIGIH0IpisLnxFb8EWN+OvG63b8vmUn3yv4UAZP/CXy/wDQla7/AN+j/hQM/9b9D9R03ULbSb+50N459be2f7PcXhyPMIOAccKucfKoxQxNaaGP4G8Wvc6VJpni7XNLfxFpKM+qpDIoWAbjgt/CMAgNg4BJ5qU+4ovuZr/F2LX5ZLH4WeG7vxg8bGJ7+GVbbSonHUNdvkSEdxCshHene4XvsVdX0HxzfaXda38SfiG+k6VaxNPPpnhWNoPkAyQ13IDO/AIzGITn86Qbak/whm+EV5Ddy/DXSbe3uk2G9eSFvtjs2SDLM5Lyng/MWP8ASmmmCkmaHh/xX4ok8S6/p3iWwtFsLacLpn2ItNNImTy6oSV42n5guCSKL6gpO50bXerXOGtdEWLHR7yZUPPcLGH/ACJU0DZyfxDuviFaWdnDoekwasmoT/Zb6O2hKNFAwwSWZjwem7GBjNJ36EzcrEU3wg8DaT4KvPDt9faiuiK7385kucbWVQS+QvZV6Ht70cugnCKVjlfD+gP4h8f+H/Efgq41CfwZaac1mZRqUqGJkV18oqzBx0Q4HGADmptd3QkryViXxhp3iv4dWGveIdI8V3GuStdRXL2NzfSKbG0JbJwJMuSSi54GB0PNN6bCd4rc6K/vPFviTwNp/iT4U6xN9rvGR9t84YCPlXA8wHBDD8lOM8U9Wrot3cbo0bX4bCDxWnj06sX11rRbeVpYg0X3QpwqFPTA7YA4zT5eoctnc6T/AIqOPqumXPuWkh/TD0ytSjrXiweGNLm1nxFpc9tZ2yhpZ4pY5ETJwBgkNySPur3FK9gvYpXvi7w/r/hG5vrLXbnTLW9tJDDqLW8sSxAqf3gcgDI9iOlK90Ju6F+GDWcfg+wtLPxePEflhx9tLYZxuY4IzuXHI5ORjmmmEdih8Y/BGu+PPCw0bQdRht5I5vPkimXAnCjhd2CVOce3rRNXWgpxclobXgCw1zS/Bul6b4khtIb61gEMkdsf3SKpIQDHH3MA44z0oW2o4rSzIdc+F/w38SzNceIPAXh/ULiTrNcadE0h99+NxPv1p2Ksjgdf0XwP4E1+08PaF4+8XeEru8ge5jW1v3vLKONQfvRXYljQfIfuqvA9MGk3YiVoml4au/idrGlpr3g/4meH/FFmztH5eraI9m7MvUGW3cBT7mE+uO9Fxp82xqjx18RdM/d+IPg5qNwy8GbQdVtbyI+4E7W8n4FPxNFx3AfG/wAC2/7vxB/bfh64GR5Gr6PdQHPoH2GNz6bXINFxOSRs+HviR4D8VOkGheLdLurp13m0FyguVH+1ETvX8RRdDUlLY6XB9DTGf//X+4pfFPxI8axm28GeEm8N2EwIOs+I0KyhD/FDYxsJSx9ZWix3DDilqJ6lPw7+zl8NtIKT65preIrtDu36p+8iLZBz5GRGcEA5ZSQehFCjYlQS1On8F+AtE+Hg1abT9QuWh1Cf7U4uZAEgHPCgAADBPJyeOTximlYajysg8afETSvDmhXmpvaG9toUXzdwGGV22AhScupJ6gbP9qpbsEpJEnhPwv4Zn02PXLCyjjh1iKK5eKC3FrG4K5UNGoycA9HLAHmmrdASW6OqgggtolhtoY4ol4VUTao+mOlMdh0kiRDMjqn+8cUDIzdIfuCST02ocfnQBzms3/jA+J7DTbPQLWfw/dQyC/mnPzg88HDbQOnBBzk8CkyWaui2On6dp0Vp4esdPtrEZMa2rjy+TyQVHPPU0LyGrLYx4fh7okXi3UPGhsxJf6nb/ZZ45Zt0DIQoI2Fe4UA549utK2txcqvc6K3iazgS3tbGCKGNQqRxHaqjjgKFAH/6/WqWg7EhmkA+a1k+oKn+ZoGH2qPo6yr2IMZH8qAIb2DStYtZdN1CK2u7e5XZJBIAyyDvkenSgN9xDo2kvpLaH9gg/s9oDbm3CYjMR4K4GABzS02DRqxnweC/DmnaKNE0rRrO2hhjZLcNCspic5O75s5IY56/jRZWJcVsjnfA/h3UfDNhDoPi3xybjW7iaWW3EV62WiGOFjk4bHJzs43Y6AUlpuJK27Ot+y65b82+rxTL6XVuCx/4FGVH/jpp6laiG91qAEXGipMD3s7lST9RJsx+Zphc5DxE+t33jnRb9WsbbQreKWO+XUbAiUl9wYLIUKgEbRhWA4Od3SpauS1eVzstCsdC0/TktPDlvZQWKklI7QJ5YJOSfl4zzyfami9FsaFMCO6t7e8t5bS7gSaCdCkkcgyrqeCCD2OcUMLLqYGo/Dj4f6tpkei6p4K0S8soRiOG4sInWP3UFSVPuKEkhJJbEH/Cqvh3/wBCfZfrQM//0P0oGc8sfXrnB/GgDkPGWp+KNS0eew+Gc9u+rRSokkz7TFCo5K5Py7/YA4yc4yDUu72Jk7rQ2tP0iZ4Le48Qype3qqrOAMQRy45MceMDkcMctz1FNaDXmZrfD3w7/wAJnP47m8+S9nthayRySgwFAAN23HXaMY6d8d6Vri5Ve5k6H4d17wJofiCfTdWvvE15dStd2sVyvyRnBwgyw3HnnGAcAADrRsKzjdnSeG7nW9W0Ozv/ABDZtp19NHuntlIyhycZOSRkAHAPGQO2aaKjtqZvhrwpr+j+Kdc1nU/E76hY6g4NnaMn/HsM9Mk+mBx1xk80JO4opp3bOrxj/CmUV9QsYNTsLnTbrd5N3C8Em1ip2spU4I5BwetAMyvBng/SvAugxeHtGluZLaJ2kDXD73ZmPJJAAH4DFJKwkrG7TGM82PzDD5i+YBuKZ5A9cUXAfnHPpQBy7w+Ph8QFlS5sP+ESFrtaIj9952OO2Qc477dvbNTrcnVyKWtfFnwLoXiK78L69qBtLi0gWaSWaA+ScgEKuASWwQcBcdcHijmXUTmk7MzT8W/Dt346sfBOg6Rd3/2oK015bHbFErIHVwB95cEEnIAHTPSi+ugufU6HXPB15q3iHR9cg8R3lomlMWa2UsRNnscOox67g2R0xTa1Kcbu4up+BtG1TxPp/i++s0n1LS0KW8gd0CjJPK5IbBJIPHP4UWG43dzoPPcf623kH+0MMP05pjAXMGeZdh9Gyp/I0Ac7B4906fx7P8P10+9W7t7Rbk3DR5hxgHAPXocZ6buKXNfQnmu7G1PomkXUhmm02ATf89VTbIPo4+b9aLDsZevadr9jo15P4Rvrh9SjiZrW3upRJFJIOgJky3I/2gPpQ9hO9tCLw1dePDoVnP4m0zTTqDJm4igmMZQ5IH95SSoGfmA5x0FJX6gnK2pqf25HCMX+n31pz/HCXX8WjLKB9SKY7jf+El8O/wDQbsv+/i/40xn/0f0G8QR67caFfXaWzSzxW8j22mwS7TK+07Q8nUknHyjA9d1J7CexyXwI8CX/AIV8Pz6zr63kWq6zIZZ4LjjylVm2/L2Yg5Pfn2pRT3IpppanpRmLkrbqJCOrZwoqjQQQbjvnbzD16YA/D/HP4UAS4GMDj6UAL+FAHK6dqHj1/GurWOo6JaJ4egt1awuUceZLJ8o2nnv8/UDGB1BpXZMea4/wBrvirxBpM954t8NHRLuO6eOODJO+MAYbnnqSPQ4yOKEEW3udPTKCgAoA5m38A6Rb+Orjx/HdXpvrm2Fs0Rl/dBQAMgf8BHHTqcUrWdyeVXudNTKOX8EeBU8GNqrLrl9qX9qXZuj9qOfKzngepOeT3wKSQkrDfFfwy8H+NdSstV8Q6cZ57NSo2OUEik52vj7wzyPqaTjcTimdDZaZpunbfsFhb25WNIQY4gpEa8KuQOgxgDOKpaDSSMCx8V6vc+O73wlJ4UvIdOtLZZotTb/VSMQpKj5QO5HBLZU5GDST1EndnUUyg/lQAm0HggE/SgDD8V6/4f8ABWjTeI9Y82C2hKIWgQ7yWbAXCnnnnmjRCbSL2l3kWq6Zaarpl2z215ClxD5kfVHGRwMEcHnmgFrqWd9wn3oFYeqv/Q4/nQMpa9JqUujXsOj3K2WoPA62s1xGSkcmDtJwCMA49aGJ6rQr+DbfxHZ+GrG38WajBf6rGjLPcQ42tySv1+XAPHXmktNwSsrM0/sOm/8APlD/AN8imPlR/9L9JXlEeF5Lnoq9TQAwxvL80547RqePxPWgCbgDGBhenoKADuR3HX2oAKAOX8eSeP4rOx/4V/BYy3DXSC7F0QFEOOSM9Rnr39KTZMr9DqPf26UygoAKACgAoAKACgAoAKACgBOep54AJxzQAtAHL+ONE8Z6ydL/AOEQ8Sx6T9muhLeb13edH6Dg5xzwcA55IwKTTJkm7WOoPXpjPUfn+FMohu7W1v7d7W9tYriCTho5UDqw91NANXJEjSNVijRURFCqqjAAHReOABxgCjYVhtwbgW8ptBGZ9jeUJM7d+OM47ZxSew/Q5X4ax/ERNLupPiNNbteyXLPbpDszHF6EoMHnOBycdTSV+pMOa2p1TW8LncEAY/xKdp/MVRRH9lh/uT/9/wA0Af/T/SaOJIx8o+bue5oAdQADrQBy/g7wHa+Dr/Wr+21a+vDrNz9pZLh8rEcn7vvyRn0CiklbUlRS1OoplBQAUAFABQAUAFABQAUAFABQBy/h7SfG1n4q1y+17xBb3mjXTA6baohDwj34GOODydx546UknclJp3Z1FMoKACgAoAKACgAoAKAOY1zx9p2geLNG8JXFhey3GtZ8qaKLMaYOPmOfUHOOg5NJslys7WOnx70yj//U/SigAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAEKhiGKgsDwTyR680BYXBoA/9X9KKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoABQA7A9BQB//2Q== :alt: 1-s2.0-S245190221830329X-gr2.jpg 1-s2.0-S245190221830329X-gr2.jpg @@ -531,12 +478,12 @@ individual participants. S_hat=np.mean(S2,axis=0) n=S2.shape[0] CI[i,:]=z*np.power(S_hat/n,.5) - return CI + return CI feature_names=['left_CA1','left_CA3','right_CA1','right_CA3'] sex_covariates=[ 'Female','Male'] - # Creating plots for Female and male + # Creating plots for Female and male for i,sex in enumerate(sex_covariates): #forward model data forward_yhat = pd.read_csv('yhat_forward.txt', sep = ' ', header=None) @@ -544,71 +491,71 @@ individual participants. yhat_forward=yhat_forward[7*i:7*(i+1)] x_forward=[20, 30, 40, 50, 60, 70, 80] - # Find the index of the data exclusively for one sex. Female:0, Male: 1 + # Find the index of the data exclusively for one sex. Female:0, Male: 1 inx=np.where(covariate_normsample.sex==i)[0] x=covariate_normsample.values[inx,1] # actual data y = pd.read_csv('features_normsample.txt', sep = ' ', header=None) y=y.values[inx] - # confidence Interval yhat+ z *(std/n^.5)-->.95 % CI:z=1.96, 99% CI:z=2.58 + # confidence Interval yhat+ z *(std/n^.5)-->.95 % CI:z=1.96, 99% CI:z=2.58 s2= pd.read_csv('ys2_2fold.txt', sep = ' ', header=None) s2=s2.values[inx] CI_95=confidence_interval(s2,x,1.96) CI_99=confidence_interval(s2,x,2.58) - # Creat a trejactroy for each point + # Creat a trejactroy for each point for j,name in enumerate(feature_names): fig=plt.figure() ax=fig.add_subplot(111) ax.plot(x_forward,yhat_forward[:,j], linewidth=4, label='Normative trejactory') - ax.plot(x_forward,CI_95[:,j]+yhat_forward[:,j], linewidth=2,linestyle='--',c='g', label='95% confidence interval') - ax.plot(x_forward,-CI_95[:,j]+yhat_forward[:,j], linewidth=2,linestyle='--',c='g') + ax.plot(x_forward,CI_95[:,j]+yhat_forward[:,j], linewidth=2,linestyle='--',c='g', label='95% confidence interval') + ax.plot(x_forward,-CI_95[:,j]+yhat_forward[:,j], linewidth=2,linestyle='--',c='g') - ax.plot(x_forward,CI_99[:,j]+yhat_forward[:,j], linewidth=1,linestyle='--',c='k', label='99% confidence interval') - ax.plot(x_forward,-CI_99[:,j]+yhat_forward[:,j], linewidth=1,linestyle='--',c='k') + ax.plot(x_forward,CI_99[:,j]+yhat_forward[:,j], linewidth=1,linestyle='--',c='k', label='99% confidence interval') + ax.plot(x_forward,-CI_99[:,j]+yhat_forward[:,j], linewidth=1,linestyle='--',c='k') ax.scatter(x,y[:,j],c='r', label=name) plt.legend(loc='upper left') plt.title('Normative trejectory of' +name+' in '+sex+' cohort') plt.show() plt.close() - + # code by M. Zabihi -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_0.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_0.png -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_1.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_1.png -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_2.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_2.png -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_3.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_3.png -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_4.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_4.png -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_5.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_5.png -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_6.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_6.png -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_7.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_7.png **TASK 6:** Apply the normative model to Nordan’s data and the dementia patients. @@ -626,25 +573,25 @@ individual participants. # create a covariate file for Nordan's as well as the patient's demograhpics covariate_nordan = demographics_nordan[['sex', - 'age']] + 'age']] covariate_nordan.to_csv('covariate_nordan.txt', sep = ' ', - header = False, + header = False, index = False) # create the corresponding feature file - features_nordan = features_nordan[['left_CA1', + features_nordan = features_nordan[['left_CA1', 'left_CA3', 'right_CA1', 'right_CA3']] - features_nordan.to_csv('features_nordan.txt', - sep = ' ', - header = False, + features_nordan.to_csv('features_nordan.txt', + sep = ' ', + header = False, index = False) # apply normative modeling - pcn.normative.estimate(covfile = 'covariate_normsample.txt', + pcn.normative.estimate(covfile = 'covariate_normsample.txt', respfile = 'features_normsample.txt', testcov = 'covariate_nordan.txt', testresp = 'features_nordan.txt', @@ -657,8 +604,22 @@ individual participants. .. parsed-literal:: + inscaler: None + outscaler: None Processing data in features_normsample.txt Estimating model 1 of 4 + + +.. parsed-literal:: + + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:147: RuntimeWarning: overflow encountered in exp + self.sf2 = np.exp(2*theta[self.D]) + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:160: RuntimeWarning: invalid value encountered in multiply + dK = K * squared_dist(x[:, i]/self.ell[i], x[:, i]/self.ell[i]) + + +.. parsed-literal:: + Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -667,8 +628,8 @@ individual participants. Optimization terminated successfully. Current function value: 3781.497401 Iterations: 20 - Function evaluations: 61 - Gradient evaluations: 54 + Function evaluations: 58 + Gradient evaluations: 52 Estimating model 2 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -682,10 +643,22 @@ individual participants. Warning: Estimation of posterior distribution failed Optimization terminated successfully. Current function value: 3201.761309 - Iterations: 39 - Function evaluations: 108 - Gradient evaluations: 108 + Iterations: 48 + Function evaluations: 114 + Gradient evaluations: 114 Estimating model 3 of 4 + + +.. parsed-literal:: + + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:147: RuntimeWarning: overflow encountered in exp + self.sf2 = np.exp(2*theta[self.D]) + /usr/local/lib/python3.10/dist-packages/pcntoolkit/model/gp.py:160: RuntimeWarning: invalid value encountered in multiply + dK = K * squared_dist(x[:, i]/self.ell[i], x[:, i]/self.ell[i]) + + +.. parsed-literal:: + Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -696,9 +669,9 @@ individual participants. Warning: Estimation of posterior distribution failed Optimization terminated successfully. Current function value: 3771.310488 - Iterations: 47 - Function evaluations: 181 - Gradient evaluations: 167 + Iterations: 48 + Function evaluations: 156 + Gradient evaluations: 143 Estimating model 4 of 4 Warning: Estimation of posterior distribution failed Warning: Estimation of posterior distribution failed @@ -712,9 +685,9 @@ individual participants. Warning: Estimation of posterior distribution failed Optimization terminated successfully. Current function value: 3200.837262 - Iterations: 40 - Function evaluations: 104 - Gradient evaluations: 104 + Iterations: 42 + Function evaluations: 116 + Gradient evaluations: 116 Evaluating the model ... Writing outputs ... @@ -761,21 +734,21 @@ age 20. Do that for both sexes seperately. lengths = len(forward_yhat[hyppocampal_subfield]) for entry in forward_yhat[hyppocampal_subfield]: if count > 0 and count < 7: - loop_percentage_change_female = calculate_percentage_change(entry, + loop_percentage_change_female = calculate_percentage_change(entry, forward_yhat.iloc[0, hyppocampal_subfield]) percentage_change_female.append(loop_percentage_change_female) - elif count > 7: + elif count > 7: loop_percentage_change_male = calculate_percentage_change(entry, forward_yhat.iloc[9, hyppocampal_subfield]) percentage_change_male.append(loop_percentage_change_male) - count = count + 1 + count = count + 1 - names = ['30 compared to 20 years', - '40 compared to 20 years', - '50 compared to 20 years', - '60 compared to 20 years', + names = ['30 compared to 20 years', + '40 compared to 20 years', + '50 compared to 20 years', + '60 compared to 20 years', '70 compared to 20 years', '80 compared to 20 years'] @@ -803,5 +776,6 @@ age 20. Do that for both sexes seperately. -.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_32_1.png +.. image:: normative_modelling_walkthrough_files/normative_modelling_walkthrough_33_1.png + diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_0.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_0.png deleted file mode 100644 index 732e6d2f..00000000 Binary files a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_0.png and /dev/null differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_1.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_1.png deleted file mode 100644 index 50ae34e9..00000000 Binary files a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_1.png and /dev/null differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_2.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_2.png deleted file mode 100644 index 396e5341..00000000 Binary files a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_2.png and /dev/null differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_3.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_3.png deleted file mode 100644 index 76859174..00000000 Binary files a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_3.png and /dev/null differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_4.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_4.png deleted file mode 100644 index 542f02fe..00000000 Binary files a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_4.png and /dev/null differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_5.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_5.png deleted file mode 100644 index 7a38f4e2..00000000 Binary files a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_5.png and /dev/null differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_6.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_6.png deleted file mode 100644 index 943656aa..00000000 Binary files a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_6.png and /dev/null differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_7.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_7.png deleted file mode 100644 index b1907fc7..00000000 Binary files a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_22_7.png and /dev/null differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_0.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_0.png new file mode 100644 index 00000000..ff4eeede Binary files /dev/null and b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_0.png differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_1.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_1.png new file mode 100644 index 00000000..4a955a70 Binary files /dev/null and b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_1.png differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_2.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_2.png new file mode 100644 index 00000000..b7116146 Binary files /dev/null and b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_2.png differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_3.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_3.png new file mode 100644 index 00000000..5f46fe3a Binary files /dev/null and b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_3.png differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_4.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_4.png new file mode 100644 index 00000000..c7f5edc6 Binary files /dev/null and b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_4.png differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_5.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_5.png new file mode 100644 index 00000000..475de807 Binary files /dev/null and b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_5.png differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_6.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_6.png new file mode 100644 index 00000000..5bc3ed8f Binary files /dev/null and b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_6.png differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_7.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_7.png new file mode 100644 index 00000000..083bbae6 Binary files /dev/null and b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_23_7.png differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_32_1.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_32_1.png deleted file mode 100644 index 0cbfb428..00000000 Binary files a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_32_1.png and /dev/null differ diff --git a/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_33_1.png b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_33_1.png new file mode 100644 index 00000000..0c847136 Binary files /dev/null and b/doc/source/pages/normative_modelling_walkthrough_files/normative_modelling_walkthrough_33_1.png differ diff --git a/doc/source/pages/other_predictive_models.rst b/doc/source/pages/other_predictive_models.rst index c67b860c..2b841cad 100644 --- a/doc/source/pages/other_predictive_models.rst +++ b/doc/source/pages/other_predictive_models.rst @@ -1,24 +1,23 @@ -.. title:: Predictive modeling tutorial - -Predictive modeling using deviation scores -============================================= - -The Normative Modeling Framework for Computational Psychiatry. Nature Protocols. https://www.nature.com/articles/s41596-022-00696-5. - -Created by `Saige Rutherford `__ - +.. code:: ipython3 -.. image:: https://colab.research.google.com/assets/colab-badge.svg - :target: https://colab.research.google.com/github/predictive-clinical-neuroscience/PCNtoolkit-demo/blob/main/tutorials/BLR_protocol/other_predictive_models.ipynb + ! git clone https://github.com/predictive-clinical-neuroscience/PCNtoolkit-demo.git -.. code:: ipython3 +.. parsed-literal:: - ! git clone https://github.com/predictive-clinical-neuroscience/PCNtoolkit-demo.git + Cloning into 'PCNtoolkit-demo'... + remote: Enumerating objects: 1237, done. + remote: Counting objects: 100% (360/360), done. + remote: Compressing objects: 100% (185/185), done. + remote: Total 1237 (delta 200), reused 306 (delta 172), pack-reused 877 (from 1) + Receiving objects: 100% (1237/1237), 141.45 MiB | 10.83 MiB/s, done. + Resolving deltas: 100% (562/562), done. + Updating files: 100% (70/70), done. .. code:: ipython3 + import os .. code:: ipython3 @@ -42,7 +41,7 @@ Created by `Saige Rutherford `__ import numpy as np from matplotlib import pyplot as plt from scipy import stats, linalg - from sklearn import preprocessing, decomposition, linear_model, metrics + from sklearn import preprocessing, decomposition, linear_model, metrics import warnings .. code:: ipython3 @@ -62,7 +61,7 @@ Created by `Saige Rutherford `__ plt.rc('figure', titlesize=BIGGER_SIZE) # fontsize of the figure title Load Data ------------------------------ +========= .. code:: ipython3 @@ -85,7 +84,7 @@ Load Data Create Train/Test Splits --------------------------------------- +======================== .. code:: ipython3 @@ -117,7 +116,7 @@ Create Train/Test Splits test_mu_centered_ct = (test_data_ct - train_data_ct.mean(axis=0)) Principal Component Regression (BBS) --------------------------------------- +==================================== .. code:: ipython3 @@ -131,7 +130,7 @@ Principal Component Regression (BBS) .. code:: ipython3 - print(f'First PC explains {pca_model_z.explained_variance_ratio_[0]*100:.2f}% of the total variance.') + print(f'First PC explains {pca_model_z.explained_variance_ratio_[0]*100:.2f}% of the total variance.\nThis is an artifact of zero inflated data') plt.figure(figsize=(10, 7)) plt.bar(range(1, 51), pca_model_z.explained_variance_ratio_[1:51]) plt.title('Deviations model Variance Explained Ratio\nPCs 1-50', fontsize=25) @@ -141,15 +140,16 @@ Principal Component Regression (BBS) .. parsed-literal:: First PC explains 23.41% of the total variance. + This is an artifact of zero inflated data -.. image:: other_predictive_models_files/other_predictive_models_16_1.png +.. image:: other_predictive_models_files/other_predictive_models_17_1.png .. code:: ipython3 - print(f'First PC explains {pca_model_ct.explained_variance_ratio_[0]*100:.2f}% of the total variance.') + print(f'First PC explains {pca_model_ct.explained_variance_ratio_[0]*100:.2f}% of the total variance.\nThis is an artifact of zero inflated data') plt.figure(figsize=(10, 7)) plt.bar(range(1, 51), pca_model_ct.explained_variance_ratio_[1:51]) plt.title('Cortical Thickness model Variance Explained Ratio\nPCs 1-50', fontsize=25) @@ -159,10 +159,11 @@ Principal Component Regression (BBS) .. parsed-literal:: First PC explains 24.28% of the total variance. + This is an artifact of zero inflated data -.. image:: other_predictive_models_files/other_predictive_models_17_1.png +.. image:: other_predictive_models_files/other_predictive_models_18_1.png .. code:: ipython3 @@ -176,7 +177,7 @@ Principal Component Regression (BBS) test_transformed_ct = pca_model_ct.transform(test_data_ct) Fit Linear Regression Model --------------------------------------- +--------------------------- .. code:: ipython3 @@ -184,14 +185,14 @@ Fit Linear Regression Model # we will check that this matches sklearn results later # fit ols model on dimension reduced train data - train_features_z = np.hstack([np.ones((train_transformed_z.shape[0], 1)), + train_features_z = np.hstack([np.ones((train_transformed_z.shape[0], 1)), train_transformed_z]) - train_features_inv_z = linalg.pinv2(train_features_z) + train_features_inv_z = linalg.pinv(train_features_z) train_betas_z = np.dot(train_features_inv_z, train_phen) train_pred_phen_z = np.dot(train_features_z, train_betas_z) # fit ols model on dimension reduced test data - test_features_z = np.hstack([np.ones((test_transformed_z.shape[0], 1)), + test_features_z = np.hstack([np.ones((test_transformed_z.shape[0], 1)), test_transformed_z]) test_pred_phen_z = np.dot(test_features_z, train_betas_z) @@ -201,14 +202,14 @@ Fit Linear Regression Model # we will check that this matches sklearn results later # fit ols model on dimension reduced train data - train_features_ct = np.hstack([np.ones((train_transformed_ct.shape[0], 1)), + train_features_ct = np.hstack([np.ones((train_transformed_ct.shape[0], 1)), train_transformed_ct]) - train_features_inv_ct = linalg.pinv2(train_features_ct) + train_features_inv_ct = linalg.pinv(train_features_ct) train_betas_ct = np.dot(train_features_inv_ct, train_phen) train_pred_phen_ct = np.dot(train_features_ct, train_betas_ct) # fit ols model on dimension reduced test data - test_features_ct = np.hstack([np.ones((test_transformed_ct.shape[0], 1)), + test_features_ct = np.hstack([np.ones((test_transformed_ct.shape[0], 1)), test_transformed_ct]) test_pred_phen_ct = np.dot(test_features_ct, train_betas_ct) @@ -216,34 +217,20 @@ Fit Linear Regression Model # OLS using sklearn - lr_model_z = linear_model.LinearRegression(fit_intercept=True, normalize=False) + lr_model_z = linear_model.LinearRegression(fit_intercept=True) lr_model_z.fit(train_transformed_z, train_phen) train_pred_phen_lr_model_z = lr_model_z.predict(train_transformed_z) test_pred_phen_lr_model_z = lr_model_z.predict(test_transformed_z) - -.. parsed-literal:: - - /usr/local/lib/python3.7/dist-packages/sklearn/linear_model/_base.py:155: FutureWarning: 'normalize' was deprecated in version 1.0 and will be removed in 1.2. Please leave the normalize parameter to its default value to silence this warning. The default behavior of this estimator is to not do any normalization. If normalization is needed please use sklearn.preprocessing.StandardScaler instead. - FutureWarning, - - .. code:: ipython3 # OLS using sklearn - lr_model_ct = linear_model.LinearRegression(fit_intercept=True, normalize=False) + lr_model_ct = linear_model.LinearRegression(fit_intercept=True) lr_model_ct.fit(train_transformed_ct, train_phen) train_pred_phen_lr_model_ct = lr_model_ct.predict(train_transformed_ct) test_pred_phen_lr_model_ct = lr_model_ct.predict(test_transformed_ct) - -.. parsed-literal:: - - /usr/local/lib/python3.7/dist-packages/sklearn/linear_model/_base.py:155: FutureWarning: 'normalize' was deprecated in version 1.0 and will be removed in 1.2. Please leave the normalize parameter to its default value to silence this warning. The default behavior of this estimator is to not do any normalization. If normalization is needed please use sklearn.preprocessing.StandardScaler instead. - FutureWarning, - - .. code:: ipython3 # ensure matrix math predictions and sklearn predictions are accurate to 5 decimals @@ -270,8 +257,8 @@ Fit Linear Regression Model Passed -Mean Squared/Absolute Error of Predictions ------------------------------------------------ +Accuracy of Predictions +----------------------- .. code:: ipython3 @@ -326,18 +313,18 @@ BBS Cross Validation def bbs(X, y, n_components, n_cv_splits, pred_summary_function, verbose=False): assert X.shape[0] == y.shape[0] - + fold_accs_train = [] fold_accs_test = [] np.random.seed(42) shuffled_idxs = np.random.choice(range(X.shape[0]), size=X.shape[0], replace=False) for fold_i, test_idxs in enumerate(np.array_split(shuffled_idxs, n_cv_splits)): - train_mask = np.ones(X.shape[0], np.bool) + train_mask = np.ones(X.shape[0], bool) train_mask[test_idxs] = 0 # create train/text X, y train_X, test_X = X[train_mask, :], X[test_idxs, :] - train_y, test_y = y[train_mask], y[test_idxs] + train_y, test_y = y[train_mask], y[test_idxs] # mean center columns using train data only train_X_mu = train_X.mean(axis=0) @@ -356,7 +343,7 @@ BBS Cross Validation # fit OLS model if verbose: print(f'CV Fold: {fold_i+1:<10} Fitting Linear Regression model...') - lr_model = linear_model.LinearRegression(fit_intercept=True, normalize=False) + lr_model = linear_model.LinearRegression(fit_intercept=True) lr_model.fit(train_X, train_y) train_pred = lr_model.predict(train_X) @@ -364,11 +351,11 @@ BBS Cross Validation fold_accs_train.append(pred_summary_function(train_y, train_pred)) fold_accs_test.append(pred_summary_function(test_y, test_pred)) - + if verbose: - print(f'CV Fold: {fold_i+1:<10} Train MAE: {round(fold_accs_train[-1], 3):<10} Test MAE: {round(fold_accs_test[-1], 3):<10}') + print(f'CV Fold: {fold_i+1:<10} Train Accuracy: {round(fold_accs_train[-1], 3):<10} Test Accuracy: {round(fold_accs_test[-1], 3):<10}') + - plt.figure(figsize=(13, 7)) plt.plot(range(1, len(fold_accs_train)+1), fold_accs_train, linestyle='-', marker='o', color='C0', label='Train CV Performance') plt.plot(range(1, len(fold_accs_test)+1), fold_accs_test, linestyle='-', marker='o', color='C1', label='Test CV Performance') @@ -377,7 +364,7 @@ BBS Cross Validation plt.xlabel('CV Fold') plt.legend(fontsize=20) plt.show() - + return fold_accs_train, fold_accs_test .. code:: ipython3 @@ -389,23 +376,23 @@ BBS Cross Validation CV Fold: 1 Fitting PCA model... CV Fold: 1 Fitting Linear Regression model... - CV Fold: 1 Train MAE: 0.599 Test MAE: 0.619 + CV Fold: 1 Train Accuracy: 0.599 Test Accuracy: 0.619 CV Fold: 2 Fitting PCA model... CV Fold: 2 Fitting Linear Regression model... - CV Fold: 2 Train MAE: 0.572 Test MAE: 0.713 + CV Fold: 2 Train Accuracy: 0.572 Test Accuracy: 0.713 CV Fold: 3 Fitting PCA model... CV Fold: 3 Fitting Linear Regression model... - CV Fold: 3 Train MAE: 0.577 Test MAE: 0.687 + CV Fold: 3 Train Accuracy: 0.577 Test Accuracy: 0.687 CV Fold: 4 Fitting PCA model... CV Fold: 4 Fitting Linear Regression model... - CV Fold: 4 Train MAE: 0.604 Test MAE: 0.608 + CV Fold: 4 Train Accuracy: 0.604 Test Accuracy: 0.608 CV Fold: 5 Fitting PCA model... CV Fold: 5 Fitting Linear Regression model... - CV Fold: 5 Train MAE: 0.581 Test MAE: 0.687 + CV Fold: 5 Train Accuracy: 0.581 Test Accuracy: 0.687 -.. image:: other_predictive_models_files/other_predictive_models_32_3.png +.. image:: other_predictive_models_files/other_predictive_models_33_1.png .. code:: ipython3 @@ -417,41 +404,43 @@ BBS Cross Validation CV Fold: 1 Fitting PCA model... CV Fold: 1 Fitting Linear Regression model... - CV Fold: 1 Train MAE: 0.622 Test MAE: 0.643 + CV Fold: 1 Train Accuracy: 0.622 Test Accuracy: 0.643 CV Fold: 2 Fitting PCA model... CV Fold: 2 Fitting Linear Regression model... - CV Fold: 2 Train MAE: 0.605 Test MAE: 0.723 + CV Fold: 2 Train Accuracy: 0.605 Test Accuracy: 0.723 CV Fold: 3 Fitting PCA model... CV Fold: 3 Fitting Linear Regression model... - CV Fold: 3 Train MAE: 0.604 Test MAE: 0.701 + CV Fold: 3 Train Accuracy: 0.604 Test Accuracy: 0.701 CV Fold: 4 Fitting PCA model... CV Fold: 4 Fitting Linear Regression model... - CV Fold: 4 Train MAE: 0.624 Test MAE: 0.646 + CV Fold: 4 Train Accuracy: 0.624 Test Accuracy: 0.646 CV Fold: 5 Fitting PCA model... CV Fold: 5 Fitting Linear Regression model... - CV Fold: 5 Train MAE: 0.614 Test MAE: 0.722 + CV Fold: 5 Train Accuracy: 0.614 Test Accuracy: 0.722 -.. image:: other_predictive_models_files/other_predictive_models_33_3.png +.. image:: other_predictive_models_files/other_predictive_models_34_1.png Connectome Predictive Modelling --------------------------------------- +=============================== .. code:: ipython3 # correlation train_brain with train_phenotype train_z_pheno_corr_p = [stats.pearsonr(train_data_z[:, i], train_phen) for i in range(train_data_z.shape[1])] # train_pheno_corr_p: (259200, ) + # there are some nan correlations if brain data is poorly cropped (ie: some columns are always 0) .. code:: ipython3 # correlation train_brain with train_phenotype train_ct_pheno_corr_p = [stats.pearsonr(train_data_ct[:, i], train_phen) for i in range(train_data_ct.shape[1])] # train_pheno_corr_p: (259200, ) + # there are some nan correlations if brain data is poorly cropped (ie: some columns are always 0) .. code:: ipython3 - # split into positive and negative correlations + # split into positive and negative correlations # and keep edges with p values below threshold pval_threshold = 0.01 @@ -495,15 +484,13 @@ Connectome Predictive Modelling .. code:: ipython3 - fit_pos_z = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(train_pos_edges_sum_z.reshape(-1, 1), train_phen) - fit_neg_z = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(train_neg_edges_sum_z.reshape(-1, 1), train_phen) - + fit_pos_z = linear_model.LinearRegression(fit_intercept=True).fit(train_pos_edges_sum_z.reshape(-1, 1), train_phen) + fit_neg_z = linear_model.LinearRegression(fit_intercept=True).fit(train_neg_edges_sum_z.reshape(-1, 1), train_phen) .. code:: ipython3 - fit_pos_ct = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(train_pos_edges_sum_ct.reshape(-1, 1), train_phen) - fit_neg_ct = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(train_neg_edges_sum_ct.reshape(-1, 1), train_phen) - + fit_pos_ct = linear_model.LinearRegression(fit_intercept=True).fit(train_pos_edges_sum_ct.reshape(-1, 1), train_phen) + fit_neg_ct = linear_model.LinearRegression(fit_intercept=True).fit(train_neg_edges_sum_ct.reshape(-1, 1), train_phen) .. code:: ipython3 @@ -512,52 +499,48 @@ Connectome Predictive Modelling pos_error_ct = metrics.mean_absolute_error(train_phen, fit_pos_ct.predict(train_pos_edges_sum_ct.reshape(-1, 1))) neg_error_ct = metrics.mean_absolute_error(train_phen, fit_neg_ct.predict(train_neg_edges_sum_ct.reshape(-1, 1))) - print(f'Training Error (MAE) (Positive Z Features Model) = {pos_error_z:.3f}') - print(f'Training Error (MAE) (Negative Z Features Model) = {neg_error_z:.3f}') - print(f'Training Error (MAE) (Positive CT Features Model) = {pos_error_ct:.3f}') - print(f'Training Error (MAE) (Negative CT Features Model) = {neg_error_ct:.3f}') + print(f'Training Error (Positive Z Features Model) = {pos_error_z:.3f}') + print(f'Training Error (Negative Z Features Model) = {neg_error_z:.3f}') + print(f'Training Error (Positive CT Features Model) = {pos_error_ct:.3f}') + print(f'Training Error (Negative CT Features Model) = {neg_error_ct:.3f}') .. parsed-literal:: - Training Error (MAE) (Positive Z Features Model) = 0.631 - Training Error (MAE) (Negative Z Features Model) = 0.666 - Training Error (MAE) (Positive CT Features Model) = 0.662 - Training Error (MAE) (Negative CT Features Model) = 0.665 + Training Error (Positive Z Features Model) = 0.631 + Training Error (Negative Z Features Model) = 0.666 + Training Error (Positive CT Features Model) = 0.662 + Training Error (Negative CT Features Model) = 0.665 .. code:: ipython3 # combine positive/negative edges in one linear regression model - fit_pos_neg_z = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(np.stack((train_pos_edges_sum_z, train_neg_edges_sum_z)).T, train_phen) - - + fit_pos_neg_z = linear_model.LinearRegression(fit_intercept=True).fit(np.stack((train_pos_edges_sum_z, train_neg_edges_sum_z)).T, train_phen) .. code:: ipython3 # combine positive/negative edges in one linear regression model - fit_pos_neg_ct = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(np.stack((train_pos_edges_sum_ct, train_neg_edges_sum_ct)).T, train_phen) - - + fit_pos_neg_ct = linear_model.LinearRegression(fit_intercept=True).fit(np.stack((train_pos_edges_sum_ct, train_neg_edges_sum_ct)).T, train_phen) .. code:: ipython3 pos_neg_error_z = metrics.mean_absolute_error(train_phen, fit_pos_neg_z.predict(np.stack((train_pos_edges_sum_z, train_neg_edges_sum_z)).T)) pos_neg_error_ct = metrics.mean_absolute_error(train_phen, fit_pos_neg_ct.predict(np.stack((train_pos_edges_sum_ct, train_neg_edges_sum_ct)).T)) - print(f'Training Error (MAE) (Positive/Negative Z Features Model) = {pos_neg_error_z:.3f}') - print(f'Training Error (MAE) (Positive/Negative CT Features Model) = {pos_neg_error_ct:.3f}') + print(f'Training Error (Positive/Negative Z Features Model) = {pos_neg_error_z:.3f}') + print(f'Training Error (Positive/Negative CT Features Model) = {pos_neg_error_ct:.3f}') .. parsed-literal:: - Training Error (MAE) (Positive/Negative Z Features Model) = 0.620 - Training Error (MAE) (Positive/Negative CT Features Model) = 0.642 + Training Error (Positive/Negative Z Features Model) = 0.620 + Training Error (Positive/Negative CT Features Model) = 0.642 .. code:: ipython3 - # evaluate out of sample performance + # evaluate out of sample performance test_pos_edges_sum_z = test_data_z[:, keep_edges_pos_z].sum(1) test_neg_edges_sum_z = test_data_z[:, keep_edges_neg_z].sum(1) @@ -572,44 +555,44 @@ Connectome Predictive Modelling neg_test_error_ct = metrics.mean_absolute_error(test_phen, fit_neg_ct.predict(test_neg_edges_sum_ct.reshape(-1, 1))) pos_neg_test_error_ct = metrics.mean_absolute_error(test_phen, fit_pos_neg_ct.predict(np.stack((test_pos_edges_sum_ct, test_neg_edges_sum_ct)).T)) - print(f'Testing Error (MAE) (Positive Z Features Model) = {pos_test_error_z:.3f}') - print(f'Testing Error (MAE) (Negative Z Features Model) = {neg_test_error_z:.3f}') - print(f'Testing Error (MAE) (Positive/Negative Z Features Model) = {pos_neg_test_error_z:.3f}') - print(f'Testing Error (MAE) (Positive CT Features Model) = {pos_test_error_ct:.3f}') - print(f'Testing Error (MAE) (Negative CT Features Model) = {neg_test_error_ct:.3f}') - print(f'Testing Error (MAE) (Positive/Negative CT Features Model) = {pos_neg_test_error_ct:.3f}') + print(f'Testing Error (Positive Z Features Model) = {pos_test_error_z:.3f}') + print(f'Testing Error (Negative Z Features Model) = {neg_test_error_z:.3f}') + print(f'Testing Error (Positive/Negative Z Features Model) = {pos_neg_test_error_z:.3f}') + print(f'Testing Error (Positive CT Features Model) = {pos_test_error_ct:.3f}') + print(f'Testing Error (Negative CT Features Model) = {neg_test_error_ct:.3f}') + print(f'Testing Error (Positive/Negative CT Features Model) = {pos_neg_test_error_ct:.3f}') .. parsed-literal:: - Testing Error (MAE) (Positive Z Features Model) = 0.705 - Testing Error (MAE) (Negative Z Features Model) = 0.696 - Testing Error (MAE) (Positive/Negative Z Features Model) = 0.697 - Testing Error (MAE) (Positive CT Features Model) = 0.710 - Testing Error (MAE) (Negative CT Features Model) = 0.695 - Testing Error (MAE) (Positive/Negative CT Features Model) = 0.701 + Testing Error (Positive Z Features Model) = 0.705 + Testing Error (Negative Z Features Model) = 0.696 + Testing Error (Positive/Negative Z Features Model) = 0.697 + Testing Error (Positive CT Features Model) = 0.710 + Testing Error (Negative CT Features Model) = 0.695 + Testing Error (Positive/Negative CT Features Model) = 0.701 CPM Cross Validation --------------------------------------- +-------------------- .. code:: ipython3 def cpm(X, y, p_threshold, n_cv_splits, pred_summary_function, verbose=False): assert X.shape[0] == y.shape[0] - + fold_accs_train = [] fold_accs_test = [] np.random.seed(42) shuffled_idxs = np.random.choice(range(X.shape[0]), size=X.shape[0], replace=False) for fold_i, test_idxs in enumerate(np.array_split(shuffled_idxs, n_cv_splits)): - train_mask = np.ones(X.shape[0], np.bool) + train_mask = np.ones(X.shape[0], bool) train_mask[test_idxs] = 0 # create train/text X, y train_X, test_X = X[train_mask, :], X[test_idxs, :] - train_y, test_y = y[train_mask], y[test_idxs] - + train_y, test_y = y[train_mask], y[test_idxs] + # create correlation matrix between train_X and train_y if verbose: print(f'CV Fold: {fold_i+1:<10} Computing correlations between train_X and train_y...') @@ -622,18 +605,18 @@ CPM Cross Validation # create masks for edges below p-threshold and split pos/neg correlations keep_edges_pos = (train_corrs > 0) & (train_pvals < p_threshold) keep_edges_neg = (train_corrs < 0) & (train_pvals < p_threshold) - + # sum X entries with significant correlations with y train_pos_edges_sum = train_X[:, keep_edges_pos].sum(1) train_neg_edges_sum = train_X[:, keep_edges_neg].sum(1) test_pos_edges_sum = test_X[:, keep_edges_pos].sum(1) test_neg_edges_sum = test_X[:, keep_edges_neg].sum(1) - + # fit linear regression models based on summed values - fit_pos = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(train_pos_edges_sum.reshape(-1, 1), train_y) - fit_neg = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(train_neg_edges_sum.reshape(-1, 1), train_y) - fit_pos_neg = linear_model.LinearRegression(fit_intercept=True, normalize=False).fit(np.stack((train_pos_edges_sum, train_neg_edges_sum)).T, train_y) - + fit_pos = linear_model.LinearRegression(fit_intercept=True).fit(train_pos_edges_sum.reshape(-1, 1), train_y) + fit_neg = linear_model.LinearRegression(fit_intercept=True).fit(train_neg_edges_sum.reshape(-1, 1), train_y) + fit_pos_neg = linear_model.LinearRegression(fit_intercept=True).fit(np.stack((train_pos_edges_sum, train_neg_edges_sum)).T, train_y) + # compute train errors train_pos_error = pred_summary_function(train_y, fit_pos.predict(train_pos_edges_sum.reshape(-1, 1))) train_neg_error = pred_summary_function(train_y, fit_neg.predict(train_neg_edges_sum.reshape(-1, 1))) @@ -646,27 +629,27 @@ CPM Cross Validation fold_accs_train.append((train_pos_error, train_neg_error, train_posneg_error)) fold_accs_test.append((test_pos_error, test_neg_error, test_posneg_error)) - + if verbose: - print(f'CV Fold: {fold_i+1:<10} Train Pos-Edges Model MAE: {round(train_pos_error, 3):<10} Train Neg-Edges Model Accuracy: {round(train_neg_error, 3):<10} Train Pos/Neg-Edges Model Accuracy: {round(train_posneg_error, 3):<10}') - print(f'CV Fold: {fold_i+1:<10} Test Pos-Edges Model MAE: {round(test_pos_error, 3):<10} Test Neg-Edges Model Accuracy: {round(test_neg_error, 3):<10} Test Pos/Neg-Edges Model Accuracy: {round(test_posneg_error, 3):<10}') + print(f'CV Fold: {fold_i+1:<10} Train Pos-Edges Model Accuracy: {round(train_pos_error, 3):<10} Train Neg-Edges Model Accuracy: {round(train_neg_error, 3):<10} Train Pos/Neg-Edges Model Accuracy: {round(train_posneg_error, 3):<10}') + print(f'CV Fold: {fold_i+1:<10} Test Pos-Edges Model Accuracy: {round(test_pos_error, 3):<10} Test Neg-Edges Model Accuracy: {round(test_neg_error, 3):<10} Test Pos/Neg-Edges Model Accuracy: {round(test_posneg_error, 3):<10}') + - plt.figure(figsize=(13, 7)) plt.plot(range(1, len(fold_accs_train)+1), [x[0] for x in fold_accs_train], linestyle='--', marker='o', color='C0', label='Train Pos-Edges Model') plt.plot(range(1, len(fold_accs_train)+1), [x[1] for x in fold_accs_train], linestyle='--', marker='o', color='C1', label='Train Neg-Edges Model') plt.plot(range(1, len(fold_accs_train)+1), [x[2] for x in fold_accs_train], linestyle='--', marker='o', color='C2', label='Train Pos/Neg-Edges Model') - + plt.plot(range(1, len(fold_accs_test)+1), [x[0] for x in fold_accs_test], linestyle='-', marker='o', color='C0', label='Test Pos-Edges Model') plt.plot(range(1, len(fold_accs_test)+1), [x[1] for x in fold_accs_test], linestyle='-', marker='o', color='C1', label='Test Neg-Edges Model') plt.plot(range(1, len(fold_accs_test)+1), [x[2] for x in fold_accs_test], linestyle='-', marker='o', color='C2', label='Test Pos/Neg-Edges Model') - + plt.title(pred_summary_function.__name__, fontsize=20) plt.xticks(range(1, len(fold_accs_test)+1)) plt.xlabel('CV Fold') plt.legend(fontsize=10) plt.show() - + return fold_accs_train, fold_accs_test .. code:: ipython3 @@ -674,32 +657,27 @@ CPM Cross Validation fold_accs_train_z, fold_accs_test_z = cpm(hcp_z, gscores, p_threshold=0.01, n_cv_splits=5, pred_summary_function=metrics.mean_absolute_error, verbose=True) - .. parsed-literal:: CV Fold: 1 Computing correlations between train_X and train_y... - CV Fold: 1 Train Pos-Edges Model MAE: 0.652 Train Neg-Edges Model MAE: 0.673 Train Pos/Neg-Edges Model MAE: 0.644 - CV Fold: 1 Test Pos-Edges Model MAE: 0.636 Test Neg-Edges Model MAE: 0.671 Test Pos/Neg-Edges Model MAE: 0.632 + CV Fold: 1 Train Pos-Edges Model Accuracy: 0.652 Train Neg-Edges Model Accuracy: 0.673 Train Pos/Neg-Edges Model Accuracy: 0.644 + CV Fold: 1 Test Pos-Edges Model Accuracy: 0.636 Test Neg-Edges Model Accuracy: 0.671 Test Pos/Neg-Edges Model Accuracy: 0.632 CV Fold: 2 Computing correlations between train_X and train_y... - CV Fold: 2 Train Pos-Edges Model MAE: 0.648 Train Neg-Edges Model MAE: 0.678 Train Pos/Neg-Edges Model MAE: 0.636 - CV Fold: 2 Test Pos-Edges Model MAE: 0.651 Test Neg-Edges Model MAE: 0.659 Test Pos/Neg-Edges Model MAE: 0.662 + CV Fold: 2 Train Pos-Edges Model Accuracy: 0.648 Train Neg-Edges Model Accuracy: 0.678 Train Pos/Neg-Edges Model Accuracy: 0.636 + CV Fold: 2 Test Pos-Edges Model Accuracy: 0.651 Test Neg-Edges Model Accuracy: 0.659 Test Pos/Neg-Edges Model Accuracy: 0.662 CV Fold: 3 Computing correlations between train_X and train_y... - CV Fold: 3 Train Pos-Edges Model MAE: 0.644 Train Neg-Edges Model MAE: 0.662 Train Pos/Neg-Edges Model MAE: 0.636 - CV Fold: 3 Test Pos-Edges Model MAE: 0.65 Test Neg-Edges Model MAE: 0.708 Test Pos/Neg-Edges Model MAE: 0.646 + CV Fold: 3 Train Pos-Edges Model Accuracy: 0.644 Train Neg-Edges Model Accuracy: 0.662 Train Pos/Neg-Edges Model Accuracy: 0.636 + CV Fold: 3 Test Pos-Edges Model Accuracy: 0.65 Test Neg-Edges Model Accuracy: 0.708 Test Pos/Neg-Edges Model Accuracy: 0.646 CV Fold: 4 Computing correlations between train_X and train_y... - CV Fold: 4 Train Pos-Edges Model MAE: 0.653 Train Neg-Edges Model MAE: 0.676 Train Pos/Neg-Edges Model MAE: 0.648 - CV Fold: 4 Test Pos-Edges Model MAE: 0.626 Test Neg-Edges Model MAE: 0.659 Test Pos/Neg-Edges Model MAE: 0.625 + CV Fold: 4 Train Pos-Edges Model Accuracy: 0.653 Train Neg-Edges Model Accuracy: 0.676 Train Pos/Neg-Edges Model Accuracy: 0.648 + CV Fold: 4 Test Pos-Edges Model Accuracy: 0.626 Test Neg-Edges Model Accuracy: 0.659 Test Pos/Neg-Edges Model Accuracy: 0.625 CV Fold: 5 Computing correlations between train_X and train_y... + CV Fold: 5 Train Pos-Edges Model Accuracy: 0.631 Train Neg-Edges Model Accuracy: 0.666 Train Pos/Neg-Edges Model Accuracy: 0.62 + CV Fold: 5 Test Pos-Edges Model Accuracy: 0.704 Test Neg-Edges Model Accuracy: 0.696 Test Pos/Neg-Edges Model Accuracy: 0.697 -.. parsed-literal:: - - CV Fold: 5 Train Pos-Edges Model MAE: 0.631 Train Neg-Edges Model MAE: 0.666 Train Pos/Neg-Edges Model MAE: 0.62 - CV Fold: 5 Test Pos-Edges Model MAE: 0.704 Test Neg-Edges Model MAE: 0.696 Test Pos/Neg-Edges Model MAE: 0.697 - - -.. image:: other_predictive_models_files/other_predictive_models_50_4.png +.. image:: other_predictive_models_files/other_predictive_models_51_1.png .. code:: ipython3 @@ -710,51 +688,61 @@ CPM Cross Validation .. parsed-literal:: CV Fold: 1 Computing correlations between train_X and train_y... - CV Fold: 1 Train Pos-Edges Model MAE: 0.675 Train Neg-Edges Model MAE: 0.673 Train Pos/Neg-Edges Model MAE: 0.659 - CV Fold: 1 Test Pos-Edges Model MAE: 0.659 Test Neg-Edges Model MAE: 0.67 Test Pos/Neg-Edges Model MAE: 0.653 + CV Fold: 1 Train Pos-Edges Model Accuracy: 0.675 Train Neg-Edges Model Accuracy: 0.673 Train Pos/Neg-Edges Model Accuracy: 0.659 + CV Fold: 1 Test Pos-Edges Model Accuracy: 0.659 Test Neg-Edges Model Accuracy: 0.67 Test Pos/Neg-Edges Model Accuracy: 0.653 CV Fold: 2 Computing correlations between train_X and train_y... - CV Fold: 2 Train Pos-Edges Model MAE: 0.674 Train Neg-Edges Model MAE: 0.678 Train Pos/Neg-Edges Model MAE: 0.636 - CV Fold: 2 Test Pos-Edges Model MAE: 0.661 Test Neg-Edges Model MAE: 0.657 Test Pos/Neg-Edges Model MAE: 0.668 + CV Fold: 2 Train Pos-Edges Model Accuracy: 0.674 Train Neg-Edges Model Accuracy: 0.678 Train Pos/Neg-Edges Model Accuracy: 0.636 + CV Fold: 2 Test Pos-Edges Model Accuracy: 0.661 Test Neg-Edges Model Accuracy: 0.657 Test Pos/Neg-Edges Model Accuracy: 0.668 CV Fold: 3 Computing correlations between train_X and train_y... - CV Fold: 3 Train Pos-Edges Model MAE: 0.659 Train Neg-Edges Model MAE: 0.665 Train Pos/Neg-Edges Model MAE: 0.644 - CV Fold: 3 Test Pos-Edges Model MAE: 0.699 Test Neg-Edges Model MAE: 0.704 Test Pos/Neg-Edges Model MAE: 0.684 + CV Fold: 3 Train Pos-Edges Model Accuracy: 0.659 Train Neg-Edges Model Accuracy: 0.665 Train Pos/Neg-Edges Model Accuracy: 0.644 + CV Fold: 3 Test Pos-Edges Model Accuracy: 0.699 Test Neg-Edges Model Accuracy: 0.704 Test Pos/Neg-Edges Model Accuracy: 0.684 CV Fold: 4 Computing correlations between train_X and train_y... - CV Fold: 4 Train Pos-Edges Model MAE: 0.674 Train Neg-Edges Model MAE: 0.678 Train Pos/Neg-Edges Model MAE: 0.658 - CV Fold: 4 Test Pos-Edges Model MAE: 0.653 Test Neg-Edges Model MAE: 0.656 Test Pos/Neg-Edges Model MAE: 0.638 + CV Fold: 4 Train Pos-Edges Model Accuracy: 0.674 Train Neg-Edges Model Accuracy: 0.678 Train Pos/Neg-Edges Model Accuracy: 0.658 + CV Fold: 4 Test Pos-Edges Model Accuracy: 0.653 Test Neg-Edges Model Accuracy: 0.656 Test Pos/Neg-Edges Model Accuracy: 0.638 CV Fold: 5 Computing correlations between train_X and train_y... - CV Fold: 5 Train Pos-Edges Model MAE: 0.662 Train Neg-Edges Model MAE: 0.666 Train Pos/Neg-Edges Model MAE: 0.642 - CV Fold: 5 Test Pos-Edges Model MAE: 0.709 Test Neg-Edges Model MAE: 0.698 Test Pos/Neg-Edges Model MAE: 0.708 + CV Fold: 5 Train Pos-Edges Model Accuracy: 0.662 Train Neg-Edges Model Accuracy: 0.666 Train Pos/Neg-Edges Model Accuracy: 0.642 + CV Fold: 5 Test Pos-Edges Model Accuracy: 0.709 Test Neg-Edges Model Accuracy: 0.698 Test Pos/Neg-Edges Model Accuracy: 0.708 -.. image:: other_predictive_models_files/other_predictive_models_51_2.png +.. image:: other_predictive_models_files/other_predictive_models_52_1.png Lasso (Linear Regression + L1 Regularization) ------------------------------------------------------ +============================================= .. code:: ipython3 - # LassoCV uses coordinate descent to select hyperparameter alpha + # LassoCV uses coordinate descent to select hyperparameter alpha alpha_grid = np.array([10**a for a in np.arange(-3, 3, 0.25)]) - lassoCV_model_z = linear_model.LassoCV(cv=5, n_alphas=len(alpha_grid), alphas=alpha_grid, fit_intercept=True, normalize=False, random_state=42, verbose=True, n_jobs=5).fit(train_data_z, train_phen) + lassoCV_model_z = linear_model.LassoCV(cv=5, n_alphas=len(alpha_grid), alphas=alpha_grid, fit_intercept=True, random_state=42, verbose=True, n_jobs=5).fit(train_data_z, train_phen) +.. parsed-literal:: + + [Parallel(n_jobs=5)]: Using backend ThreadingBackend with 5 concurrent workers. + .....................................................................................................................[Parallel(n_jobs=5)]: Done 2 out of 5 | elapsed: 0.5s remaining: 0.7s + /usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.07308221069854426, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.15375414865695802, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + ./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.10611096508367268, tolerance: 0.04382929483334259 + model = cd_fast.enet_coordinate_descent_gram( + .[Parallel(n_jobs=5)]: Done 5 out of 5 | elapsed: 0.5s finished + .. code:: ipython3 - # LassoCV uses coordinate descent to select hyperparameter alpha + # LassoCV uses coordinate descent to select hyperparameter alpha alpha_grid = np.array([10**a for a in np.arange(-3, 3, 0.25)]) - lassoCV_model_ct = linear_model.LassoCV(cv=5, n_alphas=len(alpha_grid), alphas=alpha_grid, fit_intercept=True, normalize=False, random_state=42, verbose=True, n_jobs=5).fit(train_data_ct, train_phen) + lassoCV_model_ct = linear_model.LassoCV(cv=5, n_alphas=len(alpha_grid), alphas=alpha_grid, fit_intercept=True, random_state=42, verbose=True, n_jobs=5).fit(train_data_ct, train_phen) .. parsed-literal:: [Parallel(n_jobs=5)]: Using backend ThreadingBackend with 5 concurrent workers. - ...................................................................................................................[Parallel(n_jobs=5)]: Done 2 out of 5 | elapsed: 0.3s remaining: 0.5s - .....[Parallel(n_jobs=5)]: Done 5 out of 5 | elapsed: 0.3s finished - /usr/local/lib/python3.7/dist-packages/sklearn/linear_model/_base.py:155: FutureWarning: 'normalize' was deprecated in version 1.0 and will be removed in 1.2. Please leave the normalize parameter to its default value to silence this warning. The default behavior of this estimator is to not do any normalization. If normalization is needed please use sklearn.preprocessing.StandardScaler instead. - FutureWarning, + ....................................................................................................................[Parallel(n_jobs=5)]: Done 2 out of 5 | elapsed: 0.2s remaining: 0.3s + ....[Parallel(n_jobs=5)]: Done 5 out of 5 | elapsed: 0.3s finished .. code:: ipython3 @@ -770,7 +758,7 @@ Lasso (Linear Regression + L1 Regularization) -.. image:: other_predictive_models_files/other_predictive_models_55_0.png +.. image:: other_predictive_models_files/other_predictive_models_56_0.png .. code:: ipython3 @@ -786,22 +774,18 @@ Lasso (Linear Regression + L1 Regularization) -.. image:: other_predictive_models_files/other_predictive_models_56_0.png +.. image:: other_predictive_models_files/other_predictive_models_57_0.png .. code:: ipython3 # based on cv results above, set alpha=100 - lasso_model_z = linear_model.Lasso(alpha=lassoCV_model_z.alpha_, fit_intercept=True, normalize=False).fit(train_data_z, train_phen) - - + lasso_model_z = linear_model.Lasso(alpha=lassoCV_model_z.alpha_, fit_intercept=True).fit(train_data_z, train_phen) .. code:: ipython3 # based on cv results above, set alpha=100 - lasso_model_ct = linear_model.Lasso(alpha=lassoCV_model_ct.alpha_, fit_intercept=True, normalize=False).fit(train_data_ct, train_phen) - - + lasso_model_ct = linear_model.Lasso(alpha=lassoCV_model_ct.alpha_, fit_intercept=True).fit(train_data_ct, train_phen) .. code:: ipython3 @@ -832,23 +816,23 @@ Lasso (Linear Regression + L1 Regularization) Ridge (Linear Regression + L2 Regularization) --------------------------------------------------------- +============================================= .. code:: ipython3 - # RidgeCV uses generalized cross validation to select hyperparameter alpha + # RidgeCV uses generalized cross validation to select hyperparameter alpha with warnings.catch_warnings(): # ignore matrix decomposition errors warnings.simplefilter("ignore") - ridgeCV_model_z = linear_model.RidgeCV(alphas=(0.1, 1.0, 10.0), fit_intercept=True, normalize=False, cv=5).fit(train_data_z, train_phen) + ridgeCV_model_z = linear_model.RidgeCV(alphas=(0.1, 1.0, 10.0), fit_intercept=True, cv=5).fit(train_data_z, train_phen) .. code:: ipython3 - # RidgeCV uses generalized cross validation to select hyperparameter alpha + # RidgeCV uses generalized cross validation to select hyperparameter alpha with warnings.catch_warnings(): # ignore matrix decomposition errors warnings.simplefilter("ignore") - ridgeCV_model_ct = linear_model.RidgeCV(alphas=(0.1, 1.0, 10.0), fit_intercept=True, normalize=False, cv=5).fit(train_data_ct, train_phen) + ridgeCV_model_ct = linear_model.RidgeCV(alphas=(0.1, 1.0, 10.0), fit_intercept=True, cv=5).fit(train_data_ct, train_phen) .. code:: ipython3 @@ -874,14 +858,11 @@ Ridge (Linear Regression + L2 Regularization) .. code:: ipython3 - ridge_model_z = linear_model.Ridge(alpha=ridge_alpha_z, fit_intercept=True, normalize=False).fit(train_data_z, train_phen) - - + ridge_model_z = linear_model.Ridge(alpha=ridge_alpha_z, fit_intercept=True).fit(train_data_z, train_phen) .. code:: ipython3 - ridge_model_ct = linear_model.Ridge(alpha=ridge_alpha_ct, fit_intercept=True, normalize=False).fit(train_data_ct, train_phen) - + ridge_model_ct = linear_model.Ridge(alpha=ridge_alpha_ct, fit_intercept=True).fit(train_data_ct, train_phen) .. code:: ipython3 @@ -912,21 +893,81 @@ Ridge (Linear Regression + L2 Regularization) Elastic Net (Linear Regression + L1/L2 Regularization) ------------------------------------------------------------- +====================================================== .. code:: ipython3 - # RidgeCV uses generalized cross validation to select hyperparameter alpha + # RidgeCV uses generalized cross validation to select hyperparameter alpha elasticnetCV_model_z = linear_model.ElasticNetCV(l1_ratio=[.1, .5, .7, .9, .95, .99, 1], cv=5, n_alphas=len(alpha_grid), alphas=alpha_grid, random_state=42, verbose=True, n_jobs=5).fit(train_data_z, train_phen) +.. parsed-literal:: -.. code:: ipython3 - - # RidgeCV uses generalized cross validation to select hyperparameter alpha + [Parallel(n_jobs=5)]: Using backend ThreadingBackend with 5 concurrent workers. + ............................................................................................................./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.21318694590257792, tolerance: 0.0423918944559644 + model = cd_fast.enet_coordinate_descent_gram( + ./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.17936527851907158, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + .../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.8618322913218321, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 11.57867990423236, tolerance: 0.0423918944559644 + model = cd_fast.enet_coordinate_descent_gram( + ./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 10.2273489799189, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + ................../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.4036642558553467, tolerance: 0.04401109832998077 + model = cd_fast.enet_coordinate_descent_gram( + .................../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 8.073063075099014, tolerance: 0.04382929483334259 + model = cd_fast.enet_coordinate_descent_gram( + ../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 18.227358858718446, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ............................................/usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 14.883650580549045, tolerance: 0.04401109832998077 + model = cd_fast.enet_coordinate_descent_gram( + ....................................../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.18805636326129616, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ............../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0544661418971657, tolerance: 0.0423918944559644 + model = cd_fast.enet_coordinate_descent_gram( + ........................................................................................................./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.1788130249701112, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + ............/usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.13839227040918445, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ........................................................................................................../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.15009167262110168, tolerance: 0.04382929483334259 + model = cd_fast.enet_coordinate_descent_gram( + ........................................./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.20204581109658193, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + ............................/usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.09891903798924773, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ......................................................................./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.13078279402705562, tolerance: 0.04382929483334259 + model = cd_fast.enet_coordinate_descent_gram( + ../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.18265009272980137, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + ..../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.0877297694903234, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ............................................................................................../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.11087317503455552, tolerance: 0.04382929483334259 + model = cd_fast.enet_coordinate_descent_gram( + ...................../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.16051209546739642, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + ..................................../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.07594686106816084, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ............................................................./usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.10611096508367268, tolerance: 0.04382929483334259 + model = cd_fast.enet_coordinate_descent_gram( + ...../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.15375414865695802, tolerance: 0.03970345334827422 + model = cd_fast.enet_coordinate_descent_gram( + ../usr/local/lib/python3.10/dist-packages/sklearn/linear_model/_coordinate_descent.py:683: ConvergenceWarning: Objective did not converge. You might want to increase the number of iterations. Duality gap: 0.07308221069854426, tolerance: 0.04611195889050071 + model = cd_fast.enet_coordinate_descent_gram( + ..[Parallel(n_jobs=5)]: Done 35 out of 35 | elapsed: 6.3s finished + + +.. code:: ipython3 + + # RidgeCV uses generalized cross validation to select hyperparameter alpha elasticnetCV_model_ct = linear_model.ElasticNetCV(l1_ratio=[.1, .5, .7, .9, .95, .99, 1], cv=5, n_alphas=len(alpha_grid), alphas=alpha_grid, random_state=42, verbose=True, n_jobs=5).fit(train_data_ct, train_phen) +.. parsed-literal:: + + [Parallel(n_jobs=5)]: Using backend ThreadingBackend with 5 concurrent workers. + ........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................[Parallel(n_jobs=5)]: Done 35 out of 35 | elapsed: 1.8s finished + .. code:: ipython3 @@ -958,7 +999,7 @@ Elastic Net (Linear Regression + L1/L2 Regularization) -.. image:: other_predictive_models_files/other_predictive_models_72_0.png +.. image:: other_predictive_models_files/other_predictive_models_73_0.png .. code:: ipython3 @@ -975,12 +1016,12 @@ Elastic Net (Linear Regression + L1/L2 Regularization) -.. image:: other_predictive_models_files/other_predictive_models_73_0.png +.. image:: other_predictive_models_files/other_predictive_models_74_0.png .. code:: ipython3 - elasticnet_model_z = linear_model.ElasticNet(alpha=elasticnetCV_model_z.alpha_, l1_ratio=elasticnetCV_model_z.l1_ratio_, fit_intercept=True, normalize=False, random_state=42).fit(train_data_z, train_phen) + elasticnet_model_z = linear_model.ElasticNet(alpha=elasticnetCV_model_z.alpha_, l1_ratio=elasticnetCV_model_z.l1_ratio_, fit_intercept=True, random_state=42).fit(train_data_z, train_phen) train_preds_en_model_z = elasticnet_model_z.predict(train_data_z) test_preds_en_model_z = elasticnet_model_z.predict(test_data_z) @@ -988,7 +1029,7 @@ Elastic Net (Linear Regression + L1/L2 Regularization) train_mae_z = metrics.mean_absolute_error(train_phen, train_preds_en_model_z) test_mae_z = metrics.mean_absolute_error(test_phen, test_preds_en_model_z) - elasticnet_model_ct = linear_model.ElasticNet(alpha=elasticnetCV_model_ct.alpha_, l1_ratio=elasticnetCV_model_ct.l1_ratio_, fit_intercept=True, normalize=False, random_state=42).fit(train_data_ct, train_phen) + elasticnet_model_ct = linear_model.ElasticNet(alpha=elasticnetCV_model_ct.alpha_, l1_ratio=elasticnetCV_model_ct.l1_ratio_, fit_intercept=True, random_state=42).fit(train_data_ct, train_phen) train_preds_en_model_ct = elasticnet_model_ct.predict(train_data_ct) test_preds_en_model_ct = elasticnet_model_ct.predict(test_data_ct) @@ -1008,3 +1049,5 @@ Elastic Net (Linear Regression + L1/L2 Regularization) Test MAE Z model: 0.680 Train MAE CT model: 0.633 Test MAE CT model: 0.692 + + diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_16_1.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_16_1.png deleted file mode 100644 index 4b8e0cbf..00000000 Binary files a/doc/source/pages/other_predictive_models_files/other_predictive_models_16_1.png and /dev/null differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_17_1.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_17_1.png index f832a949..99be9491 100644 Binary files a/doc/source/pages/other_predictive_models_files/other_predictive_models_17_1.png and b/doc/source/pages/other_predictive_models_files/other_predictive_models_17_1.png differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_18_1.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_18_1.png new file mode 100644 index 00000000..46a04ca9 Binary files /dev/null and b/doc/source/pages/other_predictive_models_files/other_predictive_models_18_1.png differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_32_3.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_32_3.png deleted file mode 100644 index 1947e14f..00000000 Binary files a/doc/source/pages/other_predictive_models_files/other_predictive_models_32_3.png and /dev/null differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_33_1.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_33_1.png new file mode 100644 index 00000000..21cc4855 Binary files /dev/null and b/doc/source/pages/other_predictive_models_files/other_predictive_models_33_1.png differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_33_3.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_33_3.png deleted file mode 100644 index 8297272e..00000000 Binary files a/doc/source/pages/other_predictive_models_files/other_predictive_models_33_3.png and /dev/null differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_34_1.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_34_1.png new file mode 100644 index 00000000..7d926578 Binary files /dev/null and b/doc/source/pages/other_predictive_models_files/other_predictive_models_34_1.png differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_50_4.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_50_4.png deleted file mode 100644 index b174b498..00000000 Binary files a/doc/source/pages/other_predictive_models_files/other_predictive_models_50_4.png and /dev/null differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_51_1.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_51_1.png new file mode 100644 index 00000000..b07fe78a Binary files /dev/null and b/doc/source/pages/other_predictive_models_files/other_predictive_models_51_1.png differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_51_2.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_51_2.png deleted file mode 100644 index 76c13cd8..00000000 Binary files a/doc/source/pages/other_predictive_models_files/other_predictive_models_51_2.png and /dev/null differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_52_1.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_52_1.png new file mode 100644 index 00000000..d1491e4d Binary files /dev/null and b/doc/source/pages/other_predictive_models_files/other_predictive_models_52_1.png differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_55_0.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_55_0.png deleted file mode 100644 index 624d6ed4..00000000 Binary files a/doc/source/pages/other_predictive_models_files/other_predictive_models_55_0.png and /dev/null differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_56_0.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_56_0.png index 86713369..fabb2f3b 100644 Binary files a/doc/source/pages/other_predictive_models_files/other_predictive_models_56_0.png and b/doc/source/pages/other_predictive_models_files/other_predictive_models_56_0.png differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_57_0.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_57_0.png new file mode 100644 index 00000000..f15e8bc8 Binary files /dev/null and b/doc/source/pages/other_predictive_models_files/other_predictive_models_57_0.png differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_72_0.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_72_0.png deleted file mode 100644 index e391a4fc..00000000 Binary files a/doc/source/pages/other_predictive_models_files/other_predictive_models_72_0.png and /dev/null differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_73_0.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_73_0.png index 0ce9b332..f18578d9 100644 Binary files a/doc/source/pages/other_predictive_models_files/other_predictive_models_73_0.png and b/doc/source/pages/other_predictive_models_files/other_predictive_models_73_0.png differ diff --git a/doc/source/pages/other_predictive_models_files/other_predictive_models_74_0.png b/doc/source/pages/other_predictive_models_files/other_predictive_models_74_0.png new file mode 100644 index 00000000..0f8766b3 Binary files /dev/null and b/doc/source/pages/other_predictive_models_files/other_predictive_models_74_0.png differ diff --git a/notebooks/pcntk_colab_dev_env.ipynb b/notebooks/pcntk_colab_dev_env.ipynb new file mode 100644 index 00000000..6d5f5408 --- /dev/null +++ b/notebooks/pcntk_colab_dev_env.ipynb @@ -0,0 +1,171 @@ +{ + "nbformat": 4, + "nbformat_minor": 0, + "metadata": { + "colab": { + "provenance": [], + "authorship_tag": "ABX9TyNfYdKn7+C4d4WSym/CFRMQ", + "include_colab_link": true + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "view-in-github", + "colab_type": "text" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "code", + "source": [ + "!pip install https://github.com/amarquand/PCNtoolkit/archive/dev.zip\n", + "!pip install nutpie" + ], + "metadata": { + "id": "vIbnkHN9ydb3" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "# %%\n", + "from warnings import filterwarnings\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import os\n", + "\n", + "from pcntoolkit.normative_model.norm_utils import norm_init\n", + "from pcntoolkit.util.utils import simulate_data\n", + "\n", + "filterwarnings(\"ignore\")\n", + "\n", + "\n", + "########################### Experiment Settings ###############################\n", + "\n", + "\n", + "random_state = 40\n", + "working_dir = \"temp\" # Specify a working directory to save data and results.\n", + "os.makedirs(working_dir, exist_ok=True)\n", + "simulation_method = \"linear\"\n", + "n_features = 1 # The number of input features of X\n", + "n_grps = 3 # Number of batches in data\n", + "n_samples = 500 # Number of samples in each group (use a list for different\n", + "# sample numbers across different batches)\n", + "\n", + "model_type = \"bspline\" # modelto try 'linear, ''polynomial', 'bspline'\n", + "\n", + "\n", + "############################## Data Simulation ################################\n", + "\n", + "\n", + "X_train, Y_train, grp_id_train, X_test, Y_test, grp_id_test, coef = simulate_data(\n", + " simulation_method,\n", + " n_samples,\n", + " n_features,\n", + " n_grps,\n", + " working_dir=working_dir,\n", + " plot=True,\n", + " noise=\"heteroscedastic_nongaussian\",\n", + " random_state=random_state,\n", + ")\n", + "\n", + "################################# Fittig and Predicting ###############################\n", + "\n", + "nm = norm_init(\n", + " X_train,\n", + " Y_train,\n", + " alg=\"hbr\",\n", + " model_type=model_type,\n", + " likelihood=\"SHASHb\",\n", + " linear_sigma=\"True\",\n", + " random_slope_mu=\"False\",\n", + " linear_epsilon=\"False\",\n", + " linear_delta=\"False\",\n", + " nuts_sampler=\"nutpie\",\n", + ")\n", + "\n", + "nm.estimate(X_train, Y_train, trbefile=os.path.join(working_dir, \"trbefile.pkl\"))\n", + "yhat, ys2 = nm.predict(X_test, tsbefile=os.path.join(working_dir, \"tsbefile.pkl\"))\n", + "\n", + "\n", + "################################# Plotting Quantiles ###############################\n", + "for i in range(n_features):\n", + " sorted_idx = X_test[:, i].argsort(axis=0).squeeze()\n", + " temp_X = X_test[sorted_idx, i]\n", + " temp_Y = Y_test[sorted_idx,]\n", + " temp_be = grp_id_test[sorted_idx, :].squeeze()\n", + " temp_yhat = yhat[sorted_idx,]\n", + " temp_s2 = ys2[sorted_idx,]\n", + "\n", + " plt.figure()\n", + " for j in range(n_grps):\n", + " scat1 = plt.scatter(\n", + " temp_X[temp_be == j,], temp_Y[temp_be == j,], label=\"Group\" + str(j)\n", + " )\n", + " # Showing the quantiles\n", + " resolution = 200\n", + " synth_X = np.linspace(np.min(X_train), np.max(X_train), resolution)\n", + " q = nm.get_mcmc_quantiles(synth_X, batch_effects=j * np.ones(resolution))\n", + " col = scat1.get_facecolors()[0]\n", + " plt.plot(synth_X, q.T, linewidth=1, color=col, zorder=0)\n", + "\n", + " plt.title(\"Model %s, Feature %d\" % (model_type, i))\n", + " plt.legend()\n", + " plt.show(block=False)\n", + " plt.savefig(working_dir + \"quantiles_\" + model_type + \"_feature_\" + str(i) + \".png\")\n", + "\n", + " for j in range(n_grps):\n", + " plt.figure()\n", + " plt.scatter(temp_X[temp_be == j,], temp_Y[temp_be == j,])\n", + " plt.plot(temp_X[temp_be == j,], temp_yhat[temp_be == j,], color=\"red\")\n", + " plt.fill_between(\n", + " temp_X[temp_be == j,].squeeze(),\n", + " (temp_yhat[temp_be == j,] - 2 * np.sqrt(temp_s2[temp_be == j,])).squeeze(),\n", + " (temp_yhat[temp_be == j,] + 2 * np.sqrt(temp_s2[temp_be == j,])).squeeze(),\n", + " color=\"red\",\n", + " alpha=0.2,\n", + " )\n", + " plt.title(\"Model %s, Group %d, Feature %d\" % (model_type, j, i))\n", + " plt.show(block=False)\n", + " plt.savefig(\n", + " working_dir\n", + " + \"pred_\"\n", + " + model_type\n", + " + \"_group_\"\n", + " + str(j)\n", + " + \"_feature_\"\n", + " + str(i)\n", + " + \".png\"\n", + " )" + ], + "metadata": { + "id": "RT0EbS7yzCNh" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "3gneUhT80BZZ" + }, + "execution_count": null, + "outputs": [] + } + ] +} \ No newline at end of file diff --git a/notebooks/pcntk_colab_env.ipynb b/notebooks/pcntk_colab_env.ipynb index 1f804dc0..1d1ef1d3 100644 --- a/notebooks/pcntk_colab_env.ipynb +++ b/notebooks/pcntk_colab_env.ipynb @@ -4,7 +4,8 @@ "metadata": { "colab": { "provenance": [], - "authorship_tag": "ABX9TyNfYdKn7+C4d4WSym/CFRMQ", + "authorship_tag": "ABX9TyNpr04OpO9Ta6c7GFsTKsic", + "include_colab_link": true }, "kernelspec": { @@ -13,6 +14,89 @@ }, "language_info": { "name": "python" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "76210a49be1144d58d8ed3b02aadbe72": { + "model_module": "@jupyter-widgets/output", + "model_name": "OutputModel", + "model_module_version": "1.0.0", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_a868d75a2c2849bca3edd651767b54f1", + "msg_id": "", + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": "Sampling chain 0, 0 divergences \u001b[38;2;23;100;244m━━━━━━━━━\u001b[0m\u001b[38;2;23;100;244m╸\u001b[0m\u001b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[35m 25%\u001b[0m \u001b[36m0:32:20\u001b[0m / \u001b[33m0:06:35\u001b[0m\n", + "text/html": "
                            Sampling chain 0, 0 divergences ━━━━━━━━━╸━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  25% 0:32:20 / 0:06:35\n
                            \n" + }, + "metadata": {} + } + ] + } + }, + "a868d75a2c2849bca3edd651767b54f1": { + "model_module": "@jupyter-widgets/base", + "model_name": "LayoutModel", + "model_module_version": "1.2.0", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + } + } } }, "cells": [ @@ -23,12 +107,14 @@ "colab_type": "text" }, "source": [ - "\"Open" + "\"Open" + ] }, { "cell_type": "code", "source": [ + "!pip install https://github.com/amarquand/PCNtoolkit/archive/dev.zip\n", "!pip install nutpie" ], @@ -213,16 +299,51 @@ " )" ], "metadata": { - "id": "RT0EbS7yzCNh" + "colab": { + "base_uri": "https://localhost:8080/", + "height": 129, + "referenced_widgets": [ + "76210a49be1144d58d8ed3b02aadbe72", + "a868d75a2c2849bca3edd651767b54f1" + ] + }, + "id": "CuoBYP1g2I20", + "outputId": "ca033c81-ab9b-4cda-938d-73e19c7360ca" }, "execution_count": null, - "outputs": [] + "outputs": [ + { + "output_type": "stream", + "name": "stderr", + "text": [ + "/usr/local/lib/python3.10/dist-packages/pytensor/tensor/random/op.py:84: FutureWarning: ndim_supp is deprecated. Provide signature instead.\n", + " warnings.warn(\n", + "/usr/local/lib/python3.10/dist-packages/pytensor/tensor/random/op.py:94: FutureWarning: ndims_params is deprecated. Provide signature instead.\n", + " warnings.warn(\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "Output()" + ], + "application/vnd.jupyter.widget-view+json": { + "version_major": 2, + "version_minor": 0, + "model_id": "76210a49be1144d58d8ed3b02aadbe72" + } + }, + "metadata": {} + } + }, { "cell_type": "code", "source": [], "metadata": { - "id": "3gneUhT80BZZ" + "id": "L9gKL8bz2Jj4" + }, "execution_count": null, "outputs": [] diff --git a/pcntoolkit/model/KnuOp.py b/pcntoolkit/model/KnuOp.py new file mode 100644 index 00000000..35e93302 --- /dev/null +++ b/pcntoolkit/model/KnuOp.py @@ -0,0 +1,48 @@ +# Third-party imports +import scipy.special as spp +from pytensor.gradient import grad_not_implemented +from pytensor.scalar.basic import BinaryScalarOp, upgrade_to_float + + +class KnuOp(BinaryScalarOp): + """ + Modified Bessel function of the second kind, pytensor wrapper for scipy.special.kv + """ + + nfunc_spec = ("scipy.special.kv", 2, 1) + + @staticmethod + def st_impl(p, x): + return spp.kv(p, x) + + def impl(self, p, x): + return KnuOp.st_impl(p, x) + + def grad(self, inputs, grads): + dp = 1e-16 + (p, x) = inputs + (gz,) = grads + dfdp = (knuop(p + dp, x) - knuop(p - dp, x)) / (2 * dp) + return [gz * dfdp, gz * knupop(p, x)] + + +class KnuPrimeOp(BinaryScalarOp): + """ + Derivative of the modified Bessel function of the second kind. + """ + + nfunc_spec = ("scipy.special.kvp", 2, 1) + + @staticmethod + def st_impl(p, x): + return spp.kvp(p, x) + + def impl(self, p, x): + return KnuPrimeOp.st_impl(p, x) + + def grad(self, inputs, grads): + return [grad_not_implemented(self, 0, "p"), grad_not_implemented(self, 1, "x")] + + +knuop = KnuOp(upgrade_to_float, name="knuop") +knupop = KnuPrimeOp(upgrade_to_float, name="knupop") diff --git a/pcntoolkit/model/SHASH.py b/pcntoolkit/model/SHASH.py index 39bf0646..c00585a9 100644 --- a/pcntoolkit/model/SHASH.py +++ b/pcntoolkit/model/SHASH.py @@ -1,206 +1,245 @@ -from typing import Union, List, Optional -import pymc as pm -from pymc import floatX -from pymc.distributions import Continuous - -import pytensor as pt -import pytensor.tensor as ptt -from pytensor.graph.op import Op -from pytensor.graph import Apply -from pytensor.gradient import grad_not_implemented -from pytensor.tensor.random.basic import normal -from pytensor.tensor.random.op import RandomVariable - - -import numpy as np -import scipy.special as spp -import matplotlib.pyplot as plt - - """ @author: Stijn de Boer (AuguB) See: Jones et al. (2009), Sinh-Arcsinh distributions. """ +from functools import lru_cache -def numpy_P(q): - """ - The P function as given in Jones et al. - :param q: - :return: - """ - frac = np.exp(1.0 / 4.0) / np.power(8.0 * np.pi, 1.0 / 2.0) - K1 = numpy_K((q + 1) / 2, 1.0 / 4.0) - K2 = numpy_K((q - 1) / 2, 1.0 / 4.0) - a = (K1 + K2) * frac - return a - - -def numpy_K(p, x): - """ - Computes the values of spp.kv(p,x) for only the unique values of p - """ - - ps, idxs = np.unique(p, return_inverse=True) - return spp.kv(ps, x)[idxs].reshape(p.shape) - +import numpy as np +from pymc import floatX +from pymc.distributions import Continuous +from pytensor.tensor import as_tensor_variable +from pytensor.tensor.elemwise import Elemwise +from pytensor.tensor.random.op import RandomVariable +from scipy.special import kv -class K(Op): - """ - Modified Bessel function of the second kind, pytensor implementation - """ +from pcntoolkit.model.KnuOp import knuop - __props__ = () +##### Constants ##### - def make_node(self, p, x): - p = pt.tensor.as_tensor_variable(p) - x = pt.tensor.as_tensor_variable(x) - return Apply(self, [p, x], [p.type()]) +CONST1 = np.exp(0.25) / np.power(8.0 * np.pi, 0.5) +CONST2 = -np.log(2 * np.pi) / 2 - def perform(self, node, inputs_storage, output_storage): - # Doing this on the unique values avoids doing A LOT OF double work, apparently scipy doesn't do this by itself +##### SHASH Transformations ##### - unique_inputs, inverse_indices = np.unique( - 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) - output_storage[0][0] = outputs - def grad(self, inputs, output_grads): - # Approximation of the derivative. This should suffice for using NUTS - dp = 1e-10 - p = inputs[0] - x = inputs[1] - grad = (self(p + dp, x) - self(p, x)) / dp - return [output_grads[0] * grad, grad_not_implemented(0, 1, 2, 3)] +def S(x, epsilon, delta): + """Sinh-arcsinh transformation. + Args: + x: input value + epsilon: parameter for skew + delta: parameter for kurtosis -def S(x, epsilon, delta): - """ - :param epsilon: - :param delta: - :param x: - :return: The sinharcsinh transformation of x + Returns: + Sinh-arcsinh transformed value """ return np.sinh(np.arcsinh(x) * delta - epsilon) def S_inv(x, epsilon, delta): + """Inverse sinh-arcsinh transformation. + + Args: + x: input value + epsilon: parameter for skew + delta: parameter for kurtosis + + Returns: + Inverse sinh-arcsinh transformed value + """ return np.sinh((np.arcsinh(x) + epsilon) / delta) def C(x, epsilon, delta): - """ - :param epsilon: - :param delta: - :param x: - :return: the cosharcsinh transformation of x - Be aware that this is sqrt(1+S(x)^2), so you may save some compute if you can re-use the result from S. + """Cosh-arcsinh transformation. + + Args: + x: input value + epsilon: parameter for skew + delta: parameter for kurtosis + + Returns: + The cosh-arcsinh transformation of x. + + Note: C(x) = sqrt(1+S(x)^2) """ return np.cosh(np.arcsinh(x) * delta - epsilon) -def P(q): - """ - The P function as given in Jones et al. - :param q: - :return: - """ - frac = np.exp(1.0 / 4.0) / np.power(8.0 * np.pi, 1.0 / 2.0) - K1 = K()((q + 1) / 2, 1.0 / 4.0) - K2 = K()((q - 1) / 2, 1.0 / 4.0) - a = (K1 + K2) * frac - return a +##### SHASH Distributions ##### -def m(epsilon, delta, r): - """ - :param epsilon: - :param delta: - :param r: - :return: The r'th uncentered moment of the SHASH distribution parameterized by epsilon and delta. Given by Jones et al. - The first four moments are given in closed form. - """ - if r == 1: - return np.sinh(epsilon / delta) * P(1 / delta) - elif r == 2: - return (np.cosh(2 * epsilon / delta) * P(2 / delta) - 1) / 2 - elif r == 3: - return ( - np.sinh(3 * epsilon / delta) * P(3 / delta) - - 3 * np.sinh(epsilon / delta) * P(1 / delta) - ) / 4 - elif r == 4: - return ( - np.cosh(4 * epsilon / delta) * P(4 / delta) - - 4 * np.cosh(2 * epsilon / delta) * P(2 / delta) - + 3 - ) / 8 - # else: - # frac1 = ptt.as_tensor_variable(1 / pm.power(2, r)) - # acc = ptt.as_tensor_variable(0) - # for i in range(r + 1): - # combs = spp.comb(r, i) - # flip = pm.power(-1, i) - # ex = np.exp((r - 2 * i) * epsilon / delta) - # p = P((r - 2 * i) / delta) - # acc += combs * flip * ex * p - # return frac1 * acc - - -class SHASH(RandomVariable): +class SHASHrv(RandomVariable): + """SHASH RV, described by Jones et al., based on a standard normal distribution.""" + name = "shash" - ndim_supp = 0 - ndims_params = [0, 0] + signature = "(),()->()" dtype = "floatX" _print_name = ("SHASH", "\\operatorname{SHASH}") @classmethod - def rng_fn(cls, rng, epsilon, delta, size=None) -> np.ndarray: + def rng_fn(cls, rng, epsilon, delta, size=None): + """Draw random samples from SHASH distribution. + + Args: + rng: Random number generator + epsilon: skew parameter + delta: kurtosis parameter + size: sample size. Defaults to None. + + Returns: + Random samples from SHASH distribution + """ return np.sinh( (np.arcsinh(rng.normal(loc=0, scale=1, size=size)) + epsilon) / delta ) -shash = SHASH() +shash = SHASHrv() class SHASH(Continuous): rv_op = shash """ - SHASH described by Jones et al., based on a standard normal distribution. + SHASH distribution described by Jones et al., based on a standard normal distribution. """ + # Instance of the KOp + my_K = Elemwise(knuop) + + @staticmethod + @lru_cache(maxsize=128) + def P(q): + """The P function as given in Jones et al. + + Args: + q: input parameter for the P function + + Returns: + Result of the P function computation + """ + K1 = SHASH.my_K((q + 1) / 2, 0.25) + K2 = SHASH.my_K((q - 1) / 2, 0.25) + a = (K1 + K2) * CONST1 + return a + + @staticmethod + def m1(epsilon, delta): + """The first moment of the SHASH distribution parametrized by epsilon and delta. + + Args: + epsilon: skew parameter + delta: kurtosis parameter + + Returns: + First moment of the SHASH distribution + """ + return np.sinh(epsilon / delta) * SHASH.P(1 / delta) + + @staticmethod + def m2(epsilon, delta): + """The second moment of the SHASH distribution parametrized by epsilon and delta. + + Args: + epsilon: skew parameter + delta: kurtosis parameter + + Returns: + Second moment of the SHASH distribution + """ + return (np.cosh(2 * epsilon / delta) * SHASH.P(2 / delta) - 1) / 2 + + @staticmethod + def m1m2(epsilon, delta): + """Compute both first and second moments together to avoid redundant calculations. + + Args: + epsilon: skew parameter + delta: kurtosis parameter + + Returns: + Tuple containing (mean, variance) of the SHASH distribution + """ + inv_delta = 1.0 / delta + two_inv_delta = 2.0 * inv_delta + + # Compute P values once + p1 = SHASH.P(inv_delta) + p2 = SHASH.P(two_inv_delta) + + # Compute trig terms once + eps_delta = epsilon / delta + sinh_eps_delta = np.sinh(eps_delta) + cosh_2eps_delta = np.cosh(2 * eps_delta) + + # Compute moments + mean = sinh_eps_delta * p1 + raw_second = (cosh_2eps_delta * p2 - 1) / 2 + var = raw_second - mean**2 + return mean, var + @classmethod def dist(cls, epsilon, delta, **kwargs): - epsilon = ptt.as_tensor_variable(floatX(epsilon)) - delta = ptt.as_tensor_variable(floatX(delta)) + """Return a SHASH distribution. + + Args: + epsilon: skew parameter + delta: kurtosis parameter + **kwargs: Additional arguments passed to the distribution + + Returns: + A SHASH distribution + """ + epsilon = as_tensor_variable(floatX(epsilon)) + delta = as_tensor_variable(floatX(delta)) return super().dist([epsilon, delta], **kwargs) def logp(value, epsilon, delta): + """Log-probability of the SHASH distribution. + + Args: + value: value to evaluate the log-probability at + epsilon: skew parameter + delta: kurtosis parameter + + Returns: + Log-probability of the SHASH distribution + """ this_S = S(value, epsilon, delta) - this_S_sqr = ptt.sqr(this_S) + this_S_sqr = np.log(this_S) 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 - ) + frac2 = np.log(delta) + np.log(this_C_sqr) / 2 - np.log(1 + np.log(value)) / 2 exp = -this_S_sqr / 2 - return frac1 + frac2 + exp + return CONST2 + frac2 + exp class SHASHoRV(RandomVariable): + """SHASHo Random Variable. + + Samples from a SHASHo distribution, which is a SHASH distribution scaled by sigma and translated by mu. + """ + name = "shasho" - ndim_supp = 0 - ndims_params = [0, 0, 0, 0] + signature = "(),(),(),()->()" dtype = "floatX" _print_name = ("SHASHo", "\\operatorname{SHASHo}") @classmethod - def rng_fn(cls, rng, mu, sigma, epsilon, delta, size=None) -> np.ndarray: + def rng_fn(cls, rng, mu, sigma, epsilon, delta, size=None): + """Draw random samples from a SHASHo distribution. + + Args: + rng: Random number generator + mu: location parameter + sigma: scale parameter + epsilon: skew parameter + delta: kurtosis parameter + size: sample size. Defaults to None. + + Returns: + Random samples from SHASHo distribution + """ s = rng.normal(size=size) return np.sinh((np.arcsinh(s) + epsilon) / delta) * sigma + mu @@ -209,43 +248,84 @@ def rng_fn(cls, rng, mu, sigma, epsilon, delta, size=None) -> np.ndarray: class SHASHo(Continuous): + """SHASHo distribution, which is a SHASH distribution scaled by sigma and translated by mu.""" + rv_op = shasho - """ - This is the transformation where the location and scale parameters have simply been applied as an linear transformation directly on the original distribution. - """ @classmethod def dist(cls, mu, sigma, epsilon, delta, **kwargs): - mu = ptt.as_tensor_variable(floatX(mu)) - sigma = ptt.as_tensor_variable(floatX(sigma)) - epsilon = ptt.as_tensor_variable(floatX(epsilon)) - delta = ptt.as_tensor_variable(floatX(delta)) + """Return a SHASHo distribution. + + Args: + mu: location parameter + sigma: scale parameter + epsilon: skew parameter + delta: kurtosis parameter + **kwargs: Additional arguments passed to the distribution + + Returns: + A SHASHo distribution + """ + mu = as_tensor_variable(floatX(mu)) + sigma = as_tensor_variable(floatX(sigma)) + epsilon = as_tensor_variable(floatX(epsilon)) + delta = as_tensor_variable(floatX(delta)) return super().dist([mu, sigma, epsilon, delta], **kwargs) def logp(value, mu, sigma, epsilon, delta): + """The log-probability of the SHASHo distribution. + + Args: + value: value to evaluate the log-probability at + mu: location parameter + sigma: scale parameter + epsilon: skew parameter + delta: kurtosis parameter + + Returns: + Log-probability of the SHASHo distribution + """ remapped_value = (value - mu) / sigma this_S = S(remapped_value, epsilon, delta) - this_S_sqr = ptt.sqr(this_S) + this_S_sqr = np.log(this_S) 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(remapped_value)) / 2 + np.log(delta) + + np.log(this_C_sqr) / 2 + - np.log(1 + np.log(remapped_value)) / 2 ) exp = -this_S_sqr / 2 - return frac1 + frac2 + exp - ptt.log(sigma) + return CONST2 + frac2 + exp - np.log(sigma) class SHASHo2RV(RandomVariable): + """SHASHo2 Random Variable. + + Samples from a SHASHo2 distribution, which is a SHASH distribution scaled by sigma/delta + and translated by mu. This variant provides an alternative parameterization where the + scale parameter is adjusted by the kurtosis parameter. + """ + name = "shasho2" - ndim_supp = 0 - ndims_params = [0, 0, 0, 0] + signature = "(),(),(),()->()" dtype = "floatX" _print_name = ("SHASHo2", "\\operatorname{SHASHo2}") @classmethod - def rng_fn(cls, rng, mu, sigma, epsilon, delta, size=None) -> np.ndarray: + def rng_fn(cls, rng, mu, sigma, epsilon, delta, size=None): + """Draw random samples from SHASHo2 distribution. + + Args: + rng: Random number generator + mu: location parameter + sigma: scale parameter + epsilon: skew parameter + delta: kurtosis parameter + size: sample size. Defaults to None. + + Returns: + Random samples from SHASHo2 distribution + """ s = rng.normal(size=size) sigma_d = sigma / delta return np.sinh((np.arcsinh(s) + epsilon) / delta) * sigma_d + mu @@ -255,56 +335,113 @@ def rng_fn(cls, rng, mu, sigma, epsilon, delta, size=None) -> np.ndarray: class SHASHo2(Continuous): - rv_op = shasho2 - """ - This is the reparameterization where we apply the transformation provided in section 4.3 in Jones et al. + """SHASHo2 distribution, which is a SHASH distribution scaled by sigma/delta and translated by mu. + + This distribution provides an alternative parameterization of the SHASH distribution where + the scale parameter is adjusted by the kurtosis parameter. This can be useful in scenarios + where the relationship between scale and kurtosis needs to be explicitly modeled. """ + rv_op = shasho2 + @classmethod def dist(cls, mu, sigma, epsilon, delta, **kwargs): - mu = ptt.as_tensor_variable(floatX(mu)) - sigma = ptt.as_tensor_variable(floatX(sigma)) - epsilon = ptt.as_tensor_variable(floatX(epsilon)) - delta = ptt.as_tensor_variable(floatX(delta)) + """Return a SHASHo2 distribution. + + Args: + mu: location parameter + sigma: scale parameter + epsilon: skew parameter + delta: kurtosis parameter + **kwargs: Additional arguments passed to the distribution + + Returns: + A SHASHo2 distribution + """ + mu = as_tensor_variable(floatX(mu)) + sigma = as_tensor_variable(floatX(sigma)) + epsilon = as_tensor_variable(floatX(epsilon)) + delta = as_tensor_variable(floatX(delta)) return super().dist([mu, sigma, epsilon, delta], **kwargs) def logp(value, mu, sigma, epsilon, delta): + """The log-probability of the SHASHo2 distribution. + + Args: + value: value to evaluate the log-probability at + mu: location parameter + sigma: scale parameter + epsilon: skew parameter + delta: kurtosis parameter + + Returns: + Log-probability of the SHASHo2 distribution + """ sigma_d = sigma / delta remapped_value = (value - mu) / sigma_d this_S = S(remapped_value, epsilon, delta) - this_S_sqr = ptt.sqr(this_S) + this_S_sqr = np.log(this_S) 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(remapped_value)) / 2 + np.log(delta) + + np.log(this_C_sqr) / 2 + - np.log(1 + np.log(remapped_value)) / 2 ) exp = -this_S_sqr / 2 - return frac1 + frac2 + exp - ptt.log(sigma_d) + return CONST2 + frac2 + exp - np.log(sigma_d) class SHASHbRV(RandomVariable): + """SHASHb Random Variable. + + Samples from a SHASHb distribution, which is a standardized SHASH distribution scaled by sigma + and translated by mu. This variant provides a standardized version of the SHASH distribution + where the base distribution is normalized to have zero mean and unit variance before applying + the location and scale transformations. + """ + name = "shashb" - ndim_supp = 0 - ndims_params = [0, 0, 0, 0] + signature = "(),(),(),()->()" dtype = "floatX" - _print_name = ("SHASHo2", "\\operatorname{SHASHo2}") + _print_name = ("SHASHb", "\\operatorname{SHASHb}") @classmethod - def rng_fn( - cls, - rng: np.random.RandomState, - mu: Union[np.ndarray, float], - sigma: Union[np.ndarray, float], - epsilon: Union[np.ndarray, float], - delta: Union[np.ndarray, float], - size: Optional[Union[List[int], int]], - ) -> np.ndarray: + def rng_fn(cls, rng, mu, sigma, epsilon, delta, size=None): + """Draw random samples from SHASHb distribution. + + Args: + rng: Random number generator + mu: location parameter + sigma: scale parameter + epsilon: skew parameter + delta: kurtosis parameter + size: sample size. Defaults to None. + + Returns: + Random samples from SHASHb distribution + """ 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 + + def P(q): + K1 = kv((q + 1) / 2, 0.25) + K2 = kv((q - 1) / 2, 0.25) + a = (K1 + K2) * CONST1 + return a + + def m1m2(epsilon, delta): + inv_delta = 1.0 / delta + two_inv_delta = 2.0 * inv_delta + p1 = P(inv_delta) + p2 = P(two_inv_delta) + eps_delta = epsilon / delta + sinh_eps_delta = np.sinh(eps_delta) + cosh_2eps_delta = np.cosh(2 * eps_delta) + mean = sinh_eps_delta * p1 + raw_second = (cosh_2eps_delta * p2 - 1) / 2 + var = raw_second - mean**2 + return mean, var + + mean, var = m1m2(epsilon, delta) out = ( (np.sinh((np.arcsinh(s) + epsilon) / delta) - mean) / np.sqrt(var) ) * sigma + mu @@ -315,31 +452,58 @@ def rng_fn( class SHASHb(Continuous): - rv_op = shashb - """ - 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. + """SHASHb distribution, which is a standardized SHASH distribution scaled by sigma and translated by mu. + + This distribution provides a standardized version of the SHASH distribution where the base + distribution is normalized to have zero mean and unit variance before applying the location + and scale transformations. This transformation aims to remove the correlation between the + parameters, which can be useful in MCMC sampling. """ + rv_op = shashb + @classmethod def dist(cls, mu, sigma, epsilon, delta, **kwargs): - mu = ptt.as_tensor_variable(floatX(mu)) - sigma = ptt.as_tensor_variable(floatX(sigma)) - epsilon = ptt.as_tensor_variable(floatX(epsilon)) - delta = ptt.as_tensor_variable(floatX(delta)) + """Return a SHASHb distribution. + + Args: + mu: location parameter + sigma: scale parameter + epsilon: skew parameter + delta: kurtosis parameter + **kwargs: Additional arguments passed to the distribution + + Returns: + A SHASHb distribution + """ + mu = as_tensor_variable(floatX(mu)) + sigma = as_tensor_variable(floatX(sigma)) + epsilon = as_tensor_variable(floatX(epsilon)) + delta = as_tensor_variable(floatX(delta)) return super().dist([mu, sigma, epsilon, delta], **kwargs) def logp(value, mu, sigma, epsilon, delta): - mean = m(epsilon, delta, 1) - var = m(epsilon, delta, 2) - mean**2 + """The log-probability of the SHASHb distribution. + + Args: + value: value to evaluate the log-probability at + mu: location parameter + sigma: scale parameter + epsilon: skew parameter + delta: kurtosis parameter + + Returns: + Log-probability of the SHASHb distribution + """ + mean, var = SHASH.m1m2(epsilon, delta) remapped_value = ((value - mu) / sigma) * np.sqrt(var) + mean this_S = S(remapped_value, epsilon, delta) this_S_sqr = np.square(this_S) this_C_sqr = 1 + this_S_sqr - frac1 = -np.log(2 * np.pi) / 2 frac2 = ( np.log(delta) + np.log(this_C_sqr) / 2 - np.log(1 + np.square(remapped_value)) / 2 ) exp = -this_S_sqr / 2 - return frac1 + frac2 + exp + np.log(var) / 2 - np.log(sigma) + return CONST2 + frac2 + exp + np.log(var) / 2 - np.log(sigma) diff --git a/pcntoolkit/model/architecture.py b/pcntoolkit/model/architecture.py index 0dfb09c9..8894ce3f 100644 --- a/pcntoolkit/model/architecture.py +++ b/pcntoolkit/model/architecture.py @@ -46,7 +46,8 @@ def __init__(self, x, y, args): # 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) + # in:(90,108,90) out:(43,52,43) + dilation=1, groups=self.factor, bias=True) 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=[ @@ -57,7 +58,8 @@ def __init__(self, x, y, args): # 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) + # out: (21,25,21) + dilation=1, groups=self.factor, bias=True) 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=[ @@ -68,7 +70,8 @@ def __init__(self, x, y, args): # 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) + # out: (10,12,10) + dilation=1, groups=self.factor, bias=True) 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=[ @@ -79,7 +82,8 @@ def __init__(self, x, y, args): # 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) + # out: (4,5,4) + dilation=1, groups=1, bias=True) 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=[ diff --git a/pcntoolkit/model/hbr.py b/pcntoolkit/model/hbr.py index 77c60f1d..45b6fbe6 100644 --- a/pcntoolkit/model/hbr.py +++ b/pcntoolkit/model/hbr.py @@ -225,17 +225,17 @@ def get_sample_dims(var): return None with pm.Model(coords=pb.coords) as model: - model.add_coord("datapoints", np.arange(X.shape[0]), mutable=True) - X = pm.MutableData("X", X, dims=("datapoints", "basis_functions")) + model.add_coord("datapoints", np.arange(X.shape[0])) + X = pm.Data("X", X, dims=("datapoints", "basis_functions")) pb.X = X - y = pm.MutableData("y", np.squeeze(y), dims="datapoints") + y = pm.Data("y", np.squeeze(y), dims="datapoints") + pb.y = y pb.model = model pb.batch_effect_indices = tuple( [ pm.Data( - pb.batch_effect_dim_names[i], + pb.batch_effect_dim_names[i]+"_data", pb.batch_effect_indices[i], - mutable=True, dims="datapoints", ) for i in range(len(pb.batch_effect_indices)) @@ -247,25 +247,35 @@ def get_sample_dims(var): "mu_samples", pb.make_param( "mu", - mu_slope_mu_params=(0.0, 3.0), - sigma_slope_mu_params=(3.0,), - mu_intercept_mu_params=(0.0, 3.0), - sigma_intercept_mu_params=(3.0,), + intercept_mu_params=(0.0, 10.0), + slope_mu_params=(0.0, 10.0), + mu_slope_mu_params=(0.0, 10.0), + sigma_slope_mu_params=(10.0,), + mu_intercept_mu_params=(0.0, 10.0), + sigma_intercept_mu_params=(10.0,), ).get_samples(pb), dims=get_sample_dims('mu'), ) sigma = pm.Deterministic( "sigma_samples", pb.make_param( - "sigma", mu_sigma_params=(0.0, 2.0), sigma_sigma_params=(2.0,) + "sigma", + sigma_params=(10., 10.0), + sigma_dist="normal", + slope_sigma_params=(0.0, 10.0), + intercept_sigma_params=(10.0, 10.0), ).get_samples(pb), dims=get_sample_dims('sigma'), ) sigma_plus = pm.Deterministic( - "sigma_plus_samples", pm.math.log(1 + pm.math.exp(sigma/3))*3, dims=get_sample_dims('sigma') + "sigma_plus_samples", np.log(1+np.exp(sigma/10))*10, dims=get_sample_dims('sigma') ) y_like = pm.Normal( - "y_like", mu, sigma=sigma_plus, observed=y, dims="datapoints" + "y_like", + mu=mu, + sigma=sigma_plus, + observed=y, + dims="datapoints", ) elif configs["likelihood"] in ["SHASHb", "SHASHo", "SHASHo2"]: @@ -285,11 +295,12 @@ def get_sample_dims(var): "mu_samples", pb.make_param( "mu", - slope_mu_params=(0.0, 2.0), - mu_slope_mu_params=(0.0, 2.0), - sigma_slope_mu_params=(2.0,), - mu_intercept_mu_params=(0.0, 2.0), - sigma_intercept_mu_params=(2.0,), + intercept_mu_params=(0.0, 10.0), + slope_mu_params=(0.0, 10.0), + mu_slope_mu_params=(0.0, 10.0), + sigma_slope_mu_params=(10.0,), + mu_intercept_mu_params=(0.0, 10.0), + sigma_intercept_mu_params=(10.0,), ).get_samples(pb), dims=get_sample_dims('mu'), ) @@ -297,23 +308,23 @@ def get_sample_dims(var): "sigma_samples", pb.make_param( "sigma", - sigma_params=(1.0, 1.0), + sigma_params=(10., 10.0), sigma_dist="normal", - slope_sigma_params=(0.0, 1.0), - intercept_sigma_params=(1.0, 1.0), + slope_sigma_params=(0.0, 10.0), + intercept_sigma_params=(10.0, 10.0), ).get_samples(pb), dims=get_sample_dims('sigma'), ) sigma_plus = pm.Deterministic( - "sigma_plus_samples", np.log(1 + np.exp(sigma)), dims=get_sample_dims('sigma') + "sigma_plus_samples", np.log(1+np.exp(sigma/10))*10, dims=get_sample_dims('sigma') ) epsilon = pm.Deterministic( "epsilon_samples", pb.make_param( "epsilon", - epsilon_params=(0.0, 1.0), - slope_epsilon_params=(0.0, 0.2), - intercept_epsilon_params=(0.0, 0.2), + epsilon_params=(0.0, 2.0), + slope_epsilon_params=(0.0, 3.0), + intercept_epsilon_params=(0.0, 3.0), ).get_samples(pb), dims=get_sample_dims('epsilon'), ) @@ -321,16 +332,16 @@ def get_sample_dims(var): "delta_samples", pb.make_param( "delta", - delta_params=(1.0, 1.0), + delta_params=(0., 2.0), delta_dist="normal", - slope_delta_params=(0.0, 0.2), - intercept_delta_params=(1.0, 0.3), + slope_delta_params=(0.0, 1.0), + intercept_delta_params=(0.0, 1.0), ).get_samples(pb), dims=get_sample_dims('delta'), ) delta_plus = pm.Deterministic( "delta_plus_samples", - np.log(1 + np.exp(delta * 10)) / 10 + 0.3, + np.log(1+np.exp(delta/3))*3 + 0.3, dims=get_sample_dims('delta'), ) y_like = SHASH_map[configs["likelihood"]]( @@ -448,6 +459,7 @@ def estimate(self, X, y, batch_effects, **kwargs): init=self.configs["init"], n_init=500000, cores=self.configs["cores"], + nuts_sampler=self.configs["nuts_sampler"], ) self.vars_to_sample = ['y_like'] if self.configs['remove_datapoints_from_posterior']: @@ -520,7 +532,7 @@ def predict( # Compute those indices for the test data indices = list(map(lambda x: valmap[x], batch_effects[:, i])) # Those indices need to be used by the model - pm.set_data({f"batch_effect_{i}": indices}) + pm.set_data({f"batch_effect_{i}_data": indices}) self.idata = pm.sample_posterior_predictive( trace=self.idata, @@ -557,8 +569,9 @@ def estimate_on_new_site(self, X, y, batch_effects): chains=self.configs["n_chains"], target_accept=self.configs["target_accept"], init=self.configs["init"], - n_init=50000, + n_init=500000, cores=self.configs["cores"], + nuts_sampler=self.configs["nuts_sampler"], ) return self.idata @@ -749,6 +762,7 @@ def __init__(self, name, dist, params, pb, has_random_effect=False) -> None: "hcauchy": pm.HalfCauchy, "hstudt": pm.HalfStudentT, "studt": pm.StudentT, + "lognormal": pm.LogNormal, } self.make_dist(dist, params, pb) @@ -1011,8 +1025,8 @@ def get_samples(self, pb: ParamBuilder): :return: The samples from the parameterization. """ with pb.model: - samples = self.dist[pb.batch_effect_indices] - return samples + return self.dist[pb.batch_effect_indices] + class NonCentralRandomFixedParameterization(Parameterization): @@ -1070,8 +1084,7 @@ def get_samples(self, pb: ParamBuilder): :return: The samples from the parameterization. """ with pb.model: - samples = self.dist[pb.batch_effect_indices] - return samples + return self.dist[pb.batch_effect_indices] class LinearParameterization(Parameterization): @@ -1107,15 +1120,19 @@ def get_samples(self, pb): :return: The samples from the parameterization. """ with pb.model: - intc = self.intercept_parameterization.get_samples(pb) + intercept_samples = self.intercept_parameterization.get_samples(pb) slope_samples = self.slope_parameterization.get_samples(pb) + if pb.configs[f"random_slope_{self.name}"]: - slope = pb.X * slope_samples - slope = slope.sum(axis=-1) + if slope_samples.shape.eval()[1] > 1: + slope = pm.math.sum( + pb.X * slope_samples, axis=1) + else: + slope = pb.X *slope_samples else: - slope = pb.X @ self.slope_parameterization.get_samples(pb) + slope = pb.X @ slope_samples - samples = pm.math.flatten(intc) + pm.math.flatten(slope) + samples = pm.math.flatten(intercept_samples) + pm.math.flatten(slope) return samples diff --git a/pcntoolkit/normative.py b/pcntoolkit/normative.py index 1c62737c..1f160e84 100755 --- a/pcntoolkit/normative.py +++ b/pcntoolkit/normative.py @@ -11,25 +11,37 @@ # Written by A. Marquand # ------------------------------------------------------------------------------ -from __future__ import print_function -from __future__ import division +from __future__ import division, print_function -import os -import sys -import numpy as np import argparse -import pickle import glob +import os +import pickle +import sys +from pathlib import Path +import numpy as np from sklearn.model_selection import KFold -from pathlib import Path + +try: + import nutpie +except ImportError: + # warnings.warn("Nutpie not installed. For fitting HBR models with the nutpie backend, install it with `conda install nutpie numba`") + pass + try: # run as a package if installed from pcntoolkit import configs from pcntoolkit.dataio import fileio from pcntoolkit.normative_model.norm_utils import norm_init - from pcntoolkit.util.utils import compute_pearsonr, CustomCV, explained_var - from pcntoolkit.util.utils import compute_MSLL, scaler, get_package_versions + from pcntoolkit.util.utils import ( + CustomCV, + compute_MSLL, + compute_pearsonr, + explained_var, + get_package_versions, + scaler, + ) except ImportError: pass @@ -41,10 +53,15 @@ import configs from dataio import fileio - - from util.utils import compute_pearsonr, CustomCV, explained_var, compute_MSLL - from util.utils import scaler, get_package_versions from normative_model.norm_utils import norm_init + from util.utils import ( + CustomCV, + compute_MSLL, + compute_pearsonr, + explained_var, + get_package_versions, + scaler, + ) PICKLE_PROTOCOL = configs.PICKLE_PROTOCOL @@ -92,71 +109,61 @@ def get_args(*args): :returns configparam: Parameters controlling the estimation algorithm :returns kw_args: Additional keyword arguments """ - + args = args[0][0] # parse arguments parser = argparse.ArgumentParser(description="Normative Modeling") - parser.add_argument("responses") - parser.add_argument("-f", help="Function to call", dest="func", - default="estimate") + parser.add_argument("respfile", help="Response variables for the normative model") + 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", - default=None) - parser.add_argument("-k", help="cross-validation folds", dest="cvfolds", - default=None) - parser.add_argument("-t", help="covariates (test data)", dest="testcov", - default=None) - parser.add_argument("-r", help="responses (test data)", dest="testresp", - default=None) + parser.add_argument("-c", help="covariates file", dest="covfile", default=None) + parser.add_argument("-k", help="cross-validation folds", dest="cvfolds", default=None) + parser.add_argument("-t", help="covariates (test data)", dest="testcov", default=None) + 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", - dest="configparam", default=None) - # 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() + parser.add_argument("-x", help="algorithm specific config options", dest="configparam", default=None) + parsed_args, keyword_args = parser.parse_known_args(args) - # Process required arguemnts + # Process required arguments wdir = os.path.realpath(os.path.curdir) - respfile = os.path.join(wdir, args.responses) - if args.covfile is None: + respfile = os.path.join(wdir, parsed_args.respfile) + if parsed_args.covfile is None: raise ValueError("No covariates specified") else: - covfile = args.covfile + covfile = parsed_args.covfile # Process optional arguments - if args.maskfile is None: + if parsed_args.maskfile is None: maskfile = None else: - maskfile = os.path.join(wdir, args.maskfile) - if args.testcov is None and args.cvfolds is not None: + maskfile = os.path.join(wdir, parsed_args.maskfile) + if parsed_args.testcov is None and parsed_args.cvfolds is not None: testcov = None testresp = None - cvfolds = int(args.cvfolds) + cvfolds = int(parsed_args.cvfolds) print("Running under " + str(cvfolds) + " fold cross-validation.") else: print("Test covariates specified") - testcov = args.testcov + testcov = parsed_args.testcov cvfolds = None - if args.testresp is None: + if parsed_args.testresp is None: testresp = None print("No test response variables specified") else: - testresp = args.testresp - if args.cvfolds is not None: + testresp = parsed_args.testresp + if parsed_args.cvfolds is not None: print("Ignoring cross-valdation specification (test data given)") # Process addtional keyword arguments. These are always added as strings kw_args = {} - for kw in args.keyword_args: + for kw in keyword_args: kw_arg = kw.split('=') 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, parsed_args.func, parsed_args.alg, \ + parsed_args.configparam, kw_args def evaluate(Y, Yhat, S2=None, mY=None, sY=None, nlZ=None, nm=None, Xz_tr=None, alg=None, @@ -369,7 +376,9 @@ def estimate(covfile, respfile, **kwargs): # '_' is in the outputsuffix to # avoid file name parsing problem. inscaler = kwargs.pop('inscaler', 'None') + print(f"inscaler: {inscaler}") outscaler = kwargs.pop('outscaler', 'None') + print(f"outscaler: {outscaler}") warp = kwargs.get('warp', None) # convert from strings if necessary @@ -522,7 +531,8 @@ 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) @@ -651,6 +661,8 @@ def fit(covfile, respfile, **kwargs): outputsuffix = "_" + outputsuffix.replace("_", "") inscaler = kwargs.pop('inscaler', 'None') outscaler = kwargs.pop('outscaler', 'None') + print(f"inscaler: {inscaler}") + print(f"outscaler: {outscaler}") if savemodel and not os.path.isdir('Models'): os.mkdir('Models') @@ -804,7 +816,7 @@ def predict(covfile, respfile, maskfile=None, **kwargs): Y, maskvol = load_response_vars(respfile, maskfile) if len(Y.shape) == 1: Y = Y[:, np.newaxis] - + sample_num = X.shape[0] if models is not None: feature_num = len(models) @@ -853,13 +865,13 @@ def predict(covfile, respfile, maskfile=None, **kwargs): if respfile is not None: if alg == 'hbr': # Z scores for HBR must be computed independently for each model - Z[:,i] = nm.get_mcmc_zscores(Xz, Yz[:, i:i+1], **kwargs) - + Z[:, i] = nm.get_mcmc_zscores(Xz, Yz[:, i:i+1], **kwargs) + if respfile is None: save_results(None, Yhat, S2, None, outputsuffix=outputsuffix) return (Yhat, S2) - + else: if models is not None and len(Y.shape) > 1: Y = Y[:, models] @@ -891,9 +903,9 @@ def predict(covfile, respfile, maskfile=None, **kwargs): Y = Yw else: warp = False - + if alg != 'hbr': - # For HBR the Z scores are already computed + # For HBR the Z scores are already computed Z = (Y - Yhat) / np.sqrt(S2) print("Evaluating the model ...") @@ -952,14 +964,14 @@ def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, 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())): + elif ('model_path' not in list(kwargs.keys())) or \ + ('trbefile' not 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': - if (not 'output_path' in list(kwargs.keys())): + if ('output_path' not in list(kwargs.keys())): print('InputError: Some mandatory arguments for hbr are missing.') return else: @@ -971,7 +983,7 @@ def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, # or (testresp==None) elif alg == 'blr': if (testcov == None) or \ - (not 'tsbefile' in list(kwargs.keys())): + ('tsbefile' not in list(kwargs.keys())): print('InputError: Some mandatory arguments for blr are missing.') return # general arguments @@ -1207,9 +1219,9 @@ def extend(covfile, respfile, maskfile=None, **kwargs): 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())): + elif ('model_path' not in list(kwargs.keys())) or \ + ('output_path' not in list(kwargs.keys())) or \ + ('trbefile' not in list(kwargs.keys())): print('InputError: Some mandatory arguments are missing.') return else: @@ -1318,9 +1330,9 @@ def tune(covfile, respfile, maskfile=None, **kwargs): 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())): + elif ('model_path' not in list(kwargs.keys())) or \ + ('output_path' not in list(kwargs.keys())) or \ + ('trbefile' not in list(kwargs.keys())): print('InputError: Some mandatory arguments are missing.') return else: @@ -1426,9 +1438,9 @@ def merge(covfile=None, respfile=None, **kwargs): 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())): + elif ('model_path1' not in list(kwargs.keys())) or \ + ('model_path2' not in list(kwargs.keys())) or \ + ('output_path' not in list(kwargs.keys())): print('InputError: Some mandatory arguments are missing.') return else: @@ -1527,6 +1539,9 @@ def main(*args): # Executing the target function exec(func + '(' + all_args + ')') +def entrypoint(): + main(sys.argv[1:]) + # For running from the command line: if __name__ == "__main__": diff --git a/pcntoolkit/normative_model/norm_base.py b/pcntoolkit/normative_model/norm_base.py index 7839e740..268d421e 100644 --- a/pcntoolkit/normative_model/norm_base.py +++ b/pcntoolkit/normative_model/norm_base.py @@ -1,8 +1,10 @@ import os +import pickle import sys -from six import with_metaclass from abc import ABCMeta, abstractmethod -import pickle + +import pandas as pd +from six import with_metaclass try: # run as a package if installed from pcntoolkit import configs @@ -53,7 +55,7 @@ def save(self, save_path): def load(self, load_path): try: with open(load_path, 'rb') as handle: - nm = pickle.load(handle) + nm = pd.read_pickle(handle) return nm except Exception as err: print('Error:', err) diff --git a/pcntoolkit/normative_model/norm_hbr.py b/pcntoolkit/normative_model/norm_hbr.py index f972bfd0..170af404 100644 --- a/pcntoolkit/normative_model/norm_hbr.py +++ b/pcntoolkit/normative_model/norm_hbr.py @@ -40,7 +40,6 @@ class NormHBR(NormBase): - """HBR multi-batch normative modelling class. By default, this function estimates a linear model with random intercept, random slope, and random homoscedastic noise. @@ -135,16 +134,18 @@ def __init__(self, **kwargs): "random_noise", "True") == "True" self.configs["likelihood"] = kwargs.get("likelihood", "Normal") # sampler settings + self.configs["nuts_sampler"] = kwargs.get("nuts_sampler", "pymc") 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["init"] = kwargs.get("init", "jitter+adapt_diag") + self.configs["init"] = kwargs.get("init", "jitter+adapt_diag_grad") 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 @@ -260,8 +261,8 @@ 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. + 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. @@ -290,9 +291,9 @@ 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. + 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. @@ -331,8 +332,8 @@ 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. + 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. @@ -349,7 +350,7 @@ 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. + 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. @@ -372,8 +373,8 @@ def extend( """ 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. + 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. @@ -426,11 +427,13 @@ def tune( """ 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.") - + # 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])) X_dummy, batch_effects_dummy = self.hbr.create_dummy_inputs( @@ -515,7 +518,7 @@ def get_mcmc_quantiles(self, X, batch_effects=None, z_scores=None): Args: X ([N*p]ndarray): covariates for which the quantiles are computed (must be scaled if scaler is set) batch_effects (ndarray): the batch effects corresponding to X - z_scores (ndarray): Use this to determine which quantiles will be computed. The resulting quantiles will have the z-scores given in this list. + z_scores (ndarray): Use this to determine which quantiles will be computed. The resulting quantiles will have the z-scores given in this list. """ # Set batch effects to zero if none are provided if batch_effects is None: @@ -524,9 +527,9 @@ def get_mcmc_quantiles(self, X, batch_effects=None, z_scores=None): # 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 + # Determine the variables to predict if self.configs["likelihood"] == "Normal": var_names = ["mu_samples", "sigma_samples", "sigma_plus_samples"] elif self.configs["likelihood"].startswith("SHASH"): @@ -542,24 +545,21 @@ def get_mcmc_quantiles(self, X, batch_effects=None, z_scores=None): exit("Unknown likelihood: " + self.configs["likelihood"]) # Delete the posterior predictive if it already exists - if 'posterior_predictive' in self.hbr.idata.groups(): + if "posterior_predictive" in self.hbr.idata.groups(): del self.hbr.idata.posterior_predictive if self.configs["transferred"] == True: - self.predict_on_new_sites( - X=X, - batch_effects=batch_effects - ) - #var_names = ["y_like"] - else: + self.predict_on_new_sites(X=X, batch_effects=batch_effects) + # var_names = ["y_like"] + else: self.hbr.predict( - # Do a forward to get the posterior predictive in the idata + # Do a forward to get the posterior predictive in the idata X=X, batch_effects=batch_effects, batch_effects_maps=self.batch_effects_maps, pred="single", - var_names=var_names+["y_like"], - ) + var_names=var_names + ["y_like"], + ) # Extract the relevant samples from the idata post_pred = az.extract( @@ -567,9 +567,9 @@ def get_mcmc_quantiles(self, X, batch_effects=None, z_scores=None): ) # Remove superfluous var_nammes - var_names.remove('sigma_samples') - if 'delta_samples' in var_names: - var_names.remove('delta_samples') + 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 array_of_vars = list(map(lambda x: post_pred[x], var_names)) @@ -585,7 +585,7 @@ def get_mcmc_quantiles(self, X, batch_effects=None, z_scores=None): quantiles[i] = xarray.apply_ufunc( quantile, *array_of_vars, - kwargs={"zs": zs, "likelihood": self.configs['likelihood']}, + kwargs={"zs": zs, "likelihood": self.configs["likelihood"]}, ) return quantiles.mean(axis=-1) @@ -598,12 +598,12 @@ def get_mcmc_zscores(self, X, y, **kwargs): y ([N*1]ndarray): response variables """ - print(self.configs['likelihood']) + print(self.configs["likelihood"]) 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 + 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]) @@ -623,7 +623,7 @@ def get_mcmc_zscores(self, X, y, **kwargs): exit("Unknown likelihood: " + self.configs["likelihood"]) # Delete the posterior predictive if it already exists - if 'posterior_predictive' in self.hbr.idata.groups(): + 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 @@ -632,7 +632,7 @@ def get_mcmc_zscores(self, X, y, **kwargs): batch_effects=batch_effects_test, batch_effects_maps=self.batch_effects_maps, pred="single", - var_names=var_names+["y_like"], + var_names=var_names + ["y_like"], ) # Extract the relevant samples from the idata @@ -641,9 +641,9 @@ def get_mcmc_zscores(self, X, y, **kwargs): ) # Remove superfluous var_names - var_names.remove('sigma_samples') - if 'delta_samples' in var_names: - var_names.remove('delta_samples') + 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 array_of_vars = list(map(lambda x: post_pred[x], var_names)) @@ -655,7 +655,7 @@ def get_mcmc_zscores(self, X, y, **kwargs): z_scores = xarray.apply_ufunc( z_score, *array_of_vars, - kwargs={"y": y, "likelihood": self.configs['likelihood']}, + kwargs={"y": y, "likelihood": self.configs["likelihood"]}, ) return z_scores.mean(axis=-1).values @@ -703,19 +703,19 @@ def m(epsilon, delta, r): 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.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 + sigma_d = sigma / delta + 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) + 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 - elif likelihood == 'Normal': - quantiles = zs*sigma + mu + elif likelihood == "Normal": + quantiles = zs * sigma + mu else: exit("Unsupported likelihood") return quantiles @@ -723,22 +723,22 @@ def quantile(mu, sigma, epsilon=None, delta=None, zs=0, 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.startswith("SHASH"): if likelihood == "SHASHo": - SHASH = (y-mu)/sigma - Z = np.sinh(np.arcsinh(SHASH)*delta - epsilon) + SHASH = (y - mu) / sigma + Z = np.sinh(np.arcsinh(SHASH) * delta - epsilon) elif likelihood == "SHASHo2": - sigma_d = sigma/delta - SHASH = (y-mu)/sigma_d - Z = np.sinh(np.arcsinh(SHASH)*delta - epsilon) + sigma_d = sigma / delta + SHASH = (y - mu) / sigma_d + Z = np.sinh(np.arcsinh(SHASH) * delta - epsilon) elif likelihood == "SHASHb": true_mu = m(epsilon, delta, 1) - true_sigma = np.sqrt((m(epsilon, delta, 2) - true_mu ** 2)) - SHASH_c = ((y-mu)/sigma) + true_sigma = np.sqrt((m(epsilon, delta, 2) - true_mu**2)) + SHASH_c = (y - mu) / sigma SHASH = SHASH_c * true_sigma + true_mu Z = np.sinh(np.arcsinh(SHASH) * delta - epsilon) - elif likelihood == 'Normal': - Z = (y-mu)/sigma + elif likelihood == "Normal": + Z = (y - mu) / sigma else: exit("Unsupported likelihood") return Z diff --git a/pcntoolkit/normative_parallel.py b/pcntoolkit/normative_parallel.py index 4733bf77..d5b95c60 100755 --- a/pcntoolkit/normative_parallel.py +++ b/pcntoolkit/normative_parallel.py @@ -112,6 +112,7 @@ def execute_nm(processing_dir, cluster_spec = kwargs.pop('cluster_spec', 'torque') log_path = kwargs.get('log_path', None) binary = kwargs.pop('binary', False) + cores = kwargs.pop('n_cores_per_batch','1') split_nm(processing_dir, respfile_path, @@ -132,7 +133,7 @@ def execute_nm(processing_dir, kwargs.update({'batch_size': str(batch_size)}) job_ids = [] start_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") - + for n in range(1, number_of_batches+1): kwargs.update({'job_id': str(n)}) if testrespfile_path is not None: @@ -165,7 +166,8 @@ def execute_nm(processing_dir, job_id = qsub_nm(job_path=batch_job_path, log_path=log_path, memory=memory, - duration=duration) + duration=duration, + cores=cores) job_ids.append(job_id) elif cluster_spec == 'slurm': # update the response file @@ -181,11 +183,10 @@ def execute_nm(processing_dir, memory=memory, duration=duration, **kwargs) - + job_id = sbatch_nm(job_path=batch_job_path) job_ids.append(job_id) - - + elif cluster_spec == 'new': # this part requires addition in different envioronment [ sbatchwrap_nm(processing_dir=batch_processing_dir, @@ -212,7 +213,8 @@ def execute_nm(processing_dir, job_id = qsub_nm(job_path=batch_job_path, log_path=log_path, memory=memory, - duration=duration) + duration=duration, + cores=cores) job_ids.append(job_id) elif cluster_spec == 'slurm': sbatchwrap_nm(batch_processing_dir, @@ -225,7 +227,7 @@ def execute_nm(processing_dir, memory=memory, duration=duration, **kwargs) - + job_id = sbatch_nm(job_path=batch_job_path) job_ids.append(job_id) elif cluster_spec == 'new': @@ -255,7 +257,8 @@ def execute_nm(processing_dir, job_id = qsub_nm(job_path=batch_job_path, log_path=log_path, memory=memory, - duration=duration) + duration=duration, + cores=cores) job_ids.append(job_id) elif cluster_spec == 'slurm': sbatchwrap_nm(batch_processing_dir, @@ -268,11 +271,10 @@ def execute_nm(processing_dir, memory=memory, duration=duration, **kwargs) - - + job_id = sbatch_nm(job_path=batch_job_path) job_ids.append(job_id) - + elif cluster_spec == 'new': # this part requires addition in different envioronment [ bashwrap_nm(processing_dir=batch_processing_dir, func=func, @@ -301,31 +303,31 @@ def execute_nm(processing_dir, if response: if cluster_spec == 'torque': rerun_nm(processing_dir, log_path=log_path, memory=memory, - duration=duration, binary=binary, - interactive=interactive) + duration=duration, binary=binary, + interactive=interactive, cores=cores) elif cluster_spec == 'slurm': sbatchrerun_nm(processing_dir, - memory=memory, - duration=duration, - binary=binary, - log_path=log_path, - interactive=interactive) - + memory=memory, + duration=duration, + binary=binary, + log_path=log_path, + interactive=interactive) + else: success = True else: print('Reruning the failed jobs ...') if cluster_spec == 'torque': rerun_nm(processing_dir, log_path=log_path, memory=memory, - duration=duration, binary=binary, - interactive=interactive) + duration=duration, binary=binary, + interactive=interactive, cores=cores) elif cluster_spec == 'slurm': sbatchrerun_nm(processing_dir, - memory=memory, - duration=duration, - binary=binary, - log_path=log_path, - interactive=interactive) + memory=memory, + duration=duration, + binary=binary, + log_path=log_path, + interactive=interactive) if interactive == 'query': response = yes_or_no('Collect the results?') @@ -508,11 +510,11 @@ def collect_nm(processing_dir, # prediction is made (when test cov is not specified). files = glob.glob(processing_dir + 'batch_*/' + 'yhat' + outputsuffix + file_extentions) - if len(files)>0: + if len(files) > 0: file_example = fileio.load(files[0]) else: - raise ValueError(f"Missing output files (yhats at: {processing_dir + 'batch_*/' + 'yhat' + outputsuffix + file_extentions}") - + raise ValueError(f"Missing output files (yhats at: {processing_dir + 'batch_*/' + 'yhat' + outputsuffix + file_extentions}") + numsubjects = file_example.shape[0] try: # doesn't exist if size=1, and txt file @@ -987,7 +989,8 @@ def bashwrap_nm(processing_dir, def qsub_nm(job_path, log_path, memory, - duration): + duration, + cores): '''This function submits a job.sh scipt to the torque custer using the qsub command. Basic usage:: @@ -1007,10 +1010,10 @@ def qsub_nm(job_path, # created qsub command if log_path is None: qsub_call = ['echo ' + job_path + ' | qsub -N ' + job_path + ' -l ' + - 'procs=1' + ',mem=' + memory + ',walltime=' + duration] + 'nodes=1:ppn='+ cores + ',mem=' + memory + ',walltime=' + duration] else: qsub_call = ['echo ' + job_path + ' | qsub -N ' + job_path + - ' -l ' + 'procs=1' + ',mem=' + memory + ',walltime=' + + ' -l ' + 'nodes=1:ppn='+ cores + ',mem=' + memory + ',walltime=' + duration + ' -o ' + log_path + ' -e ' + log_path] # submits job to cluster @@ -1026,6 +1029,7 @@ def rerun_nm(processing_dir, memory, duration, cluster_spec, + cores, binary=False, interactive=False): '''This function reruns all failed batched in processing_dir after collect_nm has identified the failed batches. @@ -1053,7 +1057,8 @@ def rerun_nm(processing_dir, job_id = qsub_nm(job_path=jobpath, log_path=log_path, memory=memory, - duration=duration) + duration=duration, + cores=cores) job_ids.append(job_id) else: file_extentions = '.txt' @@ -1066,7 +1071,8 @@ def rerun_nm(processing_dir, job_id = qsub_nm(job_path=jobpath, log_path=log_path, memory=memory, - duration=duration) + duration=duration, + cores=cores) job_ids.append(job_id) if interactive: @@ -1123,15 +1129,15 @@ def sbatchwrap_nm(processing_dir, output_changedir = ['cd ' + processing_dir + '\n'] sbatch_init = '#!/bin/bash\n' - sbatch_jobname = '#SBATCH --job-name=' + processing_dir + '\n' + sbatch_jobname = '#SBATCH --job-name=' + job_name + '\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_log_out = '#SBATCH -o ' + log_path + '%j.out' + '\n' - sbatch_log_error = '#SBATCH -e ' + log_path + '%j.err' + '\n' - #sbatch_module = 'module purge\n' - #sbatch_anaconda = 'module load anaconda3\n' + sbatch_log_out = '#SBATCH -o ' + log_path + '%x_%j.out' + '\n' + sbatch_log_error = '#SBATCH -e ' + log_path + '%x_%j.err' + '\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 " @@ -1142,8 +1148,8 @@ def sbatchwrap_nm(processing_dir, sbatch_nodes + sbatch_tasks + sbatch_time + - sbatch_memory+ - sbatch_log_out+ + sbatch_memory + + sbatch_log_out + sbatch_log_error ] @@ -1212,7 +1218,7 @@ def sbatch_nm(job_path): # submits job to cluster job_id = check_output(sbatch_call, shell=True).decode( sys.stdout.encoding).replace("\n", "") - + return job_id @@ -1240,11 +1246,11 @@ def sbatchrerun_nm(processing_dir, written by (primarily) T Wolfers, (adapted) S Rutherford. ''' - - #log_path = kwargs.pop('log_path', None) - + + # log_path = kwargs.pop('log_path', None) + job_ids = [] - + start_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") if binary: @@ -1284,15 +1290,16 @@ def sbatchrerun_nm(processing_dir, print(line.replace(memory, new_memory), end='') job_id = sbatch_nm(jobpath) job_ids.append(job_id) - + if interactive: - check_jobs(job_ids, cluster_spec='slurm', start_time=start_time, delay=60) + check_jobs(job_ids, cluster_spec='slurm', + start_time=start_time, delay=60) def retrieve_jobs(cluster_spec, start_time=None): """ A utility function to retrieve task status from the outputs of qstat. - + :param cluster_spec: type of cluster, either 'torque' or 'slurm'. :return: a dictionary of jobs. @@ -1300,7 +1307,7 @@ def retrieve_jobs(cluster_spec, start_time=None): """ if cluster_spec == 'torque': - + output = check_output('qstat', shell=True).decode(sys.stdout.encoding) output = output.split('\n') jobs = dict() @@ -1310,9 +1317,9 @@ def retrieve_jobs(cluster_spec, start_time=None): jobs[Job_ID]['name'] = Job_Name jobs[Job_ID]['walltime'] = Wall_Time jobs[Job_ID]['status'] = Status - + elif cluster_spec == 'slurm': - + end_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") cmd = ['sacct', '-n', '-X', '--parsable2', '--noheader', '-S', start_time, '-E', end_time, '--format=JobName,State'] @@ -1336,9 +1343,9 @@ def check_job_status(jobs, cluster_spec, start_time=None): c = 0 q = 0 u = 0 - + if cluster_spec == 'torque': - + for job in jobs: try: if running_jobs[job]['status'] == 'C': @@ -1352,14 +1359,14 @@ def check_job_status(jobs, cluster_spec, start_time=None): 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)) - + % (len(jobs), q, r, c, u)) + elif cluster_spec == 'slurm': - + lines = running_jobs.stdout.strip().split('\n') - + for line in lines: if line: parts = line.split('|') @@ -1373,10 +1380,10 @@ def check_job_status(jobs, cluster_spec, start_time=None): c += 1 elif state == 'FAILED': u += 1 - + print('Total Jobs:%d, Pending:%d, Running:%d, Completed:%d, Failed:%d' - % (len(jobs), q, r, c, u)) - + % (len(jobs), q, r, c, u)) + return q, r, c, u diff --git a/pcntoolkit/regression_model/blr/warp.py b/pcntoolkit/regression_model/blr/warp.py new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/pcntoolkit/regression_model/blr/warp.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pcntoolkit/util/utils.py b/pcntoolkit/util/utils.py index 9eb9208b..cd245746 100644 --- a/pcntoolkit/util/utils.py +++ b/pcntoolkit/util/utils.py @@ -1,26 +1,26 @@ from __future__ import print_function import os +import pickle +import re +import subprocess import sys -import numpy as np -from scipy import stats -from subprocess import call -from scipy.stats import genextreme, norm -from six import with_metaclass from abc import ABCMeta, abstractmethod -import pickle +from io import StringIO +from subprocess import call + +import bspline import matplotlib.pyplot as plt +import numpy as np import pandas as pd -import bspline +import pymc as pm +import scipy.special as spp from bspline import splinelab +from scipy import stats +from scipy.stats import genextreme, norm, skewnorm +from six import with_metaclass from sklearn.datasets import make_regression -import pymc as pm -from io import StringIO -import subprocess -import re from sklearn.metrics import roc_auc_score -import scipy.special as spp - try: # run as a package if installed from pcntoolkit import configs @@ -126,7 +126,7 @@ def create_design_matrix(X, intercept=True, basis='bspline', N = X.shape[0] - if type(X) is pd.DataFrame: + if isinstance(X, pd.DataFrame): X = X.to_numpy() # add intercept column @@ -149,7 +149,7 @@ def create_design_matrix(X, intercept=True, basis='bspline', else: # site ids are defined # make sure the data are in pandas format - if type(site_ids) is not pd.Series: + if not isinstance(site_ids, pd.Series): site_ids = pd.Series(data=site_ids) # site_ids = pd.Series(data=site_ids) @@ -175,8 +175,8 @@ def create_design_matrix(X, intercept=True, basis='bspline', 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)) + Phi = np.concatenate((Phi, create_poly_basis( + X[:, basis_column], **kwargs)), axis=1) return Phi @@ -350,7 +350,7 @@ def calibration_descriptives(x): 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))) - kurtosis = (n*(n+1)*m4 - 3*m2**2*(n-1)) / ((n-1)*(n-2)*(n-3)*s1**4) + kurtosis = (n * (n+1) * m4) / ((n-1) * (n-2) * (n-3) * s1**4) - (3 * (n-1)**2) / ((n-2) * (n-3)) 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)) @@ -469,7 +469,8 @@ 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): @@ -570,7 +571,8 @@ 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]) @@ -879,90 +881,92 @@ def calibration_error(Y, m, s, cal_levels): 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. - It can be also a list of size of n_grps that decides the number of samples - in each group (default=100). - :param n_features: A positive integer that decides the number of features - (default=1). - :param n_grps: A positive integer that decides the number of groups in data - (default=1). - :param working_dir: Directory to save data (default=None). - :param plot: Boolean to plot the simulated training data (default=False). - :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 - + """ + Simulates synthetic data for testing purposes, with options for linear, non-linear, + or combined data generation methods, and various noise types. + + :param method: Method to simulate ('linear', 'non-linear', or 'combined'). + :param n_samples: Number of samples per group, either an int or a list for each group (default=100). + :param n_features: Number of features to simulate (default=1). + :param n_grps: Number of groups in the data (default=1). + :param working_dir: Directory to save the data (default=None). + :param plot: Boolean flag to plot the simulated training data (default=False). + :param random_state: Seed for random number generation (default=None). + :param noise: Type of noise to add ('homoscedastic_gaussian', 'heteroscedastic_gaussian', + 'homoscedastic_nongaussian', 'heteroscedastic_nongaussian', default=None). + + :returns: Tuple of (X_train, Y_train, grp_id_train, X_test, Y_test, grp_id_test, coef) """ + np.random.seed(random_state) + if isinstance(n_samples, int): - n_samples = [n_samples for i in range(n_grps)] + n_samples = [n_samples for _ 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, + ) 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) + 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) + 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 \ - 'linear' or 'non-linear'.") - coef.append(coef_temp/100) - X_train.append(X_temp[:X_temp.shape[0]//2]) - Y_train.append(Y_temp[:X_temp.shape[0]//2]/100) - X_test.append(X_temp[X_temp.shape[0]//2:]) - Y_test.append(Y_temp[X_temp.shape[0]//2:]/100) - 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) - Y_train[i] = Y_train[i] + np.random.randn(Y_train[i].shape[0]) / t \ - * 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])) - elif noise == 'gaussian': - 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 - elif noise == 'hetero_gaussian_smaller': - 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])) - 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])) + raise ValueError( + "Unknown method. Please specify 'linear', 'non-linear', or 'combined'.") + + coef.append(coef_temp / 100) + X_train.append(X_temp[:n_samples[i]]) + Y_train.append(Y_temp[:n_samples[i]] / 100) + X_test.append(X_temp[n_samples[i]:]) + Y_test.append(Y_temp[n_samples[i]:] / 100) + grp_id = np.repeat(i, n_samples[i] * 2) + grp_id_train.append(grp_id[:n_samples[i]]) + grp_id_test.append(grp_id[n_samples[i]:]) + + t = np.random.randint(1, 5) + # Add noise to the data + if noise == 'homoscedastic_gaussian': + Y_train[i] += np.random.normal(loc=0, + scale=0.2, size=Y_train[i].shape[0]) / t + Y_test[i] += np.random.normal(loc=0, + scale=0.2, size=Y_test[i].shape[0]) / t + + elif noise == 'heteroscedastic_gaussian': + Y_train[i] += np.random.normal(loc=0, scale=np.log( + 1 + np.exp(X_train[i][:, 0])), size=Y_train[i].shape[0]) + Y_test[i] += np.random.normal(loc=0, scale=np.log( + 1 + np.exp(X_test[i][:, 0])), size=Y_test[i].shape[0]) + + elif noise == 'homoscedastic_nongaussian': + Y_train[i] += skewnorm.rvs(a=10, loc=0, + scale=0.2, size=Y_train[i].shape[0]) / t + Y_test[i] += skewnorm.rvs(a=10, loc=0, + scale=0.2, size=Y_test[i].shape[0]) / t + + elif noise == 'heteroscedastic_nongaussian': + Y_train[i] += skewnorm.rvs(a=10, loc=0, scale=np.log( + 1 + np.exp(0.3 * X_train[i][:, 0])), size=Y_train[i].shape[0]) + Y_test[i] += skewnorm.rvs(a=10, loc=0, scale=np.log(1 + + np.exp(0.3 * X_test[i][:, 0])), size=Y_test[i].shape[0]) + X_train = np.vstack(X_train) X_test = np.vstack(X_test) Y_train = np.concatenate(Y_train) @@ -970,32 +974,39 @@ def simulate_data(method='linear', n_samples=100, n_features=1, n_grps=1, 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.xlabel('X' + str(i)) - plt.ylabel('Y') - plt.legend() - - if working_dir is not None: + if plot: + 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.xlabel(f'X{i}') + plt.ylabel('Y') + plt.legend() + plt.show() + + if working_dir: 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) + pickle.dump(pd.DataFrame(grp_id_train), file, + protocol=pickle.HIGHEST_PROTOCOL) with open(os.path.join(working_dir, 'tsbefile.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(grp_id_test), - file, protocol=PICKLE_PROTOCOL) + pickle.dump(pd.DataFrame(grp_id_test), file, + protocol=pickle.HIGHEST_PROTOCOL) with open(os.path.join(working_dir, 'X_train.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(X_train), file, protocol=PICKLE_PROTOCOL) + pickle.dump(pd.DataFrame(X_train), file, + protocol=pickle.HIGHEST_PROTOCOL) with open(os.path.join(working_dir, 'X_test.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(X_test), file, protocol=PICKLE_PROTOCOL) + pickle.dump(pd.DataFrame(X_test), file, + protocol=pickle.HIGHEST_PROTOCOL) with open(os.path.join(working_dir, 'Y_train.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(Y_train), file, protocol=PICKLE_PROTOCOL) + pickle.dump(pd.DataFrame(Y_train), file, + protocol=pickle.HIGHEST_PROTOCOL) with open(os.path.join(working_dir, 'Y_test.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(Y_test), file, protocol=PICKLE_PROTOCOL) + pickle.dump(pd.DataFrame(Y_test), file, + protocol=pickle.HIGHEST_PROTOCOL) return X_train, Y_train, grp_id_train, X_test, Y_test, grp_id_test, coef @@ -1067,14 +1078,14 @@ def load_freesurfer_measure(measure, data_path, subjects_list): data = dict() a = pd.read_csv(data_path + sub + '/stats/lh.aparc.a2009s.stats', - delimiter='\s+', comment='#', header=None) + delimiter=r'\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', - delimiter='\s+', comment='#', header=None) + delimiter=r'\s+', comment='#', header=None) temp = dict(zip(a[0], a[col])) for key in list(temp.keys()): temp['R_'+key] = temp.pop(key) @@ -1126,7 +1137,7 @@ def load_freesurfer_measure(measure, data_path, subjects_list): else: tiv = a[' ICV'] a = pd.read_csv(data_path + sub + '/stats/aseg.stats', - delimiter='\s+', comment='#', header=None) + delimiter=r'\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 @@ -1464,7 +1475,7 @@ def anomaly_detection_auc(abn_p, labels, n_permutation=None): 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])) + (i, p, p_values[i])) return aucs, p_values diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..fb1978a1 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,2661 @@ +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. + +[[package]] +name = "alabaster" +version = "1.0.0" +description = "A light, configurable Sphinx theme" +optional = false +python-versions = ">=3.10" +files = [ + {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, + {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, +] + +[[package]] +name = "appnope" +version = "0.1.4" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = ">=3.6" +files = [ + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, +] + +[[package]] +name = "arviz" +version = "0.20.0" +description = "Exploratory analysis of Bayesian models" +optional = false +python-versions = ">=3.10" +files = [ + {file = "arviz-0.20.0-py3-none-any.whl", hash = "sha256:5ec4f2ec180a8305ff3d1108c29e189944ab939663eb5bc3231ff199a1a5dc36"}, + {file = "arviz-0.20.0.tar.gz", hash = "sha256:a2704e0c141410fcaea1973a90cabf280f5aed5c1e10f44381ebd6c144c10a9c"}, +] + +[package.dependencies] +h5netcdf = ">=1.0.2" +matplotlib = ">=3.5" +numpy = ">=1.23.0" +packaging = "*" +pandas = ">=1.5.0" +scipy = ">=1.9.0" +setuptools = ">=60.0.0" +typing-extensions = ">=4.1.0" +xarray = ">=2022.6.0" +xarray-einstats = ">=0.3" + +[package.extras] +all = ["bokeh (>=3)", "contourpy", "dask[distributed]", "dm-tree (>=0.1.8)", "netcdf4", "numba", "ujson", "xarray-datatree", "zarr (>=2.5.0,<3)"] +preview = ["arviz-base[h5netcdf]", "arviz-plots", "arviz-stats[xarray]"] + +[[package]] +name = "asttokens" +version = "2.4.1" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, +] + +[package.dependencies] +six = ">=1.12.0" + +[package.extras] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] + +[[package]] +name = "babel" +version = "2.16.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "black" +version = "24.10.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "bspline" +version = "0.1.1" +description = "Compute B-spline basis functions via Cox - de Boor algorithm." +optional = false +python-versions = "*" +files = [ + {file = "bspline-0.1.1.tar.gz", hash = "sha256:3be5490cd7ea81e7a08820d4d1d1b602f91991f429ce20c49800dbf226213f08"}, +] + +[package.dependencies] +numpy = "*" + +[[package]] +name = "cachetools" +version = "5.5.0" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "cloudpickle" +version = "3.1.0" +description = "Pickler class to extend the standard pickle.Pickler functionality" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cloudpickle-3.1.0-py3-none-any.whl", hash = "sha256:fe11acda67f61aaaec473e3afe030feb131d78a43461b718185363384f1ba12e"}, + {file = "cloudpickle-3.1.0.tar.gz", hash = "sha256:81a929b6e3c7335c863c771d673d105f02efdb89dfaba0c90495d1c64796601b"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "comm" +version = "0.2.2" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = false +python-versions = ">=3.8" +files = [ + {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, + {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, +] + +[package.dependencies] +traitlets = ">=4" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "cons" +version = "0.4.6" +description = "An implementation of Lisp/Scheme-like cons in Python." +optional = false +python-versions = ">=3.6" +files = [ + {file = "cons-0.4.6.tar.gz", hash = "sha256:669fe9d5ee916d5e42b9cac6acc911df803d04f2e945c1604982a04d27a29b47"}, +] + +[package.dependencies] +logical-unification = ">=0.4.0" + +[[package]] +name = "contourpy" +version = "1.3.1" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.10" +files = [ + {file = "contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab"}, + {file = "contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453"}, + {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3"}, + {file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277"}, + {file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595"}, + {file = "contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697"}, + {file = "contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e"}, + {file = "contourpy-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b"}, + {file = "contourpy-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85"}, + {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c"}, + {file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291"}, + {file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f"}, + {file = "contourpy-1.3.1-cp311-cp311-win32.whl", hash = "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375"}, + {file = "contourpy-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9"}, + {file = "contourpy-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509"}, + {file = "contourpy-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec"}, + {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9"}, + {file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b"}, + {file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d"}, + {file = "contourpy-1.3.1-cp312-cp312-win32.whl", hash = "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e"}, + {file = "contourpy-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d"}, + {file = "contourpy-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2"}, + {file = "contourpy-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7"}, + {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c"}, + {file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3"}, + {file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1"}, + {file = "contourpy-1.3.1-cp313-cp313-win32.whl", hash = "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82"}, + {file = "contourpy-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd"}, + {file = "contourpy-1.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30"}, + {file = "contourpy-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f"}, + {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda"}, + {file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242"}, + {file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1"}, + {file = "contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1"}, + {file = "contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546"}, + {file = "contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6"}, + {file = "contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750"}, + {file = "contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53"}, + {file = "contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699"}, +] + +[package.dependencies] +numpy = ">=1.23" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.11.1)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] + +[[package]] +name = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "debugpy" +version = "1.8.8" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "debugpy-1.8.8-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:e59b1607c51b71545cb3496876544f7186a7a27c00b436a62f285603cc68d1c6"}, + {file = "debugpy-1.8.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6531d952b565b7cb2fbd1ef5df3d333cf160b44f37547a4e7cf73666aca5d8d"}, + {file = "debugpy-1.8.8-cp310-cp310-win32.whl", hash = "sha256:b01f4a5e5c5fb1d34f4ccba99a20ed01eabc45a4684f4948b5db17a319dfb23f"}, + {file = "debugpy-1.8.8-cp310-cp310-win_amd64.whl", hash = "sha256:535f4fb1c024ddca5913bb0eb17880c8f24ba28aa2c225059db145ee557035e9"}, + {file = "debugpy-1.8.8-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:c399023146e40ae373753a58d1be0a98bf6397fadc737b97ad612886b53df318"}, + {file = "debugpy-1.8.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09cc7b162586ea2171eea055985da2702b0723f6f907a423c9b2da5996ad67ba"}, + {file = "debugpy-1.8.8-cp311-cp311-win32.whl", hash = "sha256:eea8821d998ebeb02f0625dd0d76839ddde8cbf8152ebbe289dd7acf2cdc6b98"}, + {file = "debugpy-1.8.8-cp311-cp311-win_amd64.whl", hash = "sha256:d4483836da2a533f4b1454dffc9f668096ac0433de855f0c22cdce8c9f7e10c4"}, + {file = "debugpy-1.8.8-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:0cc94186340be87b9ac5a707184ec8f36547fb66636d1029ff4f1cc020e53996"}, + {file = "debugpy-1.8.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64674e95916e53c2e9540a056e5f489e0ad4872645399d778f7c598eacb7b7f9"}, + {file = "debugpy-1.8.8-cp312-cp312-win32.whl", hash = "sha256:5c6e885dbf12015aed73770f29dec7023cb310d0dc2ba8bfbeb5c8e43f80edc9"}, + {file = "debugpy-1.8.8-cp312-cp312-win_amd64.whl", hash = "sha256:19ffbd84e757a6ca0113574d1bf5a2298b3947320a3e9d7d8dc3377f02d9f864"}, + {file = "debugpy-1.8.8-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:705cd123a773d184860ed8dae99becd879dfec361098edbefb5fc0d3683eb804"}, + {file = "debugpy-1.8.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890fd16803f50aa9cb1a9b9b25b5ec321656dd6b78157c74283de241993d086f"}, + {file = "debugpy-1.8.8-cp313-cp313-win32.whl", hash = "sha256:90244598214bbe704aa47556ec591d2f9869ff9e042e301a2859c57106649add"}, + {file = "debugpy-1.8.8-cp313-cp313-win_amd64.whl", hash = "sha256:4b93e4832fd4a759a0c465c967214ed0c8a6e8914bced63a28ddb0dd8c5f078b"}, + {file = "debugpy-1.8.8-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:143ef07940aeb8e7316de48f5ed9447644da5203726fca378f3a6952a50a9eae"}, + {file = "debugpy-1.8.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f95651bdcbfd3b27a408869a53fbefcc2bcae13b694daee5f1365b1b83a00113"}, + {file = "debugpy-1.8.8-cp38-cp38-win32.whl", hash = "sha256:26b461123a030e82602a750fb24d7801776aa81cd78404e54ab60e8b5fecdad5"}, + {file = "debugpy-1.8.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3cbf1833e644a3100eadb6120f25be8a532035e8245584c4f7532937edc652a"}, + {file = "debugpy-1.8.8-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:53709d4ec586b525724819dc6af1a7703502f7e06f34ded7157f7b1f963bb854"}, + {file = "debugpy-1.8.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a9c013077a3a0000e83d97cf9cc9328d2b0bbb31f56b0e99ea3662d29d7a6a2"}, + {file = "debugpy-1.8.8-cp39-cp39-win32.whl", hash = "sha256:ffe94dd5e9a6739a75f0b85316dc185560db3e97afa6b215628d1b6a17561cb2"}, + {file = "debugpy-1.8.8-cp39-cp39-win_amd64.whl", hash = "sha256:5c0e5a38c7f9b481bf31277d2f74d2109292179081f11108e668195ef926c0f9"}, + {file = "debugpy-1.8.8-py2.py3-none-any.whl", hash = "sha256:ec684553aba5b4066d4de510859922419febc710df7bba04fe9e7ef3de15d34f"}, + {file = "debugpy-1.8.8.zip", hash = "sha256:e6355385db85cbd666be703a96ab7351bc9e6c61d694893206f8001e22aee091"}, +] + +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "docutils" +version = "0.21.2" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, +] + +[[package]] +name = "etuples" +version = "0.3.9" +description = "Python S-expression emulation using tuple-like objects." +optional = false +python-versions = ">=3.8" +files = [ + {file = "etuples-0.3.9.tar.gz", hash = "sha256:a474e586683d8ba8d842ba29305005ceed1c08371a4b4b0e0e232527137e5ea3"}, +] + +[package.dependencies] +cons = "*" +multipledispatch = "*" + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "executing" +version = "2.1.0" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.8" +files = [ + {file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"}, + {file = "executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + +[[package]] +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] + +[[package]] +name = "fonttools" +version = "4.55.0" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fonttools-4.55.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:51c029d4c0608a21a3d3d169dfc3fb776fde38f00b35ca11fdab63ba10a16f61"}, + {file = "fonttools-4.55.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bca35b4e411362feab28e576ea10f11268b1aeed883b9f22ed05675b1e06ac69"}, + {file = "fonttools-4.55.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ce4ba6981e10f7e0ccff6348e9775ce25ffadbee70c9fd1a3737e3e9f5fa74f"}, + {file = "fonttools-4.55.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31d00f9852a6051dac23294a4cf2df80ced85d1d173a61ba90a3d8f5abc63c60"}, + {file = "fonttools-4.55.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e198e494ca6e11f254bac37a680473a311a88cd40e58f9cc4dc4911dfb686ec6"}, + {file = "fonttools-4.55.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7208856f61770895e79732e1dcbe49d77bd5783adf73ae35f87fcc267df9db81"}, + {file = "fonttools-4.55.0-cp310-cp310-win32.whl", hash = "sha256:e7e6a352ff9e46e8ef8a3b1fe2c4478f8a553e1b5a479f2e899f9dc5f2055880"}, + {file = "fonttools-4.55.0-cp310-cp310-win_amd64.whl", hash = "sha256:636caaeefe586d7c84b5ee0734c1a5ab2dae619dc21c5cf336f304ddb8f6001b"}, + {file = "fonttools-4.55.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fa34aa175c91477485c44ddfbb51827d470011e558dfd5c7309eb31bef19ec51"}, + {file = "fonttools-4.55.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:37dbb3fdc2ef7302d3199fb12468481cbebaee849e4b04bc55b77c24e3c49189"}, + {file = "fonttools-4.55.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5263d8e7ef3c0ae87fbce7f3ec2f546dc898d44a337e95695af2cd5ea21a967"}, + {file = "fonttools-4.55.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f307f6b5bf9e86891213b293e538d292cd1677e06d9faaa4bf9c086ad5f132f6"}, + {file = "fonttools-4.55.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f0a4b52238e7b54f998d6a56b46a2c56b59c74d4f8a6747fb9d4042190f37cd3"}, + {file = "fonttools-4.55.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3e569711464f777a5d4ef522e781dc33f8095ab5efd7548958b36079a9f2f88c"}, + {file = "fonttools-4.55.0-cp311-cp311-win32.whl", hash = "sha256:2b3ab90ec0f7b76c983950ac601b58949f47aca14c3f21eed858b38d7ec42b05"}, + {file = "fonttools-4.55.0-cp311-cp311-win_amd64.whl", hash = "sha256:aa046f6a63bb2ad521004b2769095d4c9480c02c1efa7d7796b37826508980b6"}, + {file = "fonttools-4.55.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:838d2d8870f84fc785528a692e724f2379d5abd3fc9dad4d32f91cf99b41e4a7"}, + {file = "fonttools-4.55.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f46b863d74bab7bb0d395f3b68d3f52a03444964e67ce5c43ce43a75efce9246"}, + {file = "fonttools-4.55.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33b52a9cfe4e658e21b1f669f7309b4067910321757fec53802ca8f6eae96a5a"}, + {file = "fonttools-4.55.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:732a9a63d6ea4a81b1b25a1f2e5e143761b40c2e1b79bb2b68e4893f45139a40"}, + {file = "fonttools-4.55.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7dd91ac3fcb4c491bb4763b820bcab6c41c784111c24172616f02f4bc227c17d"}, + {file = "fonttools-4.55.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1f0e115281a32ff532118aa851ef497a1b7cda617f4621c1cdf81ace3e36fb0c"}, + {file = "fonttools-4.55.0-cp312-cp312-win32.whl", hash = "sha256:6c99b5205844f48a05cb58d4a8110a44d3038c67ed1d79eb733c4953c628b0f6"}, + {file = "fonttools-4.55.0-cp312-cp312-win_amd64.whl", hash = "sha256:f8c8c76037d05652510ae45be1cd8fb5dd2fd9afec92a25374ac82255993d57c"}, + {file = "fonttools-4.55.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8118dc571921dc9e4b288d9cb423ceaf886d195a2e5329cc427df82bba872cd9"}, + {file = "fonttools-4.55.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01124f2ca6c29fad4132d930da69158d3f49b2350e4a779e1efbe0e82bd63f6c"}, + {file = "fonttools-4.55.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ffd58d2691f11f7c8438796e9f21c374828805d33e83ff4b76e4635633674c"}, + {file = "fonttools-4.55.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5435e5f1eb893c35c2bc2b9cd3c9596b0fcb0a59e7a14121562986dd4c47b8dd"}, + {file = "fonttools-4.55.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d12081729280c39d001edd0f4f06d696014c26e6e9a0a55488fabc37c28945e4"}, + {file = "fonttools-4.55.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7ad1f1b98ab6cb927ab924a38a8649f1ffd7525c75fe5b594f5dab17af70e18"}, + {file = "fonttools-4.55.0-cp313-cp313-win32.whl", hash = "sha256:abe62987c37630dca69a104266277216de1023cf570c1643bb3a19a9509e7a1b"}, + {file = "fonttools-4.55.0-cp313-cp313-win_amd64.whl", hash = "sha256:2863555ba90b573e4201feaf87a7e71ca3b97c05aa4d63548a4b69ea16c9e998"}, + {file = "fonttools-4.55.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:00f7cf55ad58a57ba421b6a40945b85ac7cc73094fb4949c41171d3619a3a47e"}, + {file = "fonttools-4.55.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f27526042efd6f67bfb0cc2f1610fa20364396f8b1fc5edb9f45bb815fb090b2"}, + {file = "fonttools-4.55.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e67974326af6a8879dc2a4ec63ab2910a1c1a9680ccd63e4a690950fceddbe"}, + {file = "fonttools-4.55.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61dc0a13451143c5e987dec5254d9d428f3c2789a549a7cf4f815b63b310c1cc"}, + {file = "fonttools-4.55.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:b2e526b325a903868c62155a6a7e24df53f6ce4c5c3160214d8fe1be2c41b478"}, + {file = "fonttools-4.55.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b7ef9068a1297714e6fefe5932c33b058aa1d45a2b8be32a4c6dee602ae22b5c"}, + {file = "fonttools-4.55.0-cp38-cp38-win32.whl", hash = "sha256:55718e8071be35dff098976bc249fc243b58efa263768c611be17fe55975d40a"}, + {file = "fonttools-4.55.0-cp38-cp38-win_amd64.whl", hash = "sha256:553bd4f8cc327f310c20158e345e8174c8eed49937fb047a8bda51daf2c353c8"}, + {file = "fonttools-4.55.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f901cef813f7c318b77d1c5c14cf7403bae5cb977cede023e22ba4316f0a8f6"}, + {file = "fonttools-4.55.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c9679fc0dd7e8a5351d321d8d29a498255e69387590a86b596a45659a39eb0d"}, + {file = "fonttools-4.55.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2820a8b632f3307ebb0bf57948511c2208e34a4939cf978333bc0a3f11f838"}, + {file = "fonttools-4.55.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23bbbb49bec613a32ed1b43df0f2b172313cee690c2509f1af8fdedcf0a17438"}, + {file = "fonttools-4.55.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a656652e1f5d55b9728937a7e7d509b73d23109cddd4e89ee4f49bde03b736c6"}, + {file = "fonttools-4.55.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f50a1f455902208486fbca47ce33054208a4e437b38da49d6721ce2fef732fcf"}, + {file = "fonttools-4.55.0-cp39-cp39-win32.whl", hash = "sha256:161d1ac54c73d82a3cded44202d0218ab007fde8cf194a23d3dd83f7177a2f03"}, + {file = "fonttools-4.55.0-cp39-cp39-win_amd64.whl", hash = "sha256:ca7fd6987c68414fece41c96836e945e1f320cda56fc96ffdc16e54a44ec57a2"}, + {file = "fonttools-4.55.0-py3-none-any.whl", hash = "sha256:12db5888cd4dd3fcc9f0ee60c6edd3c7e1fd44b7dd0f31381ea03df68f8a153f"}, + {file = "fonttools-4.55.0.tar.gz", hash = "sha256:7636acc6ab733572d5e7eec922b254ead611f1cdad17be3f0be7418e8bfaca71"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "pycairo", "scipy"] +lxml = ["lxml (>=4.0)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.1.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + +[[package]] +name = "h5netcdf" +version = "1.4.1" +description = "netCDF4 via h5py" +optional = false +python-versions = ">=3.9" +files = [ + {file = "h5netcdf-1.4.1-py3-none-any.whl", hash = "sha256:dd86c78ae69b92b16aa8a3c1ff3a14e7622571b5788dcf6d8b68569035bf71ce"}, + {file = "h5netcdf-1.4.1.tar.gz", hash = "sha256:7c8401ab807ff37c9798edc90d99467595892e6c541a5d5abeb8f53aab5335fe"}, +] + +[package.dependencies] +h5py = "*" +packaging = "*" + +[package.extras] +test = ["netCDF4", "pytest"] + +[[package]] +name = "h5py" +version = "3.12.1" +description = "Read and write HDF5 files from Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "h5py-3.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f0f1a382cbf494679c07b4371f90c70391dedb027d517ac94fa2c05299dacda"}, + {file = "h5py-3.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb65f619dfbdd15e662423e8d257780f9a66677eae5b4b3fc9dca70b5fd2d2a3"}, + {file = "h5py-3.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b15d8dbd912c97541312c0e07438864d27dbca857c5ad634de68110c6beb1c2"}, + {file = "h5py-3.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59685fe40d8c1fbbee088c88cd4da415a2f8bee5c270337dc5a1c4aa634e3307"}, + {file = "h5py-3.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:577d618d6b6dea3da07d13cc903ef9634cde5596b13e832476dd861aaf651f3e"}, + {file = "h5py-3.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ccd9006d92232727d23f784795191bfd02294a4f2ba68708825cb1da39511a93"}, + {file = "h5py-3.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad8a76557880aed5234cfe7279805f4ab5ce16b17954606cca90d578d3e713ef"}, + {file = "h5py-3.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1473348139b885393125126258ae2d70753ef7e9cec8e7848434f385ae72069e"}, + {file = "h5py-3.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:018a4597f35092ae3fb28ee851fdc756d2b88c96336b8480e124ce1ac6fb9166"}, + {file = "h5py-3.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:3fdf95092d60e8130ba6ae0ef7a9bd4ade8edbe3569c13ebbaf39baefffc5ba4"}, + {file = "h5py-3.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:06a903a4e4e9e3ebbc8b548959c3c2552ca2d70dac14fcfa650d9261c66939ed"}, + {file = "h5py-3.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b3b8f3b48717e46c6a790e3128d39c61ab595ae0a7237f06dfad6a3b51d5351"}, + {file = "h5py-3.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:050a4f2c9126054515169c49cb900949814987f0c7ae74c341b0c9f9b5056834"}, + {file = "h5py-3.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c4b41d1019322a5afc5082864dfd6359f8935ecd37c11ac0029be78c5d112c9"}, + {file = "h5py-3.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4d51919110a030913201422fb07987db4338eba5ec8c5a15d6fab8e03d443fc"}, + {file = "h5py-3.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:513171e90ed92236fc2ca363ce7a2fc6f2827375efcbb0cc7fbdd7fe11fecafc"}, + {file = "h5py-3.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:59400f88343b79655a242068a9c900001a34b63e3afb040bd7cdf717e440f653"}, + {file = "h5py-3.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e465aee0ec353949f0f46bf6c6f9790a2006af896cee7c178a8c3e5090aa32"}, + {file = "h5py-3.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba51c0c5e029bb5420a343586ff79d56e7455d496d18a30309616fdbeed1068f"}, + {file = "h5py-3.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:52ab036c6c97055b85b2a242cb540ff9590bacfda0c03dd0cf0661b311f522f8"}, + {file = "h5py-3.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2b8dd64f127d8b324f5d2cd1c0fd6f68af69084e9e47d27efeb9e28e685af3e"}, + {file = "h5py-3.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4532c7e97fbef3d029735db8b6f5bf01222d9ece41e309b20d63cfaae2fb5c4d"}, + {file = "h5py-3.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdf6d7936fa824acfa27305fe2d9f39968e539d831c5bae0e0d83ed521ad1ac"}, + {file = "h5py-3.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84342bffd1f82d4f036433e7039e241a243531a1d3acd7341b35ae58cdab05bf"}, + {file = "h5py-3.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:62be1fc0ef195891949b2c627ec06bc8e837ff62d5b911b6e42e38e0f20a897d"}, + {file = "h5py-3.12.1.tar.gz", hash = "sha256:326d70b53d31baa61f00b8aa5f95c2fcb9621a3ee8365d770c551a13dbbcbfdf"}, +] + +[package.dependencies] +numpy = ">=1.19.3" + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "importlib-resources" +version = "6.4.5" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, + {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] +type = ["pytest-mypy"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "ipykernel" +version = "6.29.5" +description = "IPython Kernel for Jupyter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, + {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.6.5" +ipython = ">=7.23.1" +jupyter-client = ">=6.1.12" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +matplotlib-inline = ">=0.1" +nest-asyncio = "*" +packaging = "*" +psutil = "*" +pyzmq = ">=24" +tornado = ">=6.1" +traitlets = ">=5.4.0" + +[package.extras] +cov = ["coverage[toml]", "curio", "matplotlib", "pytest-cov", "trio"] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "ipython" +version = "8.29.0" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.10" +files = [ + {file = "ipython-8.29.0-py3-none-any.whl", hash = "sha256:0188a1bd83267192123ccea7f4a8ed0a78910535dbaa3f37671dca76ebd429c8"}, + {file = "ipython-8.29.0.tar.gz", hash = "sha256:40b60e15b22591450eef73e40a027cf77bd652e757523eebc5bd7c7c498290eb"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt-toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5.13.0" +typing-extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} + +[package.extras] +all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "intersphinx-registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing-extensions"] +kernel = ["ipykernel"] +matplotlib = ["matplotlib"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] + +[[package]] +name = "ipywidgets" +version = "8.1.5" +description = "Jupyter interactive widgets" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ipywidgets-8.1.5-py3-none-any.whl", hash = "sha256:3290f526f87ae6e77655555baba4f36681c555b8bdbbff430b70e52c34c86245"}, + {file = "ipywidgets-8.1.5.tar.gz", hash = "sha256:870e43b1a35656a80c18c9503bbf2d16802db1cb487eec6fab27d683381dde17"}, +] + +[package.dependencies] +comm = ">=0.1.3" +ipython = ">=6.1.0" +jupyterlab-widgets = ">=3.0.12,<3.1.0" +traitlets = ">=4.3.1" +widgetsnbextension = ">=4.0.12,<4.1.0" + +[package.extras] +test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"] + +[[package]] +name = "jedi" +version = "0.19.2" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, + {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, +] + +[package.dependencies] +parso = ">=0.8.4,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "joblib" +version = "1.4.2" +description = "Lightweight pipelining with Python functions" +optional = false +python-versions = ">=3.8" +files = [ + {file = "joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6"}, + {file = "joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e"}, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +description = "Jupyter protocol implementation and client libraries" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"}, + {file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"}, +] + +[package.dependencies] +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +python-dateutil = ">=2.8.2" +pyzmq = ">=23.0" +tornado = ">=6.2" +traitlets = ">=5.3" + +[package.extras] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] + +[[package]] +name = "jupyter-core" +version = "5.7.2" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = false +python-versions = ">=3.8" +files = [ + {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, + {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, +] + +[package.dependencies] +platformdirs = ">=2.5" +pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +traitlets = ">=5.3" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "jupyterlab-widgets" +version = "3.0.13" +description = "Jupyter interactive widgets for JupyterLab" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jupyterlab_widgets-3.0.13-py3-none-any.whl", hash = "sha256:e3cda2c233ce144192f1e29914ad522b2f4c40e77214b0cc97377ca3d323db54"}, + {file = "jupyterlab_widgets-3.0.13.tar.gz", hash = "sha256:a2966d385328c1942b683a8cd96b89b8dd82c8b8f81dda902bb2bc06d46f5bed"}, +] + +[[package]] +name = "kiwisolver" +version = "1.4.7" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.8" +files = [ + {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6"}, + {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17"}, + {file = "kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3"}, + {file = "kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc"}, + {file = "kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c"}, + {file = "kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a"}, + {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54"}, + {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95"}, + {file = "kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523"}, + {file = "kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d"}, + {file = "kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b"}, + {file = "kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376"}, + {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2"}, + {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a"}, + {file = "kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520"}, + {file = "kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b"}, + {file = "kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb"}, + {file = "kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a"}, + {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e"}, + {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6"}, + {file = "kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee"}, + {file = "kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07"}, + {file = "kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76"}, + {file = "kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650"}, + {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a"}, + {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade"}, + {file = "kiwisolver-1.4.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5"}, + {file = "kiwisolver-1.4.7-cp38-cp38-win32.whl", hash = "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a"}, + {file = "kiwisolver-1.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09"}, + {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd"}, + {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583"}, + {file = "kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327"}, + {file = "kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644"}, + {file = "kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4"}, + {file = "kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0"}, + {file = "kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60"}, +] + +[[package]] +name = "logical-unification" +version = "0.4.6" +description = "Logical unification in Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "logical-unification-0.4.6.tar.gz", hash = "sha256:908435123f8a106fa4dcf9bf1b75c7beb309fa2bbecf277868af8f1c212650a0"}, +] + +[package.dependencies] +multipledispatch = "*" +toolz = "*" + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "matplotlib" +version = "3.9.2" +description = "Python plotting package" +optional = false +python-versions = ">=3.9" +files = [ + {file = "matplotlib-3.9.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9d78bbc0cbc891ad55b4f39a48c22182e9bdaea7fc0e5dbd364f49f729ca1bbb"}, + {file = "matplotlib-3.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c375cc72229614632c87355366bdf2570c2dac01ac66b8ad048d2dabadf2d0d4"}, + {file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d94ff717eb2bd0b58fe66380bd8b14ac35f48a98e7c6765117fe67fb7684e64"}, + {file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab68d50c06938ef28681073327795c5db99bb4666214d2d5f880ed11aeaded66"}, + {file = "matplotlib-3.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:65aacf95b62272d568044531e41de26285d54aec8cb859031f511f84bd8b495a"}, + {file = "matplotlib-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:3fd595f34aa8a55b7fc8bf9ebea8aa665a84c82d275190a61118d33fbc82ccae"}, + {file = "matplotlib-3.9.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8dd059447824eec055e829258ab092b56bb0579fc3164fa09c64f3acd478772"}, + {file = "matplotlib-3.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c797dac8bb9c7a3fd3382b16fe8f215b4cf0f22adccea36f1545a6d7be310b41"}, + {file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d719465db13267bcef19ea8954a971db03b9f48b4647e3860e4bc8e6ed86610f"}, + {file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8912ef7c2362f7193b5819d17dae8629b34a95c58603d781329712ada83f9447"}, + {file = "matplotlib-3.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7741f26a58a240f43bee74965c4882b6c93df3e7eb3de160126d8c8f53a6ae6e"}, + {file = "matplotlib-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:ae82a14dab96fbfad7965403c643cafe6515e386de723e498cf3eeb1e0b70cc7"}, + {file = "matplotlib-3.9.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ac43031375a65c3196bee99f6001e7fa5bdfb00ddf43379d3c0609bdca042df9"}, + {file = "matplotlib-3.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be0fc24a5e4531ae4d8e858a1a548c1fe33b176bb13eff7f9d0d38ce5112a27d"}, + {file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf81de2926c2db243c9b2cbc3917619a0fc85796c6ba4e58f541df814bbf83c7"}, + {file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ee45bc4245533111ced13f1f2cace1e7f89d1c793390392a80c139d6cf0e6c"}, + {file = "matplotlib-3.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:306c8dfc73239f0e72ac50e5a9cf19cc4e8e331dd0c54f5e69ca8758550f1e1e"}, + {file = "matplotlib-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:5413401594cfaff0052f9d8b1aafc6d305b4bd7c4331dccd18f561ff7e1d3bd3"}, + {file = "matplotlib-3.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:18128cc08f0d3cfff10b76baa2f296fc28c4607368a8402de61bb3f2eb33c7d9"}, + {file = "matplotlib-3.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4876d7d40219e8ae8bb70f9263bcbe5714415acfdf781086601211335e24f8aa"}, + {file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9f07a80deab4bb0b82858a9e9ad53d1382fd122be8cde11080f4e7dfedb38b"}, + {file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7c0410f181a531ec4e93bbc27692f2c71a15c2da16766f5ba9761e7ae518413"}, + {file = "matplotlib-3.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:909645cce2dc28b735674ce0931a4ac94e12f5b13f6bb0b5a5e65e7cea2c192b"}, + {file = "matplotlib-3.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:f32c7410c7f246838a77d6d1eff0c0f87f3cb0e7c4247aebea71a6d5a68cab49"}, + {file = "matplotlib-3.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:37e51dd1c2db16ede9cfd7b5cabdfc818b2c6397c83f8b10e0e797501c963a03"}, + {file = "matplotlib-3.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b82c5045cebcecd8496a4d694d43f9cc84aeeb49fe2133e036b207abe73f4d30"}, + {file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f053c40f94bc51bc03832a41b4f153d83f2062d88c72b5e79997072594e97e51"}, + {file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbe196377a8248972f5cede786d4c5508ed5f5ca4a1e09b44bda889958b33f8c"}, + {file = "matplotlib-3.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5816b1e1fe8c192cbc013f8f3e3368ac56fbecf02fb41b8f8559303f24c5015e"}, + {file = "matplotlib-3.9.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cef2a73d06601437be399908cf13aee74e86932a5ccc6ccdf173408ebc5f6bb2"}, + {file = "matplotlib-3.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e0830e188029c14e891fadd99702fd90d317df294c3298aad682739c5533721a"}, + {file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ba9c1299c920964e8d3857ba27173b4dbb51ca4bab47ffc2c2ba0eb5e2cbc5"}, + {file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd93b91ab47a3616b4d3c42b52f8363b88ca021e340804c6ab2536344fad9ca"}, + {file = "matplotlib-3.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6d1ce5ed2aefcdce11904fc5bbea7d9c21fff3d5f543841edf3dea84451a09ea"}, + {file = "matplotlib-3.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:b2696efdc08648536efd4e1601b5fd491fd47f4db97a5fbfd175549a7365c1b2"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d52a3b618cb1cbb769ce2ee1dcdb333c3ab6e823944e9a2d36e37253815f9556"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:039082812cacd6c6bec8e17a9c1e6baca230d4116d522e81e1f63a74d01d2e21"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6758baae2ed64f2331d4fd19be38b7b4eae3ecec210049a26b6a4f3ae1c85dcc"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:050598c2b29e0b9832cde72bcf97627bf00262adbc4a54e2b856426bb2ef0697"}, + {file = "matplotlib-3.9.2.tar.gz", hash = "sha256:96ab43906269ca64a6366934106fa01534454a69e471b7bf3d79083981aaab92"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.3.1" +numpy = ">=1.23" +packaging = ">=20.0" +pillow = ">=8" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" + +[package.extras] +dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setuptools (>=64)", "setuptools_scm (>=7)"] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, +] + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "minikanren" +version = "1.0.3" +description = "Relational programming in Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "miniKanren-1.0.3.tar.gz", hash = "sha256:1ec8bdb01144ad5e8752c7c297fb8a122db920f859276d25a72d164e998d7f6e"}, +] + +[package.dependencies] +cons = ">=0.4.0" +etuples = ">=0.3.1" +logical-unification = ">=0.4.1" +multipledispatch = "*" +toolz = "*" + +[[package]] +name = "multipledispatch" +version = "1.0.0" +description = "Multiple dispatch" +optional = false +python-versions = "*" +files = [ + {file = "multipledispatch-1.0.0-py3-none-any.whl", hash = "sha256:0c53cd8b077546da4e48869f49b13164bebafd0c2a5afceb6bb6a316e7fb46e4"}, + {file = "multipledispatch-1.0.0.tar.gz", hash = "sha256:5c839915465c68206c3e9c473357908216c28383b425361e5d144594bf85a7e0"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + +[[package]] +name = "nibabel" +version = "5.3.2" +description = "Access a multitude of neuroimaging data formats" +optional = false +python-versions = ">=3.9" +files = [ + {file = "nibabel-5.3.2-py3-none-any.whl", hash = "sha256:52970a5a8a53b1b55249cba4d9bcfaa8cc57e3e5af35a29d7352237e8680a6f8"}, + {file = "nibabel-5.3.2.tar.gz", hash = "sha256:0bdca6503b1c784b446c745a4542367de7756cfba0d72143b91f9ffb78be569b"}, +] + +[package.dependencies] +importlib-resources = {version = ">=5.12", markers = "python_version < \"3.12\""} +numpy = ">=1.22" +packaging = ">=20" +typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} + +[package.extras] +all = ["h5py", "pillow", "pydicom (>=2.3)", "pyzstd (>=0.14.3)", "scipy"] +dev = ["tox"] +dicom = ["pydicom (>=2.3)"] +dicomfs = ["pillow", "pydicom (>=2.3)"] +doc = ["matplotlib (>=3.5)", "numpydoc", "sphinx", "texext", "tomli"] +doctest = ["tox"] +minc2 = ["h5py"] +spm = ["scipy"] +style = ["tox"] +test = ["coverage (>=7.2)", "pytest", "pytest-cov", "pytest-doctestplus", "pytest-httpserver", "pytest-xdist"] +typing = ["tox"] +zstd = ["pyzstd (>=0.14.3)"] + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pandas" +version = "2.2.3" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, + {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, + {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, + {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, + {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, + {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, + {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + +[[package]] +name = "parso" +version = "0.8.4" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, + {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, +] + +[package.extras] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["docopt", "pytest"] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pillow" +version = "11.0.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, + {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"}, + {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"}, + {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"}, + {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"}, + {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"}, + {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"}, + {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"}, + {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"}, + {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"}, + {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"}, + {file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"}, + {file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"}, + {file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"}, + {file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"}, + {file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"}, + {file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"}, + {file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"}, + {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"}, + {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"}, + {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"}, + {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.48" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, + {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psutil" +version = "6.1.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"}, + {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:000d1d1ebd634b4efb383f4034437384e44a6d455260aaee2eca1e9c1b55f047"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5cd2bcdc75b452ba2e10f0e8ecc0b57b827dd5d7aaffbc6821b2a9a242823a76"}, + {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:045f00a43c737f960d273a83973b2511430d61f283a44c96bf13a6e829ba8fdc"}, + {file = "psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e"}, + {file = "psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, + {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b"}, + {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, + {file = "psutil-6.1.0-cp36-cp36m-win32.whl", hash = "sha256:6d3fbbc8d23fcdcb500d2c9f94e07b1342df8ed71b948a2649b5cb060a7c94ca"}, + {file = "psutil-6.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1209036fbd0421afde505a4879dee3b2fd7b1e14fee81c0069807adcbbcca747"}, + {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, + {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, + {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, +] + +[package.extras] +dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, + {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pymc" +version = "5.18.2" +description = "Probabilistic Programming in Python: Bayesian Modeling and Probabilistic Machine Learning with PyTensor" +optional = false +python-versions = ">=3.10" +files = [ + {file = "pymc-5.18.2-py3-none-any.whl", hash = "sha256:c8ff5648d16f258fd28da51db4c3feff0f538bdfe3be4f2c85021e310e5cbc3c"}, + {file = "pymc-5.18.2.tar.gz", hash = "sha256:7879b3b7ee9dcab85b1d54d465acbdbb4b0f4a7dfd1fcb868a88362935519133"}, +] + +[package.dependencies] +arviz = ">=0.13.0" +cachetools = ">=4.2.1" +cloudpickle = "*" +numpy = ">=1.15.0" +pandas = ">=0.24.0" +pytensor = ">=2.26.1,<2.27" +rich = ">=13.7.1" +scipy = ">=1.4.1" +threadpoolctl = ">=3.1.0,<4.0.0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "pyparsing" +version = "3.2.0" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"}, + {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytensor" +version = "2.26.3" +description = "Optimizing compiler for evaluating mathematical expressions on CPUs and GPUs." +optional = false +python-versions = "<3.13,>=3.10" +files = [ + {file = "pytensor-2.26.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bcce400cb435309c00af2dba8eaa8825f651eb4e39571966c141bb616ff17b5b"}, + {file = "pytensor-2.26.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89da00c829a6107f275894f75219d48c18916e48a0946b77d8a1bebd4fe995b6"}, + {file = "pytensor-2.26.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f95bc45f2f2644866b4e603d85edbdc984e609d52877e6ca9381caaa641a1b03"}, + {file = "pytensor-2.26.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:acc6e799fba9cbf7e544c17e33c976d4e7230c87a322cc69c35b710ef720087b"}, + {file = "pytensor-2.26.3-cp310-cp310-win_amd64.whl", hash = "sha256:8c63279b86001029a38e82facc3b1d4a8b24723c1fd7263e3590886f3b2c2923"}, + {file = "pytensor-2.26.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e41d3b68d6dfabb75c99cd6cb579c9fb9c95f02c40fb5561f39fa7a4b8f6fc6"}, + {file = "pytensor-2.26.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab08350715554bb53be83d3be75283bb8890caf72e97b122b4fae76244950d3f"}, + {file = "pytensor-2.26.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6b6985fb9cca966231d654d4396b293f1e33dd78a4bbb274af9f18e19defd5c3"}, + {file = "pytensor-2.26.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:06362f848cb576e378fd2ff00caf79365aa44c0fe2473d68be1de78013925518"}, + {file = "pytensor-2.26.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae418d65b752a8bac50c0a401a052854ff85c509a51c70d7c6539299a75c1ba3"}, + {file = "pytensor-2.26.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:65b6b327cd8e39440b6c090d6c7ff85192f3db99f753b76cb5483fbf3212304c"}, + {file = "pytensor-2.26.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4954fbe7b45077063166c83e100d36ad461df283c2222d0d15dc523ac312609"}, + {file = "pytensor-2.26.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:131141f7acbbe80bfa48895269ffcf4d572a0e01ebee81ef6b4f6f1042d88a59"}, + {file = "pytensor-2.26.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c124fb425bd5867c6b206cb0b8d458ab1f983b180a63581abf9d07369b90e792"}, + {file = "pytensor-2.26.3-cp312-cp312-win_amd64.whl", hash = "sha256:cce401c6020991767a2fde1d9dc7dda30a8289fc002552bc604e53623c92b8e0"}, + {file = "pytensor-2.26.3-py2.py3-none-any.whl", hash = "sha256:447ce88dd75a71dbb7158f9ccec0ffc7dc8fdb379f334ebf870003c977e4346f"}, + {file = "pytensor-2.26.3.tar.gz", hash = "sha256:703cfdba1d66b84a1739f50abdd7c18d25f788a85f2d07ed9b5bc66c929fb2fb"}, +] + +[package.dependencies] +cons = "*" +etuples = "*" +filelock = ">=3.15" +logical-unification = "*" +miniKanren = "*" +numpy = ">=1.17.0,<2" +scipy = ">=1,<2" +setuptools = ">=59.0.0" + +[package.extras] +complete = ["pytensor[jax]", "pytensor[numba]"] +development = ["pytensor[complete]", "pytensor[rtd]", "pytensor[tests]"] +jax = ["jax", "jaxlib"] +numba = ["llvmlite", "numba (>=0.57)"] +rtd = ["pydot", "pydot-ng", "pydot2", "pygments", "sphinx (>=5.1.0,<6)"] +tests = ["coverage (>=5.1)", "pre-commit", "pytest", "pytest-benchmark", "pytest-cov (>=2.6.1)", "pytest-mock", "pytest-sphinx"] + +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2024.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, +] + +[[package]] +name = "pywin32" +version = "308" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, + {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, + {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, + {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, + {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, + {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, + {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, + {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, + {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, + {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, + {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, + {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, + {file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"}, + {file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"}, + {file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"}, + {file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"}, + {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, + {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, +] + +[[package]] +name = "pyzmq" +version = "26.2.0" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629"}, + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea"}, + {file = "pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6"}, + {file = "pyzmq-26.2.0-cp311-cp311-win32.whl", hash = "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b"}, + {file = "pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e"}, + {file = "pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0"}, + {file = "pyzmq-26.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b55a4229ce5da9497dd0452b914556ae58e96a4381bb6f59f1305dfd7e53fc8"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9cb3a6460cdea8fe8194a76de8895707e61ded10ad0be97188cc8463ffa7e3a8"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ab5cad923cc95c87bffee098a27856c859bd5d0af31bd346035aa816b081fe1"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ed69074a610fad1c2fda66180e7b2edd4d31c53f2d1872bc2d1211563904cd9"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cccba051221b916a4f5e538997c45d7d136a5646442b1231b916d0164067ea27"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0eaa83fc4c1e271c24eaf8fb083cbccef8fde77ec8cd45f3c35a9a123e6da097"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9edda2df81daa129b25a39b86cb57dfdfe16f7ec15b42b19bfac503360d27a93"}, + {file = "pyzmq-26.2.0-cp37-cp37m-win32.whl", hash = "sha256:ea0eb6af8a17fa272f7b98d7bebfab7836a0d62738e16ba380f440fceca2d951"}, + {file = "pyzmq-26.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4ff9dc6bc1664bb9eec25cd17506ef6672d506115095411e237d571e92a58231"}, + {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2eb7735ee73ca1b0d71e0e67c3739c689067f055c764f73aac4cc8ecf958ee3f"}, + {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a534f43bc738181aa7cbbaf48e3eca62c76453a40a746ab95d4b27b1111a7d2"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aedd5dd8692635813368e558a05266b995d3d020b23e49581ddd5bbe197a8ab6"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8be4700cd8bb02cc454f630dcdf7cfa99de96788b80c51b60fe2fe1dac480289"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcc03fa4997c447dce58264e93b5aa2d57714fbe0f06c07b7785ae131512732"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:402b190912935d3db15b03e8f7485812db350d271b284ded2b80d2e5704be780"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8685fa9c25ff00f550c1fec650430c4b71e4e48e8d852f7ddcf2e48308038640"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:76589c020680778f06b7e0b193f4b6dd66d470234a16e1df90329f5e14a171cd"}, + {file = "pyzmq-26.2.0-cp38-cp38-win32.whl", hash = "sha256:8423c1877d72c041f2c263b1ec6e34360448decfb323fa8b94e85883043ef988"}, + {file = "pyzmq-26.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:76589f2cd6b77b5bdea4fca5992dc1c23389d68b18ccc26a53680ba2dc80ff2f"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b1d464cb8d72bfc1a3adc53305a63a8e0cac6bc8c5a07e8ca190ab8d3faa43c2"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4da04c48873a6abdd71811c5e163bd656ee1b957971db7f35140a2d573f6949c"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d049df610ac811dcffdc147153b414147428567fbbc8be43bb8885f04db39d98"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05590cdbc6b902101d0e65d6a4780af14dc22914cc6ab995d99b85af45362cc9"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c811cfcd6a9bf680236c40c6f617187515269ab2912f3d7e8c0174898e2519db"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6835dd60355593de10350394242b5757fbbd88b25287314316f266e24c61d073"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc6bee759a6bddea5db78d7dcd609397449cb2d2d6587f48f3ca613b19410cfc"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c530e1eecd036ecc83c3407f77bb86feb79916d4a33d11394b8234f3bd35b940"}, + {file = "pyzmq-26.2.0-cp39-cp39-win32.whl", hash = "sha256:367b4f689786fca726ef7a6c5ba606958b145b9340a5e4808132cc65759abd44"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6fa2e3e683f34aea77de8112f6483803c96a44fd726d7358b9888ae5bb394ec"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:7445be39143a8aa4faec43b076e06944b8f9d0701b669df4af200531b21e40bb"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ea4ad4e6a12e454de05f2949d4beddb52460f3de7c8b9d5c46fbb7d7222e02c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fc4f7a173a5609631bb0c42c23d12c49df3966f89f496a51d3eb0ec81f4519d6"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:878206a45202247781472a2d99df12a176fef806ca175799e1c6ad263510d57c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17c412bad2eb9468e876f556eb4ee910e62d721d2c7a53c7fa31e643d35352e6"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0d987a3ae5a71c6226b203cfd298720e0086c7fe7c74f35fa8edddfbd6597eed"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:39887ac397ff35b7b775db7201095fc6310a35fdbae85bac4523f7eb3b840e20"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fdb5b3e311d4d4b0eb8b3e8b4d1b0a512713ad7e6a68791d0923d1aec433d919"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:226af7dcb51fdb0109f0016449b357e182ea0ceb6b47dfb5999d569e5db161d5"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bed0e799e6120b9c32756203fb9dfe8ca2fb8467fed830c34c877e25638c3fc"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:29c7947c594e105cb9e6c466bace8532dc1ca02d498684128b339799f5248277"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cdeabcff45d1c219636ee2e54d852262e5c2e085d6cb476d938aee8d921356b3"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35cffef589bcdc587d06f9149f8d5e9e8859920a071df5a2671de2213bef592a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18c8dc3b7468d8b4bdf60ce9d7141897da103c7a4690157b32b60acb45e333e6"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7133d0a1677aec369d67dd78520d3fa96dd7f3dcec99d66c1762870e5ea1a50a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a96179a24b14fa6428cbfc08641c779a53f8fcec43644030328f44034c7f1f4"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4f78c88905461a9203eac9faac157a2a0dbba84a0fd09fd29315db27be40af9f"}, + {file = "pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "13.9.4" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "scikit-learn" +version = "1.5.2" +description = "A set of python modules for machine learning and data mining" +optional = false +python-versions = ">=3.9" +files = [ + {file = "scikit_learn-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:299406827fb9a4f862626d0fe6c122f5f87f8910b86fe5daa4c32dcd742139b6"}, + {file = "scikit_learn-1.5.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:2d4cad1119c77930b235579ad0dc25e65c917e756fe80cab96aa3b9428bd3fb0"}, + {file = "scikit_learn-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c412ccc2ad9bf3755915e3908e677b367ebc8d010acbb3f182814524f2e5540"}, + {file = "scikit_learn-1.5.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a686885a4b3818d9e62904d91b57fa757fc2bed3e465c8b177be652f4dd37c8"}, + {file = "scikit_learn-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:c15b1ca23d7c5f33cc2cb0a0d6aaacf893792271cddff0edbd6a40e8319bc113"}, + {file = "scikit_learn-1.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03b6158efa3faaf1feea3faa884c840ebd61b6484167c711548fce208ea09445"}, + {file = "scikit_learn-1.5.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1ff45e26928d3b4eb767a8f14a9a6efbf1cbff7c05d1fb0f95f211a89fd4f5de"}, + {file = "scikit_learn-1.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f763897fe92d0e903aa4847b0aec0e68cadfff77e8a0687cabd946c89d17e675"}, + {file = "scikit_learn-1.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8b0ccd4a902836493e026c03256e8b206656f91fbcc4fde28c57a5b752561f1"}, + {file = "scikit_learn-1.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:6c16d84a0d45e4894832b3c4d0bf73050939e21b99b01b6fd59cbb0cf39163b6"}, + {file = "scikit_learn-1.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f932a02c3f4956dfb981391ab24bda1dbd90fe3d628e4b42caef3e041c67707a"}, + {file = "scikit_learn-1.5.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3b923d119d65b7bd555c73be5423bf06c0105678ce7e1f558cb4b40b0a5502b1"}, + {file = "scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd"}, + {file = "scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:394397841449853c2290a32050382edaec3da89e35b3e03d6cc966aebc6a8ae6"}, + {file = "scikit_learn-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:57cc1786cfd6bd118220a92ede80270132aa353647684efa385a74244a41e3b1"}, + {file = "scikit_learn-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5"}, + {file = "scikit_learn-1.5.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908"}, + {file = "scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3"}, + {file = "scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12"}, + {file = "scikit_learn-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f"}, + {file = "scikit_learn-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:757c7d514ddb00ae249832fe87100d9c73c6ea91423802872d9e74970a0e40b9"}, + {file = "scikit_learn-1.5.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:52788f48b5d8bca5c0736c175fa6bdaab2ef00a8f536cda698db61bd89c551c1"}, + {file = "scikit_learn-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:643964678f4b5fbdc95cbf8aec638acc7aa70f5f79ee2cdad1eec3df4ba6ead8"}, + {file = "scikit_learn-1.5.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca64b3089a6d9b9363cd3546f8978229dcbb737aceb2c12144ee3f70f95684b7"}, + {file = "scikit_learn-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:3bed4909ba187aca80580fe2ef370d9180dcf18e621a27c4cf2ef10d279a7efe"}, + {file = "scikit_learn-1.5.2.tar.gz", hash = "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d"}, +] + +[package.dependencies] +joblib = ">=1.2.0" +numpy = ">=1.19.5" +scipy = ">=1.6.0" +threadpoolctl = ">=3.1.0" + +[package.extras] +benchmark = ["matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "pandas (>=1.1.5)"] +build = ["cython (>=3.0.10)", "meson-python (>=0.16.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)"] +docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.3.4)", "memory_profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pydata-sphinx-theme (>=0.15.3)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)", "sphinx (>=7.3.7)", "sphinx-copybutton (>=0.5.2)", "sphinx-design (>=0.5.0)", "sphinx-design (>=0.6.0)", "sphinx-gallery (>=0.16.0)", "sphinx-prompt (>=1.4.0)", "sphinx-remove-toctrees (>=1.0.0.post1)", "sphinxcontrib-sass (>=0.3.4)", "sphinxext-opengraph (>=0.9.1)"] +examples = ["matplotlib (>=3.3.4)", "pandas (>=1.1.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.17.2)", "seaborn (>=0.9.0)"] +install = ["joblib (>=1.2.0)", "numpy (>=1.19.5)", "scipy (>=1.6.0)", "threadpoolctl (>=3.1.0)"] +maintenance = ["conda-lock (==2.5.6)"] +tests = ["black (>=24.3.0)", "matplotlib (>=3.3.4)", "mypy (>=1.9)", "numpydoc (>=1.2.0)", "pandas (>=1.1.5)", "polars (>=0.20.30)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pyarrow (>=12.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.2.1)", "scikit-image (>=0.17.2)"] + +[[package]] +name = "scipy" +version = "1.14.1" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "scipy-1.14.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b28d2ca4add7ac16ae8bb6632a3c86e4b9e4d52d3e34267f6e1b0c1f8d87e389"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8bddf15838ba768bb5f5083c1ea012d64c9a444e16192762bd858f1e126196d0"}, + {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:97c5dddd5932bd2a1a31c927ba5e1463a53b87ca96b5c9bdf5dfd6096e27efc3"}, + {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d"}, + {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69"}, + {file = "scipy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8426251ad1e4ad903a4514712d2fa8fdd5382c978010d1c6f5f37ef286a713ad"}, + {file = "scipy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:a49f6ed96f83966f576b33a44257d869756df6cf1ef4934f59dd58b25e0327e5"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617"}, + {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8"}, + {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37"}, + {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2"}, + {file = "scipy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2"}, + {file = "scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5"}, + {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc"}, + {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310"}, + {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066"}, + {file = "scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1"}, + {file = "scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73"}, + {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e"}, + {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d"}, + {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e"}, + {file = "scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06"}, + {file = "scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84"}, + {file = "scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417"}, +] + +[package.dependencies] +numpy = ">=1.23.5,<2.3" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<=7.3.7)", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.0)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "seaborn" +version = "0.13.2" +description = "Statistical data visualization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987"}, + {file = "seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7"}, +] + +[package.dependencies] +matplotlib = ">=3.4,<3.6.1 || >3.6.1" +numpy = ">=1.20,<1.24.0 || >1.24.0" +pandas = ">=1.2" + +[package.extras] +dev = ["flake8", "flit", "mypy", "pandas-stubs", "pre-commit", "pytest", "pytest-cov", "pytest-xdist"] +docs = ["ipykernel", "nbconvert", "numpydoc", "pydata_sphinx_theme (==0.10.0rc2)", "pyyaml", "sphinx (<6.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-issues"] +stats = ["scipy (>=1.7)", "statsmodels (>=0.12)"] + +[[package]] +name = "setuptools" +version = "75.5.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +files = [ + {file = "setuptools-75.5.0-py3-none-any.whl", hash = "sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829"}, + {file = "setuptools-75.5.0.tar.gz", hash = "sha256:5c4ccb41111392671f02bb5f8436dfc5a9a7185e80500531b133f5775c4163ef"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] +core = ["importlib-metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "sphinx" +version = "8.1.3" +description = "Python documentation generator" +optional = false +python-versions = ">=3.10" +files = [ + {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"}, + {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"}, +] + +[package.dependencies] +alabaster = ">=0.7.14" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" +imagesize = ">=1.3" +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = ">=1.0.7" +sphinxcontrib-devhelp = ">=1.0.6" +sphinxcontrib-htmlhelp = ">=2.0.6" +sphinxcontrib-jsmath = ">=1.0.1" +sphinxcontrib-qthelp = ">=1.0.6" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.0.2" +description = "Read the Docs theme for Sphinx" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13"}, + {file = "sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85"}, +] + +[package.dependencies] +docutils = ">0.18,<0.22" +sphinx = ">=6,<9" +sphinxcontrib-jquery = ">=4,<5" + +[package.extras] +dev = ["bump2version", "transifex-client", "twine", "wheel"] + +[[package]] +name = "sphinx-tabs" +version = "3.4.7" +description = "Tabbed views for Sphinx" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sphinx-tabs-3.4.7.tar.gz", hash = "sha256:991ad4a424ff54119799ba1491701aa8130dd43509474aef45a81c42d889784d"}, + {file = "sphinx_tabs-3.4.7-py3-none-any.whl", hash = "sha256:c12d7a36fd413b369e9e9967a0a4015781b71a9c393575419834f19204bd1915"}, +] + +[package.dependencies] +docutils = "*" +pygments = "*" +sphinx = ">=1.8" + +[package.extras] +code-style = ["pre-commit (==2.13.0)"] +testing = ["bs4", "coverage", "pygments", "pytest (>=7.1,<8)", "pytest-cov", "pytest-regressions", "rinohtype"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +description = "Extension to include jQuery on newer Sphinx releases" +optional = false +python-versions = ">=2.7" +files = [ + {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, + {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, +] + +[package.dependencies] +Sphinx = ">=1.8" + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "threadpoolctl" +version = "3.5.0" +description = "threadpoolctl" +optional = false +python-versions = ">=3.8" +files = [ + {file = "threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467"}, + {file = "threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107"}, +] + +[[package]] +name = "tomli" +version = "2.1.0" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, + {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, +] + +[[package]] +name = "toolz" +version = "1.0.0" +description = "List processing tools and functional utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236"}, + {file = "toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02"}, +] + +[[package]] +name = "tornado" +version = "6.4.1" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">=3.8" +files = [ + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, + {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, + {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, + {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +files = [ + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "widgetsnbextension" +version = "4.0.13" +description = "Jupyter interactive widgets for Jupyter Notebook" +optional = false +python-versions = ">=3.7" +files = [ + {file = "widgetsnbextension-4.0.13-py3-none-any.whl", hash = "sha256:74b2692e8500525cc38c2b877236ba51d34541e6385eeed5aec15a70f88a6c71"}, + {file = "widgetsnbextension-4.0.13.tar.gz", hash = "sha256:ffcb67bc9febd10234a362795f643927f4e0c05d9342c727b65d2384f8feacb6"}, +] + +[[package]] +name = "xarray" +version = "2024.10.0" +description = "N-D labeled arrays and datasets in Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "xarray-2024.10.0-py3-none-any.whl", hash = "sha256:ae1d38cb44a0324dfb61e492394158ae22389bf7de9f3c174309c17376df63a0"}, + {file = "xarray-2024.10.0.tar.gz", hash = "sha256:e369e2bac430e418c2448e5b96f07da4635f98c1319aa23cfeb3fbcb9a01d2e0"}, +] + +[package.dependencies] +numpy = ">=1.24" +packaging = ">=23.1" +pandas = ">=2.1" + +[package.extras] +accel = ["bottleneck", "flox", "numba (>=0.54)", "numbagg", "opt-einsum", "scipy"] +complete = ["xarray[accel,etc,io,parallel,viz]"] +dev = ["hypothesis", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-env", "pytest-timeout", "pytest-xdist", "ruff", "sphinx", "sphinx-autosummary-accessors", "xarray[complete]"] +etc = ["sparse"] +io = ["cftime", "fsspec", "h5netcdf", "netCDF4", "pooch", "pydap", "scipy", "zarr"] +parallel = ["dask[complete]"] +viz = ["cartopy", "matplotlib", "nc-time-axis", "seaborn"] + +[[package]] +name = "xarray-einstats" +version = "0.8.0" +description = "Stats, linear algebra and einops for xarray" +optional = false +python-versions = ">=3.10" +files = [ + {file = "xarray_einstats-0.8.0-py3-none-any.whl", hash = "sha256:fd00552c3fb5c859b1ebc7c88a97342d3bb93d14bba904c5a9b94a4f724b76b4"}, + {file = "xarray_einstats-0.8.0.tar.gz", hash = "sha256:7f1573f9bd4d60d6e7ed9fd27c4db39da51ec49bf8ba654d4602a139a6309d7f"}, +] + +[package.dependencies] +numpy = ">=1.23" +scipy = ">=1.9" +xarray = ">=2022.09.0" + +[package.extras] +doc = ["furo", "jupyter-sphinx", "matplotlib", "myst-nb", "myst-parser[linkify]", "numpydoc", "sphinx (>=5)", "sphinx-copybutton", "sphinx-design", "sphinx-togglebutton", "watermark"] +einops = ["einops"] +numba = ["numba (>=0.55)"] +test = ["hypothesis", "packaging", "pytest", "pytest-cov"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.10,<3.13" +content-hash = "858856891da45344da31868e4092894a8e14559329d189c7cbba46df4d221901" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..e15a13a2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[tool.poetry] +name = "pcntoolkit" +version = "0.31.0" +description = "Predictive Clinical Neuroscience Toolkit" +authors = ["Andre Marquand"] +license = "GNU GPLv3" +readme = "README.md" + +[tool.poetry.dependencies] +python = ">=3.10,<3.13" +bspline = "^0.1.1" +nibabel = "^5.3.1" +pymc = "^5.18.0" +scikit-learn = "^1.5.2" +six = "^1.16.0" +scipy = "^1.12" +matplotlib = "^3.9.2" +seaborn = "^0.13.2" +numpy = "^1.26" + +[tool.poetry.group.dev.dependencies] +sphinx-tabs = "^3.4.7" +pytest = "^8.3.3" +ipywidgets = "^8.1.5" +black = "^24.10.0" +ipykernel = "^6.29.5" +sphinx-rtd-theme = "^3.0.2" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[requires-python] +python_version = ">=3.10,<3.13" + +[tool.poetry.scripts] +normative = "pcntoolkit.normative:entrypoint" diff --git a/pytest_tests/test_shash.py b/pytest_tests/test_shash.py new file mode 100644 index 00000000..2e81460a --- /dev/null +++ b/pytest_tests/test_shash.py @@ -0,0 +1,100 @@ +import numpy as np +from numpy.testing import assert_almost_equal, assert_array_almost_equal + +from pcntoolkit.model.SHASH import SHASH, C, S, S_inv, SHASHb, SHASHo, SHASHo2 + + +def test_shash_transformations(): + """Test the basic SHASH transformations (S, S_inv, C)""" + x = np.array([-2.0, -1.0, 0.0, 1.0, 2.0]) + epsilon = 0.5 + delta = 1.5 + + # Test S transformation + s_result = S(x, epsilon, delta) + # Test inverse relationship + s_inv_result = S_inv(s_result, epsilon, delta) + assert_array_almost_equal(x, s_inv_result, decimal=6) + + # Test C transformation + c_result = C(x, epsilon, delta) + # C should always be >= 1 since it's sqrt(1 + S^2) + assert np.all(c_result >= 1.0) + # Test relationship between S and C + assert_array_almost_equal(c_result, np.sqrt(1 + s_result**2), decimal=6) + + +def test_moment_calculations(): + """Test moment calculation functions""" + epsilon = 0.5 + delta = 1.5 + + # Test compute_moments function + mean, var = SHASH.m1m2(epsilon, delta) + assert_almost_equal(mean.eval(), SHASH.m1(epsilon, delta).eval(), decimal=6) + assert_almost_equal( + var.eval(), SHASH.m2(epsilon, delta).eval() - mean.eval() ** 2, decimal=6 + ) + + +def test_shash_random_generation(): + """Test random number generation for SHASH distributions""" + rng = np.random.RandomState(42) + n_samples = 1000 + + # Test base SHASH + epsilon, delta = 0.5, 1.5 + samples = SHASH.rv_op.rng_fn(rng, epsilon, delta, size=n_samples) + assert samples.shape == (n_samples,) + + # Test SHASHo + mu, sigma = 1.0, 2.0 + samples_o = SHASHo.rv_op.rng_fn(rng, mu, sigma, epsilon, delta, size=n_samples) + assert samples_o.shape == (n_samples,) + + # Test SHASHo2 + samples_o2 = SHASHo2.rv_op.rng_fn(rng, mu, sigma, epsilon, delta, size=n_samples) + assert samples_o2.shape == (n_samples,) + + # Test SHASHb + samples_b = SHASHb.rv_op.rng_fn(rng, mu, sigma, epsilon, delta, size=n_samples) + assert samples_b.shape == (n_samples,) + + +def test_shash_distribution_properties(): + """Test statistical properties of SHASH distributions""" + rng = np.random.RandomState(42) + n_samples = 10000 + mu, sigma = 1.0, 2.0 + epsilon, delta = 0.5, 1.5 + + # Generate samples from SHASHb + samples = SHASHb.rv_op.rng_fn(rng, mu, sigma, epsilon, delta, size=n_samples) + + # Check mean and standard deviation are close to specified values + assert_almost_equal(np.mean(samples), mu, decimal=1) + assert_almost_equal(np.std(samples), sigma, decimal=1) + + +def test_edge_cases(): + """Test edge cases and boundary conditions""" + x = np.array([-1e10, 0.0, 1e10]) # Test very large/small values + epsilon = 0.0 # Test zero skewness + delta = 1.0 # Test unit tail weight + + # S transformation should handle extreme values + s_result = S(x, epsilon, delta) + assert not np.any(np.isnan(s_result)) + assert not np.any(np.isinf(s_result)) + + # C transformation should handle extreme values + c_result = C(x, epsilon, delta) + assert not np.any(np.isnan(c_result)) + assert not np.any(np.isinf(c_result)) + assert np.all(c_result >= 1.0) + + # Test moment calculations with edge case parameters + mean, var = SHASH.m1m2(0.0, 1.0) # Standard case + assert not np.isnan(mean.eval()) + assert not np.isnan(var.eval()) + assert var.eval() > 0 # Variance should always be positive diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 58c3f61e..00000000 --- a/requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -argparse -nibabel>=2.5.1 -six -scikit-learn -bspline -matplotlib -numpy -scipy>=1.3.2,<1.13.0 -pandas>=0.25.3 -torch>=1.1.0 -sphinx-tabs -pymc>=5.1.0 -arviz==0.13.0 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 9b914123..00000000 --- a/setup.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[autopep8] -max-line-length = 120 -ignore = E226,E302,E41 diff --git a/setup.py b/setup.py deleted file mode 100644 index d37cd554..00000000 --- a/setup.py +++ /dev/null @@ -1,25 +0,0 @@ -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') - -# Note: to force PyPI to overwrite a version without bumping the version number -# use e.g.: -# version = '0.29-1' - -setup(name='pcntoolkit', - version='0.30-2', - description='Predictive Clinical Neuroscience toolkit', - url='http://github.com/amarquand/PCNtoolkit', - author='Andre Marquand', - author_email='andre.marquand@donders.ru.nl', - license='GNU GPLv3', - packages=find_packages(), - install_requires=requirements, - zip_safe=False) diff --git a/tests/cli_test/split_data.py b/tests/cli_test/split_data.py new file mode 100644 index 00000000..bb2caa98 --- /dev/null +++ b/tests/cli_test/split_data.py @@ -0,0 +1,65 @@ +import argparse +import os + +import numpy as np +import pandas as pd + +# Import train_test_split from sklearn +from sklearn.model_selection import train_test_split + +# Import the StandardScaler from sklearn +from sklearn.preprocessing import StandardScaler + +from pcntoolkit.util.utils import create_design_matrix + + +def main(): + + np.random.seed(42) + + parser = argparse.ArgumentParser() + parser.add_argument("--input_file", type=str, required=True) + parser.add_argument("--output_dir", type=str, required=True) + args = parser.parse_args() + infile=args.input_file.split("/")[-1] + + print(f"Splitting the data located at {args.input_file} into train and test covariates, responses and batch effects...") + df = pd.read_csv(args.input_file) + + # Select the covariates, responses and batch effects + cov = df['age'] + resp = df[['SubCortGrayVol','Left-Hippocampus','Brain-Stem','CSF']] + be = df['site'] + + # Standardize the covariates and responses + cov = StandardScaler().fit_transform(cov.to_numpy()[:,np.newaxis]) + resp = StandardScaler().fit_transform(resp.to_numpy()) + + # Map the batch effects to integers + be_ids = np.unique(be, return_inverse=True)[1] + + # Split the data into training and test sets + train_idx, test_idx = train_test_split(np.arange(len(cov)), test_size=0.2, stratify=be_ids) + + # Create the design matrices + mean_basis = 'linear' + var_basis = 'linear' + Phi_tr = create_design_matrix(cov[train_idx], basis=mean_basis, intercept=False, site_ids=be_ids[train_idx]) + Phi_var_tr = create_design_matrix(cov[train_idx], basis=var_basis) + Phi_te = create_design_matrix(cov[test_idx], basis=mean_basis, intercept=False, site_ids=be_ids[test_idx]) + Phi_var_te = create_design_matrix(cov[test_idx], basis=var_basis) + + # Save everything + pd.to_pickle(pd.DataFrame(Phi_tr), os.path.join(args.output_dir, f'X_tr_{infile}.pkl')) + pd.to_pickle(pd.DataFrame(Phi_var_tr), os.path.join(args.output_dir, f'X_var_tr_{infile}.pkl')) + pd.to_pickle(pd.DataFrame(Phi_te), os.path.join(args.output_dir, f'X_te_{infile}.pkl')) + pd.to_pickle(pd.DataFrame(Phi_var_te), os.path.join(args.output_dir, f'X_var_te_{infile}.pkl')) + pd.to_pickle(pd.DataFrame(resp[train_idx]), os.path.join(args.output_dir, f'Y_tr_{infile}.pkl')) + pd.to_pickle(pd.DataFrame(resp[test_idx]), os.path.join(args.output_dir, f'Y_te_{infile}.pkl')) + pd.to_pickle(be[train_idx], os.path.join(args.output_dir, f'be_tr_{infile}.pkl')) + pd.to_pickle(be[test_idx], os.path.join(args.output_dir, f'be_te_{infile}.pkl')) + + print(f"Done! The files can be found in: {args.output_dir}") + +if __name__ == "__main__": + main() diff --git a/tests/cli_test/test_cli.sh b/tests/cli_test/test_cli.sh new file mode 100755 index 00000000..92aa890b --- /dev/null +++ b/tests/cli_test/test_cli.sh @@ -0,0 +1,22 @@ +#! /bin/bash +set -x + +# Assign the current directory to a variable +export testdir=$(pwd) +export tempdir="$testdir/temp" +mkdir $tempdir +chmod -R 766 $tempdir +export data_name="fcon1000" +export model_config="-a blr warp=WarpSinArcsinh optimizer=l-bfgs-b warp_reparam=True" +echo "Downloading the data..." +curl -o $tempdir/$data_name https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/refs/heads/main/data/$data_name.csv +echo "Splitting the data into train and test covariates, responses and batch effects..." +python split_data.py --input_file $tempdir/$data_name --output_dir $tempdir +echo "Fitting the model..." +normative $tempdir/Y_tr_$data_name.pkl -c $tempdir/X_tr_$data_name.pkl -f fit $model_config +echo "Predicting the test set..." +normative $tempdir/Y_te_$data_name.pkl -c $tempdir/X_te_$data_name.pkl -f predict $model_config inputsuffix=fit outputsuffix=predict +echo "Also doing estimate..." +normative $tempdir/Y_tr_$data_name.pkl -c $tempdir/X_tr_$data_name.pkl -f estimate $model_config -t $tempdir/X_te_$data_name.pkl -r $tempdir/Y_te_$data_name.pkl outputsuffix=estimate +echo "Done!" +rm -R $tempdir \ No newline at end of file diff --git a/tests/profile_trendsurf.py b/tests/profile_trendsurf.py index 3877122f..3e0efefa 100644 --- a/tests/profile_trendsurf.py +++ b/tests/profile_trendsurf.py @@ -1,11 +1,12 @@ # NOTE: must be run with kernprof (otherwise the inmports get screwed up) # import pcntoolkit +import os +import sys + 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') diff --git a/tests/testHBR.py b/tests/testHBR.py index fee7065e..cd4e833e 100644 --- a/tests/testHBR.py +++ b/tests/testHBR.py @@ -1,5 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- + +# %% """ Created on Mon Jul 29 13:26:35 2019 @@ -9,101 +11,156 @@ """ -import os +# %% +from warnings import filterwarnings + +import matplotlib.pyplot as plt import numpy as np + from pcntoolkit.normative_model.norm_utils import norm_init from pcntoolkit.util.utils import simulate_data -import matplotlib.pyplot as plt -from pcntoolkit.normative import estimate -from warnings import filterwarnings -from pcntoolkit.util.utils import scaler -import xarray -filterwarnings('ignore') +filterwarnings("ignore") -np.random.seed(10) ########################### Experiment Settings ############################### -working_dir = '/home/stijn/temp/' # Specifyexit() a working directory -# to save data and results. +random_state = 40 +working_dir = "/Users/stijndeboer/temp/HBR/" # Specify a working directory 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_samples = 500 # Number of samples in each group (use a list for different +simulation_method = "linear" +n_features = 1 # The number of input features of X +n_grps = 3 # Number of batches in data +n_samples = 500 # Number of samples in each group (use a list for different # sample numbers across different batches) -model_types = ['linear', 'polynomial', 'bspline'] # models to try - -############################## Data Simulation ################################ +model_type = "linear" # modelto try 'linear, ''polynomial', 'bspline' -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, - 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.estimate(X_train, Y_train, trbefile=working_dir+'trbefile.pkl') - yhat, ys2 = nm.predict(X_test, tsbefile=working_dir+'tsbefile.pkl') +############################## Data Simulation ################################ - for i in range(n_features): - 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_yhat = yhat[sorted_idx,] - temp_s2 = ys2[sorted_idx,] +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, + working_dir=working_dir, + plot=True, + noise="heteroscedastic_nongaussian", + random_state=random_state, +) + +################################# Fittig and Predicting ############################### + +nm = norm_init( + X_train, + Y_train, + alg="hbr", + model_type=model_type, + likelihood="SHASHb", + linear_sigma="True", + random_slope_mu="True", + linear_epsilon="False", + linear_delta="False", + nuts_sampler="nutpie", +) + +nm.estimate(X_train, Y_train, trbefile=working_dir + "trbefile.pkl") +yhat, ys2 = nm.predict(X_test, tsbefile=working_dir + "tsbefile.pkl") + + +################################# Plotting Quantiles ############################### +for i in range(n_features): + 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_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) + ) + # Showing the quantiles + resolution = 200 + synth_X = np.linspace(np.min(X_train), np.max(X_train), 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.title("Model %s, Feature %d" % (model_type, i)) + plt.legend() + plt.show(block=False) + plt.savefig(working_dir + "quantiles_" + model_type + "_feature_" + str(i) + ".png") + + for j in range(n_grps): 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,]), - 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)) - col = scat1.get_facecolors()[0] - plt.plot(synth_X, q.T, linewidth=1, color=col, zorder=0) - - plt.title('Model %s, Feature %d' % (model_type, i)) - plt.legend() - plt.show() + plt.scatter(temp_X[temp_be == j,], temp_Y[temp_be == j,]) + plt.plot(temp_X[temp_be == j,], temp_yhat[temp_be == j,], color="red") + plt.fill_between( + temp_X[temp_be == j,].squeeze(), + (temp_yhat[temp_be == j,] - 2 * np.sqrt(temp_s2[temp_be == j,])).squeeze(), + (temp_yhat[temp_be == j,] + 2 * np.sqrt(temp_s2[temp_be == j,])).squeeze(), + color="red", + alpha=0.2, + ) + plt.title("Model %s, Group %d, Feature %d" % (model_type, j, i)) + plt.show(block=False) + plt.savefig( + working_dir + + "pred_" + + model_type + + "_group_" + + str(j) + + "_feature_" + + str(i) + + ".png" + ) ############################## Normative Modelling Test ####################### +# covfile = working_dir + 'X_train.pkl' +# respfile = working_dir + 'Y_train.pkl' +# testcov = working_dir + 'X_test.pkl' +# testresp = working_dir + 'Y_test.pkl' +# trbefile = working_dir + 'trbefile.pkl' +# tsbefile = working_dir + 'tsbefile.pkl' -model_type = model_types[0] - -covfile = working_dir + 'X_train.pkl' -respfile = working_dir + 'Y_train.pkl' -testcov = working_dir + 'X_test.pkl' -testresp = working_dir + 'Y_test.pkl' -trbefile = working_dir + 'trbefile.pkl' -tsbefile = working_dir + 'tsbefile.pkl' +# os.chdir(working_dir) -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, - savemodel='True', saveoutput='True') +# estimate(covfile, respfile, testcovfile_path=testcov, testrespfile_path=testresp, trbefile=trbefile, +# tsbefile=tsbefile, alg='hbr', outputsuffix='_' + model_type, +# inscaler='None', outscaler='None', model_type=model_type, +# savemodel='True', saveoutput='True') ############################################################################### + +# %% + +for j in range(n_grps): + # Showing the quantiles + resolution = 200 + synth_X = np.linspace(np.min(X_train), np.max(X_train), resolution) + q = nm.get_mcmc_quantiles(synth_X, batch_effects=j * np.ones(resolution)) + plt.figure() + plt.scatter(temp_X[temp_be == j,], temp_Y[temp_be == j,]) + plt.plot(synth_X, q.T, color="black") + plt.title("Model %s, Group %d, Feature %d" % (model_type, j, i)) + plt.show(block=False) + plt.savefig( + working_dir + + "pred_" + + model_type + + "_group_" + + str(j) + + "_feature_" + + str(i) + + ".png" + ) +# %% diff --git a/tests/testHBR_transfer.py b/tests/testHBR_transfer.py index 029ad6b1..29ec9c96 100644 --- a/tests/testHBR_transfer.py +++ b/tests/testHBR_transfer.py @@ -1,5 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- + +# %% """ Created on Mon Jul 29 13:26:35 2019 @@ -26,7 +28,7 @@ ########################### Experiment Settings ############################### -working_dir = '/home/stijn/temp/' # Specifyexit() a working directory +working_dir = '/Users/stijndeboer/temp/HBR_transfer/' # Specifyexit() a working directory # to save data and results. simulation_method = 'linear' @@ -54,7 +56,7 @@ 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") + n_chains=4, cores=4, n_samples=100, n_tuning=50, freedom=5, nknots=8, target_accept="0.99", nuts_sampler='nutpie') print("Now Estimating on original train data ==============================================") nm.estimate(X_train, Y_train, trbefile=working_dir+'trbefile.pkl') @@ -86,7 +88,7 @@ print("Now Estimating on transfer train data ==============================================") nm.estimate_on_new_sites( X_train_transfer, Y_train_transfer, grp_id_train_transfer) - print("Now Estimating on transfer test data ==============================================") + print("Now Predicting on transfer test data ==============================================") yhat, s2 = nm.predict_on_new_sites(X_test_transfer, grp_id_test_transfer) for i in range(n_features): @@ -108,6 +110,7 @@ color='gray', alpha=0.2) plt.title('Transfer model %s, Feature %d' % (model_type, i)) plt.legend() + plt.savefig(os.path.join(working_dir, 'transfer_model_' + model_type + '_feature_' + str(i) + '.png')) plt.show() diff --git a/tests/test_HBR.ipynb b/tests/test_HBR.ipynb new file mode 100644 index 00000000..45ec901a --- /dev/null +++ b/tests/test_HBR.ipynb @@ -0,0 +1,2686 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "from IPython.display import clear_output, DisplayHandle\n", + "def update_patch(self, obj):\n", + " clear_output(wait=True)\n", + " self.display(obj)\n", + "DisplayHandle.update = update_patch\n", + "import os\n", + "import numpy as np\n", + "from pcntoolkit.normative_model.norm_utils import norm_init\n", + "from pcntoolkit.util.utils import simulate_data\n", + "import matplotlib.pyplot as plt\n", + "from pcntoolkit.normative import estimate\n", + "from warnings import filterwarnings\n", + "filterwarnings('ignore')\n", + "\n", + "plt.rcParams.update({'font.size': 8, 'figure.figsize': (5, 3)})\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
                            " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(80000,)\n" + ] + } + ], + "source": [ + "########################### Experiment Settings ###############################\n", + "random_state = 29\n", + "working_dir = '/Users/stijndeboer/temp/' # Specify a working directory to save data and results.\n", + "simulation_method = 'linear'\n", + "n_features = 1 # The number of input features of X\n", + "n_grps = 80 # Number of batches in data\n", + "n_samples = 1000 # Number of samples in each group (use a list for different\n", + "# sample numbers across different batches)\n", + "model_type = 'bspline' # modelto try 'linear, ''polynomial', 'bspline'\n", + "############################## Data Simulation ################################\n", + "X_train, Y_train, grp_id_train, X_test, Y_test, grp_id_test, coef = \\\n", + " simulate_data(simulation_method, n_samples, n_features, n_grps,\n", + " working_dir=working_dir, plot=True, noise='heteroscedastic_nongaussian',\n", + " random_state=random_state)\n", + "# plt.tight_layout()\n", + "# plt.show()\n", + "print(Y_train.shape)\n", + "\n", + "# random_group_offsets = np.random.normal(0, 1, n_grps)\n", + "# print(random_group_offsets[grp_id_train])s\n", + "# Y_train += np.squeeze(np.array(random_group_offsets[grp_id_train]))\n", + "# Y_test += np.squeeze(np.array(random_group_offsets[grp_id_test]))\n", + "s" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "nm = norm_init(X_train, Y_train, alg='hbr', model_type=model_type, likelihood='SHASHb',\n", + " random_intercept_mu='True', random_slope_mu='False', linear_sigma='True', linear_delta='False',linear_epsilon='False', nuts_sampler='nutpie')" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
                            \n", + "

                            Sampler Progress

                            \n", + "

                            Total Chains: 1

                            \n", + "

                            Active Chains: 0

                            \n", + "

                            \n", + " Finished Chains:\n", + " 1\n", + "

                            \n", + "

                            Sampling for an hour

                            \n", + "

                            \n", + " Estimated Time to Completion:\n", + " now\n", + "

                            \n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
                            ProgressDrawsDivergencesStep SizeGradients/Draw
                            \n", + " \n", + " \n", + " 150000.02511
                            \n", + "
                            \n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Graph is constructed here\n", + "nm.estimate(X_train, Y_train, trbefile=working_dir+'trbefile.pkl')" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "yhat, ys2 = nm.predict(X_test, tsbefile=working_dir+'tsbefile.pkl')" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Sampling: [y_like]\n" + ] + }, + { + "data": { + "text/html": [ + "
                            \n"
                            +      ],
                            +      "text/plain": []
                            +     },
                            +     "metadata": {},
                            +     "output_type": "display_data"
                            +    },
                            +    {
                            +     "data": {
                            +      "text/html": [
                            +       "
                            \n",
                            +       "
                            \n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAbEAAAUMCAYAAAC+y6wJAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOydd3gc1dWH3zsz23elVZcsyd24F1wBY2xjejUJCcVAAgkBPkgjJEAgCeT7IIUSEgiBhBAInYQSOgZjwGCwDe7dli0XSbZ62T7lfn+stNJa1cYGE+Z9Hj32zty5c2dWmt+cc889R0gpJTY2NjY2Nl9ClC96ADY2NjY2NgeKLWI2NjY2Nl9abBGzsbGxsfnSYouYjY2Njc2XFlvEbGxsbGy+tNgiZmNjY2PzpcUWMRsbGxubLy22iNnY2NjYfGmxRczGxsbG5kuLLWI2XzpuvvlmZs2a1ef2t9xyC8cee+wB7/+sfPvb3+aiiy5KfR44cCAPPfTQITufjc1XCVvEbA46s2bNQgjBww8/nLY9HA4TCAQQQlBeXv7FDO4wYNmyZcybN+9zPacQotPPDTfccFD6Likp4ZFHHjkofR0ImzdvZtasWXg8HgYNGtTp987mvxvtix6AzX8nJSUlPPbYY1x22WWpbc8//zzBYJBQKPQFjuyLJy8v7ws577PPPsuMGTNSn/1+/xcyjq6wLAvLstC0/Xsk6brO6aefzoQJE1i2bBlLlizhiiuuYMCAAcyZM+cQjdbmcMK2xGwOCV/72tdYtmwZO3fuTG375z//meZW67h96NChuFwuxo4dy+uvv562/1//+hcDBgzA5/NxySWXEIvF0vabpskvfvELSkpKCAQCzJo1i9WrV+/3mO+44w7y8/PJysri5z//OW25saWU3HjjjRQXF+N2uxk8eDAPPvggAOXl5QghePbZZ5kwYQJut5vjjz+eXbt2dXueju7EtuNffPFFpk6dis/nY9asWWn3DeBPf/oTgwcPxuv1MmXKFN599939vr6srCwKCwtTP20itnz58pQlM3DgQH71q19hGEbquB/96Eepc48ePZpnnnkmtW/WrFlUVFRw6aWXIoRIuXlnzZrFzTff3Ot1/+tf/2LKlCm43W7WrFmz39/l66+/zq5du3j44YcZM2YM3/nOd7jgggu499579/v+2Hw5sUXM5pAQCAQ466yzePzxxwGoqKjgo48+4hvf+EZau8WLF3PZZZfxgx/8gNWrV3POOecwd+7clLuxrKyMCy+8kO9+97ssX76cYcOGpQSkjVtvvZXXXnuNp556ihUrVjB9+nROPPFEmpub+zzeVatW8dFHH7Fw4UL+9re/cd999/Hoo48CSRF98sknefbZZ9m0aRN///vfKSgoSDv+pptu4ve//z1LlizBMAwuvvji/bpft9xyC7/73e9YunQpkUiEH//4x6l9Dz/8MH/84x+5//77Wbt2LZdccgmnnXbaQXHJ1tXVceKJJ3LaaaexZs0aHnnkEZ588knuuuuuVJucnByefvpp1q5dy/e//30uvvhi1qxZAySt66KiIu655x6qqqp4/vnn9+v8v/zlL7nttttYv349gwcP3u/vcunSpUyZMoVAIJDaNmfOHJYsWXIAd8PmS4m0sTnIzJw5U950003y9ddflyNGjJBSSvnb3/5Wnn/++XLLli0SkNu3b5dSSnneeefJb3zjG2nHT5s2TV533XVSSil/9rOfyWnTpnXaP3PmTCmllNFoVHo8HrlmzZq0NsOGDZOPPfaYlFLKX/3qV3L69OndjvdXv/qV9Hg8sr6+PrXtpptukpMmTZJSSnnnnXfKOXPmSMuyOh27fft2Cci//OUvqW1t19g2pm9961ty3rx5qf0DBgyQf/vb39KOf+aZZ1L7n3zySZmTk5P6PGjQIPnyyy+nnffEE0+U//u//9vtNe0LIN1ut/T5fKmfiooKeeutt8qvf/3raW2feOIJOWTIkG77Ovnkk+Wtt96a+lxcXCz/8Y9/pLVp+x3oSFfX/cgjj6T29+W73JfLL79cfu1rX0vb9uqrr0pVVbsdv81/F/acmM0h48QTT6SxsZFly5bx2GOPcccdd3Rqs2nTpk5Wy9FHH82mTZtS+6dOnZq2f+rUqSkXU1lZGdFolKOOOiqtTTQaZdu2bX0e69ChQ8nKyko7xx//+EcAvv71r3PXXXcxcuRITj31VObOncvMmTM7jWnfvjZt2sSYMWP6dP6xY8em/l9YWEhdXR2maRKNRtm+fTvnnXceQohUm3g8TklJSZ+vD+DBBx/kmGOOSX0uKChgzZo1vPTSS2nzY6Zpous6lmWhKAqPPvoo9957L+Xl5cRiMeLxOKWlpft17u448sgjU/8/kO9S2uUQv/LYImZzyFBVlQsvvJCf/OQn1NbWctJJJ7F9+/a0Nr09hKSUaQ/vfWkLEnn33XcJBoNp+7Kzs/s81p7OMXDgQLZs2cLrr7/Om2++yZlnnsm3vvWttHmXno7vCw6Ho1NfUkrC4TAATz75JKNHj047pqMLrS/069ePoUOHpm0LhUKcf/75/PKXv+zUXlEUFi1axOWXX84dd9zBzJkz8fv9fP/730fX9R7PpShKp++2q2O8Xm/aWGD/vsuCggI2btyYtq2mpuYLC56x+fyxRczmkPKtb32Lu+++mx//+Meoqtpp/4gRI/j444/Ttn300Uccd9xxAAwfPpxFixal7V+2bBkulwuAkSNH4nQ6qaqqYvLkyQc8zi1bttDY2Jh6eC5btozhw4en9vt8Ps4991zOPfdcTjzxRL797W+nidjSpUtTVkVZWRkNDQ1pxx8o+fn5FBYWsnPnTs4+++zP3N++jB8/nrfffruTuLWxZMkSRo0axQ9/+EMgGUVYVlaWNifocDgwTTPtuLy8PPbs2ZP6XFNTk/a5Kw7ku5w6dSp33XUXoVAoZU2+8847TJs2rU/H23z5sUXM5pAybtw4amtruw3n/sEPfsBxxx3Hfffdx0knncTjjz/OihUrePrppwH43ve+x913381tt93GN77xDf71r3+xdu1aJk2aBEBGRgbXXHMNV111FYlEgokTJ7Jnzx5efvll5s2b18l66Q5VVfnud7/Lr3/9azZu3Mif/vQn7rnnHgAeffRRpJRMmzYNVVV58cUXOwnUXXfdxZAhQ8jLy+OHP/whxx13XJ9diT0hhODnP/85v/jFL/D7/Rx33HE0NDTw9ttvM3XqVI4//vjP1P/VV1/Ngw8+yOWXX84111yD2+1m1apVbN68mZtvvpkhQ4awadMmXnnlFYYNG8af/vSnTmI0YMAA3n//fU4//XQ8Hg+ZmZkcd9xx3HzzzVx00UXk5uZy8803p148uuNAvstTTjmF4uJiLrvsMn71q1+xZMkSnnrqqU4Rrjb/xXyRE3I2/510Nanfxr6BHVJK+eijj8ohQ4ZIh8Mhx4wZI1977bW0Y5566ilZWloqvV6vvPDCC+W1116bCuyQUkrTNOVtt90mBw4cKB0OhywpKZEXXXSRrKqqklL2LbBj+vTp8je/+Y3MycmRmZmZ8vrrr08FcrzwwgtyypQp0u/3y8zMTHnyySfLjRs3SinbAxSeeuopOXbsWOl0OuXMmTNleXl5qv++BHZs2bIltX/hwoUSkLqup7Y9+OCDcsSIEdLhcMjCwkJ5zjnndBrDwoULu71GQL711ltd7lu9erU8+eSTpc/nk4FAQE6ZMkU++uijUkopLcuS3//+92UwGJTZ2dny+uuvlxdeeKH81re+lTbe4cOHS03TUt9LLBaTl112mczMzJSlpaXyqaee6vW6pez9u+yKjRs3ypkzZ0qXyyUHDBggH3rooW7b2vz3IaS0Z0ZtbA6U8vJyBg0axJYtW7p1yR1q3nvvPebOncu2bdvSglNsbL4K2OvEbGy+5MyfP5+f//zntoDZfCWx58RsbL7k3HbbbV/0EGxsvjBsd6KNjY2NzZcW251oY2NjY/OlxRYxGxsbG5svLbaI2djY2Nh8aTmsAjssy6KysjJVONHGxsbG5quJlJKWlhb69euHonRvbx1WIlZZWXnQEova2NjY2Hz52bVrV4/Jrg8rEWtLaLpr1y4yMjK+4NHY2NjY2HxRNDc3U1pa2mui68NKxNpciBkZGbaI2djY2Nj0OrVkB3bY2NjY2HxpOawsMRsbG5vDlbZioTYHF4fD0WWZpr5ii5iNjY1NL4RCIXbv3m1Xkj4ECCEoKSnptlxTb9giZmNjY9MDpmmye/duvF4veXl59vKfg4iUkpqaGnbv3s2wYcMOyCKzRczGxsamB3RdR0pJXl4eHo/nix7Ofx15eXmUl5ej6/oBiZgd2GFjY2PTB2wL7NDwWe+rbYnZ2NjYfAVom89LTetJmfq/lBIpJYZhtv7oGGYCy9TJys7F6XR+MYPuA7aI2djY2BwCTEuydHs91S0x8gNupg7KRlUO3OqQUmIaFrpuEE/EiURC/OEPd/PiCy+iqApOh4PikmKuu/ZHjB4zqvWg5D8i9X+Z2i7oEKTSuk9IQEoEIKSChkJLQzU5BV1nzFiyZAlXXHEFkUiE0tJSHn/8cYqKig74Gg8EW8RsbGxsDjJvrK3i1pfXU9UUS20rynTzqzNHccqYnh/yUkoM3SQciZCIR7CkDtIEC4RpIaSFkPDDH11HOBLhzX+/QFYwC4HgjQVvU75hO9OGT6JVuhBCYJomqqq1bUmdSyQbpDbJ1v+bikRXTHRFosWj3Y5z3rx5PPTQQ8yaNYs777yTa6+9lqeeeuqA79uBYIuYjY2NzUHkjbVVXPX4cvYNxt/TFOOqx5fzl4smpgmZlJJoJE4o1IhlJcCyEKaFYlmoUkUTGqpwoQoNRahIIdhcvpXX5r/FJyuW48oJEhJgCZj2tVOxBNz/5DO8+vwLZOfmsXXTJn7x+9/S0tzM3b++DdMwyMgK8su772TIiJEsXfQBd9z8C5597z0sBJvXb+D7532D19dsoGLHDi6aOZ3LLruMRYsWEQqFuPfeezn++OP55JNPcLlczJo1C4ArrriC/Px8dF3H4XB8bvfbFjEbGxubg4RpSW59eX0nAYOkx04At768nuOPyKeluRHdCINpoRgGqqXiEA404UZRNCwHRDWThGaRUAS6omAKBROVt8o2UjJkMImiUmpb+xdYKK0/plD45OOlPL9oIQMHD6auppozph3Loy8/z/DRo3j52X/xk29fyisfvYcqEyhYOK0wYOG2mlCkiduowmVW0tDQwNixY7nzzjv5+OOPmTt3LmVlZezcuZMBAwakri8QCBAIBKiqqqJ///6H/ma3YouYjY2NzUFi6fb6NBfivkigqinGW8s3MK3Ag0u40JSkaEXcJmHNIq4KEoqKQbs1o2KgSR1NxnFIA5fVjIKJy9iFZVns2L6DH1/6Y+KxGJOOnsKRUycxcepE+pdkYSUaWPXx+4wYPYIjhpUgE82cMfdkfn3dDezduQ30KFJakIigSNASBkKCPyrIiHhwOp1cfPHFABx11FEUFhayatUqoHNk4RexGNwWMRsbG5uDRHVL9wLWkXDYgeoOEnIaRDWFuHBg4QYkThI4ZBSn1YQl45iWgZQKEhWkQJEwfvgIdpSVIyoS5GbkkZOXx3uvfMjT/36KtxfMJxAPkOXOIjeSD4A/lolTOsmO5KXGIKQgK5ZDo9mE0AWZ0VwEEqXFjZAKDiMHYYW7HL8Qgv79+1NeXp7a1tLSQktLy+ce2GGvE7OxsbE5SOQH3H1qJ/Nd7PR5qHf4MYWK22rBbVShJXYi47UQD+OM62REHOSEcsgNF5ATyScYzSUjlsPokkmccuLpXPvTn9PUlEAIJ0JoRKOxtmiNjmdjysTJrFu/hq1b16Ng8p+XnqVfUT+K8nMZVFrCzt07aG6sRhMGz73wFAjwKDoexSCRSPDEE08AsHTpUvbs2cO4ceOYNGkSsViMd999F4AHH3yQuXPnfq7zYWBbYjY2NjYHjUn9g+T7HVSHuk8UnJvhYmyJD4dZg25FkZYKUsWvK2h6LgIniGQIvEQkowdJflYwUYSFAvzt7j9y571/4LRzjkdVVIKZQfJyc/nxVT9g85bNKFi4iQGSfjkBHrz7Xq764eWYpkVmRgYP//kBBAn6FeZw9eVXMueMOZSWlHL0tGmARNcsLI8gJyeHrVu3Mm3aNEKhEE8++SQ+nw+Axx9/nCuvvJJoNEpxcTGPP/74ob/J+yDkYZTRsrm5mczMTJqamux6YjY2NocFsViM7du3M2jQINzuri0t07Sord2L1GMs2tTMz97aDdBlgMdNZxVw7LAsvCa4436EdHcwnJL/UTHQhIUiBAIBUmJKAxMr2acCQlPRnBoOpwPV4UDVVISiIIRy0LKLlJeXM3nyZGpra3tvfIB0d3/7qge2JWZjY2NzgEgpqa2pxkxEcOjgUv3MHOvjFwEPf35rJ3Ut8VTb3IDGD2cWcFJxMSLiRECrpSVxCANNgEAgLRMdE0NIVKeG2+fF6fWgKPbsT1fYImZjY2NzADQ2NBGLNuCIW3jUABGfpMKlEBMepo1wccxQFyvK62kOQYnDw8TCwtaMHQKBhVMYqELBskx0aaA7VHzBAE63+7DJ0zhw4MBDaoUdDGwRs7GxsdkPEnGd+vpK1LiBV/hIeFQqPEnx0kjgNatJGAlcpsbxOf0RORqtNhZOoaMKBdPSMYSFM+jH5886bETry4gtYjY2NjZ9QEpJTfVerHgYl+lEcfqp8VqEFC8aBl6zhoQRx2U4yYjntwqTQBM6TkHS4hIm7uxMfF5buA4WtojZ2NjY9IKuG9TWVOI1BW41g8aASaPDiUTgtRpI6GFchqtdvITAJRKoCOIygfS7CWRl28J1CDgkM4XxeJxrrrmGYcOGMXr0aC666KJDcRobGxubQ4qUkn8/8ySxlkbcugPpCbA7qFLvCOCSEbREBUo8QW44H3ciaV25hY5HJDDQceb5yBlQRCDbtrwOFYfEErvhhhtQFIXNmzcjhKCqqupQnMbGxsbmkLGnso4n/nkTJXs1HMPG0uRXCXuSrkOPUYU0ICea3bquK2l5KUBCMQgW5hJQBOxYDKG94C+AAceAsv+Vi2165qCLWDgc5h//+Ae7d+9OvXl83mlIbGxsbD4Lzz3zb3at+A9Da0uITxxNg1vBaalkRutIyBg+w4vDDAACp0igAXGhEyzKI6BpsP4leON6aK5s7zSjH5zyOxh11kEZo67r3H777Tz11FOoqorT6WTAgAHccsstTJgw4aCcozfOPfdcFi9eTFVVFS0tLfj9/s/lvB056O7EsrIycnJy+L//+z8mT57MjBkzWLBgQZdt4/E4zc3NaT82NjY2nzfSksTKGgl9uofHb/89TW89x8ToTBbMKeLfARVPJExGSyPuUJyMsECNxxEyilfomFLHVRAgp7QItU3Anr0kXcAAmquS29e/dFDGfOmll7JixQo++ugj1q1bx4oVK/jOd77DunXrOrU1TfOgnHNfrrzySlauXHlI+u4rB13EdF1n27ZtjBo1ik8++YT77ruP888/n5qamk5tf/Ob35CZmZn6KS0tPdjDsbGxsemR6Npa9vxuKbV/W0Pjv7Ywq/loZuV9j1em5LAiksOsxW+iWPvm3rAwzRYsryRnQBEOl6t1s5m0wLotxgK8cUOy3Wdgy5YtvPDCCzz88MNkZWWltp955pnMmzePRx55hFNOOYVLLrmEyZMns3TpUt544w0mTpzIuHHjmDlzJuvXrwfg3XffZfLkyak+1q5dy8CBA4Fkxo7c3Fyuu+46pk2bxujRo3nnnXdSbU844QTy8/M/07V8Vg66iA0YMABFUZg3bx4A48ePZ9CgQV2+Hdx44400NTWlfnbt2nWwh2NjY2PTLdG1tdQ9vgGjKZ62XZMqV6/J4sz3/t3j8fFwS3r5kR2LO1tgaUhorki2+wysWLGCoUOHkp2d3W2bDz74gF/84hd88sknDBkyhIsuuohHH32U1atX873vfY9vfvObfTpXXV0dY8eOZcmSJfz973/nwgsvJBzuOrv9F8FBF7Hc3FzmzJnDm2++CcCOHTvYvn07w4cP79TW5XKRkZGR9mNjY2PzeSAtScPLZa1pdtMjBxUENbHdqHGdnmIKTcNAj0XbN4T29u3kfW3XAx2jHcvKypgwYQLDhw/n8ssvB+DYY49l2LBhACxZsoQJEyYwduxYAObNm8fu3bv7FHTXUz2xw4FDEmL/wAMP8Pvf/56xY8dy9tln89e//tUO7rCxsTmsaNlUh9WU6CRgbcTNvlkbafNN/oK+nbyv7brhyCOPZMuWLTQ0NAAwZMgQVq5cyY033pja1jHIQkrZZYi/EAJN09KuIRbrvSba4bRc4JCI2ODBg3n33XdZs2YNK1eu5JxzzjkUp7GxsbE5IHZur+TNp//SYxuP2rdIO1XtEDY/4BhkRr9kYt8uEZBRnAy3/wwMGzaMs88+m+985zs0Njamtnfn5jv66KNZuXIlGzZsAODpp5+mpKSEwsJCBg0axPbt26mrqwPgscceSzu2u3pihwt2xg4bG5uvFO8t+IAVbz3ApJZjINC21cSlrEOhAYss4tZoct0leNRAT12hahoOtyf1ORaJEp/0EzIW/gQJ+0hZ66dTfntQ1os98sgj3HbbbUybNg1VVcnKyiI/P58bbrghJVZt5OXl8dhjjzFv3jxM0yQYDPLss88CUFxczHXXXcfkyZMp7T+Ao6cfi5Sk5vp6qid21llnsXz5cgCGDx/OsGHDUkUyPy/semI2NjZfGf7x0F9ILH+P4dpsHppt8eOVY+mvf0CW429ooj1buyFzadS/xzazkE+bFzLxgkvpV1iAQ00Xn2BBEe5Wt10sFKJxb3KOybXzHTKW3YkaqU61lYF+iFMP3jqxg0lTNEFlYwzdtFLbHKqC3rSH4489xq4nZmNjY/NFYlmSe+68ntyNuxnU72R+PcPNRnUoJzjvxdu8mGhCp8QHSquxpFJLjuN2AjPvJyN4Bc26kdafqmkEcvJSAialpKWufRlRvP/x1JTMxFm9AiVai+XJxew3hdyBQ3oMFOkNKSV63MQyJYoqcLjUzzw/1RRNsKMu0mm7blpUNMS6XCxwOGGLmI2NzX818WiC++65htLNAmPE8fxsciG52yu5+oNfUxeVvMYIAPxanOMLyhiWUYcQyYKVrnW3MfDKZWwr30FmfgEOTUNVVRxuT5p46LEoppEudCgqicL29VdYEj0WxenxHtB1xCI6ofoYltlBVgTgSIBqgrRaXZiitbqzgqo6cDidOB0ONE1BUdPDIKSUVDZ2H8hRXNqfD9Zs6zYw5HDAFjEbG5v/Wmr2NvDY337C0O15bDyqlAeHjWD0thXMfOvNThZRyHDyUsVIzmJDUsja1nRVrkCIHJxuT5q7qyN9zYjR13aWZRGNJohGWjDMOMJUEEYXgSYSSDhRrBYEidRG2fqvISS6gIgQSCGwFAWhCBAKinCAw5/mQuwK3bQIx0387sNTLg7PUdnY2Nh8RjZt2MbrT9zCqJrhvDknwDOFRzM0sZYp77/BviEXSQQgWbh3MEMCdSnXIpE6cOT0eC5V7VugRnftTNOipamFeCKElAaYFoppolgCBw5MpecAE0v1gysBQiKkQJEgpECTCqolEFIipcQyTCxMTGkgZYSwokMvwSsAhtWz0H2R2CJmY2PzX8eidxez6tX7GRudyqOnaMwPHsOR0bfRVi/GF+/JLSZoMdxURDIp9TUlN3lzQO/5fA63B1XTOrsUO/asqiSEA0drLF08ptPcXI9lxcGwUE0TTTrQFCeacICqEneZxIVEjfXiypOCOoeXhNZVO4mKiYqJJiWaBQ5LwWW48egajT33DICmHJLVWAcFW8RsbGz+q3jumWepXfQCoxzHc8+pKku9E5nS8gyU13Di5lnozO+1j4bEaEp9HyUzz/c7Enbs7LG9EIJATl4qOrErGhUfe2vDqEj8SgTVSgZTOKQLnwxgORTCToNmDeKKQkI4kLhwJyyy6N0Scun1CKK0zoohhECgglABFSk0DKERV52YqgYOwCNRWnR6ilFXsfC5Dt8SMraI2djY/Nfw1wf+gPPTpQzMO4Ffz/Sx1TGESQ0PkrvVw5Rt56GpW3ozqgCwOAMplyD2Y02X2+8nSBEtdTWYhoEpTdaH11FnNOJ1FdHfOQ0FsJQYLY4WhGibH0tQSwRTy0YqfhRMnDKOx2pCyjjCAsjtdD5d1/njn+/ihZf+nQw2caoUF/fjJ9f+kNFjRiGFxBQmhmJiCoElZGuKLROHsFAVB4riwnB50GPdS0GmjHUZ1FFZWcmll15KeXk5LpeLESNG8MADD/SYz/FQYIuYjY3Nlx4pJffc9XPy1+/CO+gEbjo6nzolm9G19zBiw2AGV80kW6smIEx2+DKIhrsv++RRA2S7xxCf+TjuUadBH9IwteH2+3H5fLy+5XnuXPEnamL1qX157my+PfJKJvXrKtuFiWrU4LRaSAgdCweaBS4THLqni/bww5/+D+FwmFdfeItgZjKT/Ztvv86WjZWMH3Fs66xf0sQyTQtF1VrvlQnomFqYuCOBpSRQHA4swweyg2ALgfSoGEbXZpqqqvziF7/g2GOPBeCnP/0pN9xwA3/961/7fL8OBoevo9PGxsamD+i6wR9+fw391tVgjJ7BTccMIKJ46L/nDo7+9EgGV82ixFGJYuxm5u0XcdzJF/fY38ScOShCwcqe3GO7fZFSEorpPLv2ea7/6JY0AQOoidVzx4rbWVq1tNs+DKGTGy4gJ5JHRiwHl56LoHNU4rbtZbz25ivcc8d9BDOzcAoDr6JzzkkncMnXvsbzzz3GBd86hx9cewUnnTmLVauWsvDdNznh9BnMPnUG55z3Nco2VuOP5bB+wQYuOvFsCnSLDFrYsfUTTj16DKqzitqdy5g4YmyXpVgKCgpSAgYwbdo0tm3btl/37GBgW2I2NjZfWsItMR689/sM2upl+9RR3Dt8DNlWHY5df+X0lWfjiw1hqGsH1cYezv/TT3A4nQydfDShDypYXreAqNmS6sujBpiYM4cSX7LihhJw9nkcjeEEFY0RdMvk/nV/7rHto+sfZXLhZBTR2YawhImuGbhMgSYMVEi2k2BISbKGtGDNulUMGjCYrGAWqpYATWIIkVzfZklM1eLjZUt45423GDJgEDU1tVx44kxeeep5xo4cyzMv/psrrv42i99ahNI63yaEC6/pJDPhQ5UqueE8wtFaGhoaGDt2LHfeeScff/wxc+fOpaysLJV6CpJLB/785z8zd+7cPt+zg4UtYjY2Nl9KKnZW8+w/fsYRu/vz8cxcHimdyiBjM/Gdz3D+iktwmLmMcZezU6vjontuSM3ruAZlMqDfWPp5h1Eb203UDOFR/eS6S1LComa6cA3K7HUMlmWxu7qORsMBwMbG1dTHOxcA7khdrI4N9RsYnTO66z6FiQpY0kA6FJxeBx6/D6U1PF+Pm3gCTjSnQl7/DIQQlJWV8fWvf51oNMpxxx3H9OnTOXbGDKbOSCYaXrzqE46cOJGjT56FZZrM+85FXPuLn1GxdzcKBgoSr2IgkajCBCRCOFDMzG5LsRxzTLJvKSX/8z//QzAY5Pvf/36v9+xgY4uYjY3Nl45Vy9fx7r9+w6iGcbx4oocX8mYwNr6YcPm7XLzyclTcTHDvoKogwcU3/jTtWKEIgmcOoe7xDeR7+nfZf/DMwclFwd1gWRZ1tTWYepQWKyO17KwxXten8TfGGrvdp0gVb24Qt8/R5X6nW2Pa0VPYunUrjY2NZGVlpUqxPPLII7zyyitA96VYFFXF7fMhhCCntJCGeAipChw5HsJNLURjIUDiFQYepeslAx0DPX7wgx+wa9cuXnzxRZQvIBTfnhOzsbH5UvHma2/xwWO3MSZ0DP84LZMX8mYwKfwKbF3KvJWX4URlgnsX4fF+vn7jlanjLMtk17rVbPjwPWpFJVkXDkfNTHcZqpkuci4aiWdM52hASIpBbU0N1VU7EeEwqunDEgpeBBkICl09L4puI+gOdrldkSoO04Wi9rwu7FCUYglFI2QX5fPy26+jaCquAj+mU/ZYiuUHP/gBW7du5YUXXsDp7Lv79WBiW2I2NjZfGp549B9EP57PcP8c7jjJySrXaKY0/xNfWYzzyseT5VlIELBOOZljTj4+ddyWJYt555G/Eqpvz8buz87l+G9dTmneWKyWBErAiWtQZrcWWEtziEZjD6oOTsWN6VaIKRqDdZU2m6kgeCT3uvKpjld32QdArjuXsZnjsehs5fjjWaiqgqMP67K6K8Vy/fXXs3LNOnTTIhQz8LnUPpdiGThwIMcddxwADpeTjNzsbkuxfPjhh9x7772MGDGCadOmATBo0CBeeOGFXsd+MLFLsdjY2HwpuP9Pt+FftY7c4hP4vxmZ7FZLGFP/IGdvbGFe/FP8agdXXkY/OCVZ9mTLksW8dPft3fZ71rU/Z9i0rotUSkuy8d11hD0higsL0JxeBKJ1vZUgQ3o61Q1bUP0uP1vz827Pd8vo25iRNwtTmIScDSS0KKoAXzQHl+klI8+D29u1K7E3uiup0i/oJtOz/5ZSeXk5kydPPqxLsdjuRBsbm8Ma0zD53W9+hrqpAnPwMdx4XCHVah5HVN/Jhat0vqu/RY3iYQ3D2U4JFgKaq+DZS7DWvcg7j/S8bmnho3/FstIT81qWZP2LW3j3pwuILapHk048IkCm9JIhPWRKLwGZXL+1r902J38Wvx97O/mu/LTtea78lIABqFIlM55LnnSTZXhx4f7MArajLtIpoa9uWuyoi9AUTXRz5Jcb251oY2PzhWFZkqotjYSb4/gyXBQNC6J0cOetXrWGF154FomXaJ4X9FpOXtrMlqyNTCk7llGeF/gj36FZtCexzaCFU3iXUZRhvfJTwvVH0HXC3yQtdbVsevjv9B87Ae/kSZStquPNv68GUwEU3JrFOEtgyi7rNHfJnPxZzMqbwZuNK4jF6sh25DI2OB5VdOEmjOZgqhGC2c4DFrDeSqoAVDbGyHA79qukysCBAw+pFXYwsEXMxsbmC6FsRTWLntlCuDGe2uYLuphx3jCGHJnPkiWf8PrrL4NU0hTDk4gzfs9gMtUoL6kzO/XbjJ9n5RkcEVuD1RDDm9GfLMNP3ApRG9udKlTSkcqH/gaNIaoHzWDNgPMQ+zipJBCzJG4Jjj5qgCpUZmcNJ252UUIlrXMFIZU+Z8LvinDc7FNJlb2RBH6Xhk9VDtv6YPuLLWI2NjafO2UrqnnjwbWdtocb47zx4FrGn5HPgmUvAlonk0e0bqh1R1Nb9m0Bki3acHx71iDYhdW6kPmovDNYXreAisjmtCNcuoFEsLng5B7HHbEkmlPBUECzwGvKHi0yp9SJ97C/jUzCOC034OpD6870tVTK3pjOXtPEoQj6uRwEHV9+CbDnxGxsbD5XLEuy6JktPbZZOn8VUnQWsBQCpBB020AIpMOF6U26GaNmCx9Wv0hdrIrp+XMp9h6RbCcl7oROdjhGY3AoCXdWSiS7QgJ7XIJKj8JOn8LWgEJzl+VPwJIWluzbI1YjgWjYDtHGPrXvdHxf12e1NtMtyY5ogka9+9IxXxZsEbOxsflcqdrSmOZC7ArZx8zxvSG19DmmFfULsKTFkTlzUlI1qrIOAcScfYuIVqx2d6QhBBXeroVMEQqaEuzRUkti4hCt81lNu+mxLko3+FwqDrWXx7kiYJ82lXGdwyhA/YCwRczGxuZzJdzcu4MtKA7MrbYvwkgvvBI1W6iL78SnZdDfV8CZJRsYFqhlyRGCv5wW6lOfVhfryPZ6RKeZtoRhUrv8E2IL30Rf9SnSNDsdB9CidDjS0iERQkqJYYTQ9UYMI9Sr0Agh6Bd099hGuju/GOiWJNzLXNrhzpffIWpjY/OFY1qSpdvrqW6JkR9wM3VQNmo3i4Z9Gd0LVJFDMNaj4lJyeEa6CBPvwWMoun+4S4kwEqiRlk67XMofcCvnc0q/anxqHW/O9HJXqR8hy5hc2YAvEezWpWgqdFk92RCCesJkSQ+KUGh6fyH199+FVdu+6FnJzcd71bW4jp2d7IukgMWEQhg3fpLWmG60ENMrkFa7AAvFgdtVhMPRns9R13Vuv/12nnrqKVRVxel00q+klO/84GcMHTmmfXCKSAqY1rXNoh+gJRYOhzn++OOJtZaqKSoq4oEHHmDgwIEH1N+BYouYjY3NZ+KNtVXc+vJ6qpraQ7yLMt386sxRnDKmqFP7omFBfEEn4cb0dUtFDsEUb9JaEAiO0o9ggWMN+64mlkhq3bX0c7tJNPiTuzpG2rU+lF17d3UpRQG1jhzH7ehWfywBd/YLJg9T4MOBz3PS5stSi5n3pdnbvZvTUECxFEKLFlL76+s77bdqqwn97w3wi9/iOHY2taqVst4MVKSEmOZCNzvnX5SWTjS6E+ifErJLL72UUCjERx99RFZWsp7Yyy+/TNPecgbPOIqQYbBXN0BVMM1kUuGucBxglKLH4+Htt98mEEjOO95zzz1ce+21PP/88wfU34FiuxNtbGwOmDfWVnHV48vTBAxgT1OMqx5fzhtrqzodU1s7n9zxj5JUp3ZX1lhPq4C1PlQHWfnM0cfi6xCxV+Gt4I3iV3m/6H2ezprPxwUfE1XTzy2MBO6KMhwtjfucWRLQYpT4mgBwKDtZ7naxV9NSIrg9ZzXzj3iYsDP9WEtImr0KsR7i6zULpGlS++e7um0DEHngDwjTxNFh3ZmQTuqMASR68aLG4lVIKdmyZQsvvPACDz/8cErAAM4880wuuugi/v3043z73Ln88uoruHDmsaz99BM+fHs+5884hm8cM5XvnHYyZRs34FAEyxa9z+TJ7bXT1q5dm7KmysvLyc3N7bKemKIoKQGTUtLc3PyFJAC2LTEbG5sDwrQkt768votVV+3G060vr+fEUYUp1+KevW+wbt3VuPOh+JgG9q44HyOaTY4m8HThfhxk5TMgnkdF/VIWmS/z8Yz0QpOVvkoqPZXkxnKZVJZPXp2CGmnpwoZKjnJ2wTbaTmMhKKOEklABMTVGrbsWRFLIyrPXUNQ8BK+egS/DwUD393A7unYyOkwD1TRQzQTRNRsxa7vPmwhg1ezFWLsSz5ETcSPQpEXcykbRYgil63mz1FVYOqYZZsWKFQwdOpTs7Oxu237wwQe8t3QZWskA6muq+drUyfztldcYNnoMrz77ND/79sV8smo11b1YYnV1dT3WEzvhhBNYs2YNeXl5zJ8/v8e+DgW2iNnY2BwQS7fXd7LAOiKBqqYYH2+vg2wXb5VV4657jImagiosAiUr8PdbSaR2GJnVk6HypC77EVKyeucanvxa67n2feYqUOupZdHQBs7dVYxAoAkTQ7Y70AJanNkF2xiWkXTVrWcobzCL5liAaa3dRtQIq3JWUemrRApJZeZWAIqcRfiMzg96p6Hjj0dQW92XhkMj1rC7D3cOrPpa8lCSD2ChYKkQ60XA2pAyGRbfcbFyl/XEjj2WSaNG0qgbfPDpJwwfO5Zho5NzZXPPv4DfXXct0dqea58BvdYTe/vtt7Esi9tuu43/+7//4/777+/TdRwsbBGzsbHpM5ZlUrFhHaHGBsprTYS0kF1UKO7I9z4toy6v1U/m/CnZspZLeJgpLEEoEl/+Zhya0q2IvVG7mNVD6jBdke5PIiDiMdmbHaeo3s2YzEyOyFhMyHDi1xIUe5tSFth6hvIsZ3TqwmN6OKr6KD7O/5hKX2Vyo4QsIwOfqZEbtah3JCsoOw2djFgXZU+yszpv6wIlO5eO8SEK4EajL9kNhdA48sgj2bJlCw0NDb3WEws6NEpcDryaSn+PE4cQyYwdJIVQ0zTMDpGTbYEaPY8hXdQVReHyyy9n2LBhn7uIHVIH5q233ooQgrVrO6/Mt7Gx+XKxZcli/nb1d3j21z/ntT/dQeWTd/OtXY8zJLytx+Ma9okoqCebe/gpy5iW2hbN2oTuqkeSHu69M7yJptCHSEfXdbL2JeoyKfYewRHBKyjyaIzIqKHU15TmQnyDWa3exX1zISY/j68bDzJpAQKcV5fM4uEzYWiLRf+wRUYsgqBztg513DhEXte1yNpQ8grIHHdkp+3CdCOsntfHCcWBqvoYOnQoZ511Npdddhm1NXWYhoVpWIRC6csEpJRYMYNpE6awZtUqqrZsxq+pPPPMM53qidXVJa3Uxx57LK2P7uqJ7d27l/r6dvfu008/zbhx4wjFDBojCUIx43NZg3bILLHly5fz8ccf079/15VTbWxsvjx0V87Eb4Y5tfpNXs8/mTLf4E77pVvFytqnBIhQQFr8k8uYxDIULBCS6hFP0G/VNanIQEtaLKt7G4FgijINpWkv9VoT67xbsUTXD0dvXGVizhxApV6/ggT/IWJm4VMbKHJuYIfoRzOB7sP2EXhNL7mxXFRHNZfv+S4T5HBapIluxVEt0FAQyr4FWFqPV1U8V19D5JZbur2X2Vddi1PrLFYC0GI56N7u59RiEY1Q0w6EhN/936/4471/5qijpqAoKsHMDHJyc7nm6ivZurWMRDRGvKIZBYUs6eDh3/+Red88H0tAMDur13pibXRXT2zjxo1cfvnlGEZSrPoPHMQtd/+FbbXtQvpZysD0lUNSTywejzNr1iyefPJJZs+ezSuvvMKYMWO6bBePty98bG5uprS01K4nZmNzGGFZJn+7+jtpBSU7IoGQ6ufR0nlprkUJ6BOysQo8nY5RpOTIBpPvxp6lwLWOaNYmJBJ3xTHkrr8EP26qozvZ3PwpE3Pm4NUyWvu0qMr9lPl58/nUtZOyuJK0hyT4YipXfnQUJxTNozJhsSZqEuvwdHOJZnIzFrDS03v15QnafI6OnMH2lnzWyxWM/uY36VdYgENVEUJgOpygdm8D6O8vInrffcgOGeDV3Dxyr74O/4zZ3R4ngQYliuUMoyk6bi35fJSWQEa8CN2DgooQCgKR7taTydTGSWtW4FRcyEQEK1oPssN8m1BRc/NxFvR+H/paT6ytDEx3DMjxditkn7We2CGxxH75y19y0UUXMWjQoB7b/eY3v+HWW289FEOwsbE5SFRsWNetgEHSggiYIfrFqqjwFAMQ9DupHupPEzBhWZRUlTO1qpG5exxk6RC3huBWxzAgs4JIv9dYE2nkHdciCqxsCuMK0/PnYmKx2ruZiuwVULKYgYEmjhZwNNBoCD7Z05+WpgGM3JmBis7KxnVs0z0oWjGig6jGZQbbo7PBs7rXa5bGNNa3ZLG8+kW82enuQSklSiKO5aRbIXMcNwPH5ImYa9Yg6+sR2dkEx41E82dhWTEUs3N2jRYke5EYlhNiyQe+iiSo6qiaRHeB7hGYioKFwBJJAZcIHIaBIiVSgFQEpWFnUsAiXQRuSBOzpoq6WCMikE0wmF7+Zn85VGVg+spBF7GPPvqIZcuW8dvf/rbXtjfeeCPXXntt6nObJWZjY/P5YUrJx40hqhMG+U6No4J+1A4Pm1BjQ5/6uX5mP+TgCeQH3Lz68XweKshL7Ru2bR3Hf/gqGeFmAPaVEX9tnONDFXwtYzWz5XJeih6LLzGDd1zv8MiQBdS2rdtqgmDIzdwMnSHNLmRYY5q3hoy9U9lYuYNqs4Xq2M5kW+HH4Z2N4hyK7mzCUhIIy4FiOrGURNcuRQke6WSVzMBXP7/HoAFFT2ApavpCa2hdbC1RBCjjxqK6TJx+A6kY6CRFRUgVLZqDaniBpIBV0Dn9k4mgznQiXRpoAhUTFQNFWqhSRzNMnAlJx8xVEjCkghap79RfR7yhOGGrjupoE0J1kpNbgLaPm7Mv9cT6WgYmHDfxuw++3XTQe3zvvffYuHFjygrbvXs3J598Mg899BCnnnpqWluXy4XLdXBypNnY2Ow/r9Y0cvOWCqri7SmOilwO/m9YMafnBQHwB5MRdwJBrrsEj+onanauzXXk8P70G17IPXdfz9BNVWSfO4R6l4Nh29dz9vynehxHyHDyUsVIJkd3s7E5n5ARppY3oBZmV/lYMirOzsJk6ZWMCh/ht7Ipi3V8fK2hkyrJEFHrHWI5e5CO9nEKs/W4fae1WpscYwxndXwVsX3yLnZCSrAs6FgHrHV2RujJOEPVZeLK6NyPFGZy7iuSj2J42dvlart2lGgcxVUNUkGiYiFwWCousy3BsWgNRlERCKJmlIDsJWRfWjgVHy5TJZGIULtnB0JzkZNTiOboewLmvpaB6Wu7/eWgi9gNN9zADTfckPo8cODAbufEbGxsvjherWnku2vLOz0+98R1vru2nIfGDOT0vCDFI0czNH8SI11TU3NTABGjOVWbK5CTizOrmHt+fxX9tygYI2aDoxlhBjnlvVf6kMk9WQPsk/qSTnu8MZXZy/NYODFpxcxentepTVuc4L7n0aJhvOUriRf0x8hIirFUWtdZSQ0p2kuR+HAxTR+GCwchLQbeQJe5FzuiYGGlJXSSCD2BaA1Zd/p7LnViuOswQx6MXkRMSpXMcD4OqSCEigRMpZlkBkaBEE4U090hVZabkM+LO16PZkS77bfWqWB4FTLjXvwxhXg0RN3eHQinl7zcAkQf3Ix9LQPT53Ix+4m9TszG5iuIKSU3b6noMdvGL7ZUcEpuJon1DUz0ndCpRIhHDTA9fy4fVr9I4ehpLLr3PiY1jmLJ0RoPDh1BXtkeitaX4Y73LATtdP3AFAgkkqnrslOeu31zZ4jWcXcVMygMHXdFGTGGJIWstbGUgl0iTtQZ5Yh4CZP1ASxxbCEs4oAXBgxH6AmcPYxfmD40CW6lGd000DssilYdVmsUY/dIxcRQ68DsfX2ZJbTkvZAxpNWCYnS0bBKgChDtc21SUYl68vBEa7oVMk2JEhVB9riduFwxgm1iFm5mr7EDjzebzGDPQXZtZWB6cik6VAWf6+CU19mXQy5i5eXlh/oUNjY2+8nHjaE0F+K+SKAyluDhlbuY89JuHHRe4NqWRf6YgrkouyxGK7UoWQ0UVWSzMl7F9iqdfNGE0WrRfJYpfYHAH+/5cdVD/Uwk4Nq7EyMQTM5hCUDVKZUuiLuAEO861nXqQ2oOEsH+yJ6sCOkkZuYiZAN0XK7ci4C1oYm+1H4GrzDRrDgxs6uSMRbSbAJVIPYpYxNzZeHvQsQsoWBIE+LVeDQHhhpkr9tPoytGTiyAN2YSb6qhzoiQk1vY7bjaysD0FJ3YL+g+JEEdYFtiNjZfSaoTPbu5lL1RHOsbeC2xl1PxddtOCIFHWUzQ8Vc0kQwA2MtQJtYez3CnD5wQzUhaNK69O7tIyptcgFzpLiKievGaEfrFqlB6ca/tL4KkRaZGWjB9GSmjct/nqiWgKjOXiNOFNxGnqCl5TVLtzoqwOhybCbRHA1oG7ClLEG228GQo5A9ydBkF6COGAwO9h8exA0Eg001zQ1OP1ynNFoSWLmJS0TBVF6qZLpZxVzb+uAeflCRcjTRrtXg0DV3NptLjwu+KkBcOYkZ7X2ie6XEyICcZhdjRIvs81onZImZj8xUk39n9n76yN4pjZTKyLacX+8mtLCbH0b4IuruUTlJzECseAvtkl9/qHcSinGMJaf7UNr8RYkbdBwykFqk5UuJzIO/xlhDsLhpI2BvAF2kh2zAwpEBFdhKw7XlFfDhkHGF3+7IAXyzKGRWbAZGqNi2EglNxETejdEx6JFAwHV4wYlRtCrHynQZiLe0PdG+mwpSz/PQf2y4yhoSEhH6ijh2yoNvryEcgLRPTaH/50HWdP/3lAV545RVURcXpcFBc3I+f/vgmxo6etM99UFMzd1JRMJx+nC4nitRJ4MSVyCIvIYk7GtEde/E6vISVIJGAJDsSpjdn52WXXcY//vEPmpubEQ4/hmWhKUkX4qGywNqwRczG5ivIUUE/RS4He+I6WJL+tQaBiEle7W4+qXWiCwUQ1PVoEZkEHX8FkhZNKqVT24aOCAFSEi8oRWtpRJAUsNfzT+7Ua0j18XrBycx2lDFATYb3Cz2Oa89OtHBzsuBlF6ORgOkNpISvrKCUd6afTsjfXkjSE40wY9saBteml4jZllvE/FFTO/UZdrn5cOhYzlUNTIcLTB0prVYBo9WFl5Fy4WUoMZZtU1j/n841wSJNFu891szMizNSQtZoCrKFICgiDGAvlTInZZG51ThOxSTD0vCbHgyZPuf0o+tvJBwJ88q/niWYmbzG+QveYdPmDZ1ETM3NwaFlIzQHii8Z1h8LhYk3tOBJBjYSkw5cehZ5uiTkbsChVaCpGcR7yY358ssvp4RKCHFIwuh7whYxG5uvGIaRYNWbr3FN+Q4+qBEc0TCSjHjbe3o244TFOx6drU6TPUoDy2mmABdFVhZKB/lwKevQRC2WVKiMj6TcGkpMKWaAcx0F3griToUdsoTG5kJAASGQDhemN4ASCbEo59hkR10JHpIleimlSgOKAKk5iZUMxdVqye0bwKEHgsQL+iMdSbdVd6IUdXuYP2oqJ61fmhIyC/hwyLgexxLXnChIpCIQaZHrFtJsBDWIEC40K07Zmz2vq1r2UojSkR5UPYuo2pB6CGeKCBlECGluLLeFEO2iFZcqmtYelbmtvJzX33qLTxe9lxIwgJPmHI9Qs3j6X0/w4svPkZubx+Ytm7j/gftobm7m5z//OYZhkJWVxV/+8hdGjRrFggUL+MmPr+X9l98GIVixcQsXX3YeSxd/yobqDZx70lwuvewyFi1aRCgU4t577+X4448HkmVabr31VhYsWMDDDz/c43UfKmwRs7H5L8WyLHbs2EEoFMLv9zNgwAAWPfkIn77yIrL1rT658OUtdNckHN7jsJA0qBZjRCOTnNtxKwmWt/bnky6O0o9gkJUPgEIDZbGjWNT8HcJWMqtFJiCUEoZn/J0h7g9pcrp574jhbK8bRkNdMZmZ1WhDolTWZ6W5EDsjiOBirxWgSGlOuhMNHSOQhenNwFVXhWhdx6UHgklXZdt104soScmHQ8YysLYKheQcWEcXYuehCCwhMFQVh1ARZpSE04WlKCiWhTMRR5rNKFo2e3bqxFt6nm+MNFns3OBi4AAP2RhY3ibCpkCxJKaiIlWjc4CJMNHNPWguF0ZcsHbdegYN6E9WMLhPSxUhkkK+5JOPWfDqIiZMHUNzqIFRo0axcOFCxo4dyxNPPME3v/lN1q5di6qqaE4H/pJsGvbU4BYGCFCki0AsSH1DQ7f1xK6++mpuueUWMjsI6eeNLWI2Nv+FrF+/njfeeIPm5ubUNpeqInZswiH3DYWWmPFPqFcsns+fRo5axyxn58z0YeIscKxhjj6WQVY+OxL9WdxyHZZRAXIjCB+KVkzYyuaNxp8xzvsyA11LOW3TGq4dfRL5wzczTvsQgJaqicn1yWmjIJks2KVC3ERpSOBsqccd2gFCSc2NoSjECgcgVQeYOmYgCxAp06wvohR2e6nKzKW4qZaIs28JFyyhEHc4CPkyk5k6WlEsk4xQE27DpLY5A+jsStyXSMigRUQJ6AGcRga6uw7dEQHZFkvZNU6/jhF3tl5Gu9SV79jJd6+5hlhM56hp05k66SimTTmaCVPH4PY6eGvBEiZMmMDYsWMBmDdvHldffTVVVe1uVUVVySkuZFd1FSDxCh1NmN3WE6uoqMDpdHLGGZ3nQD9PbBGzsfkyYpmwYzGE9oK/AAYcA60P1vXr16cylHckbhjQRXAFJB+bgegKCsNZHBusBbQurJhkw48dm+kfy2VJUwvxltdAhlJ9mL58FO84NKU/qyJnsjpyFj6llotXPcvZx9+AZpmMqq0ns6GYUl1ht2YhBZj5bvSRmdBhPsUZjdG8uR85jY72IbRGObortxMtGEB1Zik5Snrevr6KUls7b6JvIe6mIoi4O0dqWopKY0Y2wZYYbr+jiyM74w4kv6uwiOGUfpzRfOJGM9LTc6ooFAvVn8ERR05iW/kOGpuaCGZmMnDAIBa89j4vvv4ib7z5Ot5MJ9m5mbi9yfFIKbsMsOiqnphhmiiqish0Ind2vQxDCMHChQt55513GDhwYGr76NGjeeWVV1Ji+Xlgi5iNzZcMueZFIo/eiFHXgOY28eYlEMF+cMrvsEacwRtvvNH1gV0EV6R2AQLJCZGlxPKGd39ykbTIPmQhkeZPkuMBEjlFJLILQNOARqARxXTibx4K8WyW1l3FvKU7ofoydkWTZz4faBYWLwxS2Tkh2OlUCbeLN8cdkzZ/1Rbl6K4oQ9RUkRNPQFFx2nF9FSWldZlBUVMtvliUsMvdWbgBpESRkoTWc5h4s89FXqmKJ+Ag2tL9GjxPhkZufy8IsJAY0sSBioZKT4muwvioIxfTo5E1fiIzTz+Da371a+7+/Z8o9GYDEI1GUTWlU9qoo48+mu985zts2LCBkSNH8vTTT6fqiZmmmaonlpOTk6on5ssMkFWSn6ondvHFF6fVE7v//vvTCmAKIVi3bl2qGOfnhS1iNjZfEizLZPPdP6fqpbdxhN1kh7OSNag8JgUTG8hovoQds/6c5kLsRIfgCq2LlEpS65slsbt5Gyqt81GFA6CL4ywlQXNwPRmNo3DFsxmwI5N9lyR7paRhlC81tn3Huu/8VUch9m1dQwAP7Q4xi8zManKc5bybmECLw9etKAVCTVRs09kSGI4HndJ19Wyc2C+ZlWSf8iYIcJgGllB6DPO3FIHuVJhwYn8+er6s23ZjT05P52RhAWqPBTHD+KgmfcHx//7lr/ztzt9z1lkn4RAKWRlBivoV8vObbmTDhg1pbfPy8njssceYN28epmkSDAY/cz2x9lskMc3kWjLDCCGl75CH1XfkkNQTO1D6Wj/GxuarxpYli1nwwJ8IR9qzNbgTBqMqaylsSm4rnt7IjiPG81z06F77c1dsw9GcdF21haYb/kz0YG6PdbLa8OzYhFTV9oCK7h5aEoSpkl1zFIpIf0ibiS2UZZbxzFkX9nq+M1d+QHFTetSfZ8cmBokSdpRm487dwpChy3C5klkjljGNe/hp57G1Pu7Omv8U/Xbu5NHSeQzVNc6OONlY7OSNSW5CnnZB9sUinFGxhXPHjSQweBiiF1dlMGziSUh2b2pg5Vs70ywyT4bK2JMLKB6Z/mzLtLw4WldxRTJ2odA5ce9OBmD2YHMolqQoJMkpPrgC0ls9MV1vIhavQlrt1ykUB25XEQ5H34I9Dst6YjY2Np8N05Is3V5PdUsMffdWtj92J4q00h7IMYfK8gEFTNwBhU0h9q4I4C/eTY/1Q1rpGNnXMTS9dyQOJYoSaSEytJsIwLQTgdRMIvpTeJiG6hyWvL7EFvTwy4SLxvXprF3Nc0nNQSwRYmrQhz7qvbR9U1jCD+UdPCYuo572emC+eJTZS99m+Pb1ABRHq5ijJytSj6zQGVRTw8ZBlWkZOzL8foQc2adxKq0xMyXDsxg42knN3j3EWoxUxg4hTGKxBKbpBAkKAq1DAuFmK5egsjetzyjuHgUMklZgIkP/XC0gXW8iGt3Zabu09Nbt/fssZJ8FW8RsbA4z3lhbxa0vr6eqqT1gwV96ETPqPmBoZHt7w1bX2vp+ORQ0hTEiGoU1NbiLIWp0k0tQSjB1LM1BPKeIRF6/vg+s1bWWo22k0RvYD+EDqRrozS9jmUejuqagRxYC4OslS3wbXc1zCUOnNr6L/kcsTV7rPhc8VSxhklzGOn0s6yuPhkYXRU31+HZsSrWZaWwlIIemPlvWToqbGjudS7VMFGn1uPRbsSROI9lCcUTQ/LUUBTTSH7MWHk8L0WgA03Dik+7UsA0BDYofdJ2gUk+btvUmYG3UGY344woZrtYq2K1uPikNhNBQ1f230rqrJyalJBav6uKIdmLxKjQtw87YYWPzVeKNtVVc9fjyTg/LkOrj9fyTObX6zU5CFnM6qPd5yPeUEkejkhay8LcuCG5/gKTyBWpO4sWD2zfu85Cx6Jw/UAEwDTx7diIdg0mU7J+733S6Ed4AMvIRZmwFkBTokqpy/KEmQr6MbuevfPFoKodh2zZhJFAjLXiLwpiexm7PqwqLcY5VjBuwiniBl22bJxDb0C6cp2nrWJk4pbVbCyuyGujfZV/eRJywq/vQ/cyIlbrbDm/PUYZudxilJQNXh0dw1AKQyLgLmn0Ip46CjlM16TXvEyAw2RPeQ8AZwDCaO7v50NC0Qtye4GcWFtMMp/XdFdLSMc0wWo/rAT87tojZ2BwmmJbk1pfXd/2232p1LcqZzuBIeVqC3GLvERTPuAKHO8ibgRW8OXgRGcYgRjYUMLC+JeVdFF31vM/DbFtu1/kDp5etZvqGMobmnMQid/dBC51oFUk9rx86dEgEnBQxRUrmfPgq/znpgq6DKoDpZWvaPaSt27TmZhI5RZiDI0Bnl1ZXOF0RRoxdTFnTCOp3ZFKVmYNRqFEb0uhfayD1CtRwNUIvTAa47HNvnKaBGg0TcnuwOqRiUjHIoRavN4YeycaSCkLpPK9lmhqWVFCEhaoaOFQTTAcWELUkpoSCRhNFOol5ki5QYZm4og1oGQaGqtJtuRppIKwYOhCO12Al9nZqIzHQjd1E9ybwZmSnwu8PBCl7XtC9v+0+C18ZEXv073+itn4nDkcOwYxS+g8YyZFHjiQz2/tFD83GBoCl2+vTXIidEIKQFqDSXURJrBJICtj0/LkAvJ2vcNPYsZjacTQBu0ogEDGYtWoDxZGNyYCNHt7Ae8ofOH/UVCbqg1je8GnrWPpwQV3EjHWVCPiI1srPC/bJc+iPhpi+ZTWDGmvSOxECPSeZgilh9OzS2ucwpIS90/J5dOrlhN3tf/uBiMmJS6sZsjFZsiVWPKRLUXUZCdwNLeiqhqUouDxxFE1iohJTHLj9NbQkArg7nNcwnMTjXqRUO4zFRAoDVULEar9P+1ZvkYpKzJNLTlMLe7Oz6LpiGnhDYQzhw9B0TL22x69HddfRXOOBPA5YyITom3T0td1n4SsjYo4VKygJZ2LIzehiORs/+jcrX/WRyPSietz4/Edw8onfYODQgs91ctTmq4spJR83hqhOGOQ7NaqbexCwDkTU5MNXIJiYMweAhQUObpjgBtJfylo8Ki8fNYaT1kc6Jb3tSF9SNT00PJuvLY33JW4k/dgu+ooVDkhbq3bE9vUMqKuifPhEwh5/uxvT0FFDjaA6MP2dgwSamguIxn1scw6kSWQRpIERbEBh36wkST4R07jf+YNO21s8Cs/PHM7Z8VEcsX09VJR1EfAiEXocYZo4TRNUqA3kY9IuBCoGWY52V6JhOInFOrvTpFQIS4kidZQ+PIZV6aGoZi812TkYHaJHVSkhahCRHpAeSECFbpLtbsDr6LoQplBMFC1GqF7B5dEO6Hmnqj6E4ujRpSgUB6rafRmfg8VXRsQi+cWU98+nIOygX4uX4U0ZOKMxWvbWUB+vImx+xCsbPiCWF0D15jHzuIuZOGWkLWg2h4RXaxq5eUtFWmHK3F5y7gksjsgqY5C/gkCDj9zwsRj4MYE7R7ZG7/VlrVUX9CVVU61HS6Vq6hPd/e0IAZqDRE4RrrqksOqBIIl+g+nX0gihDjWzNAdmMK/rfoBtucU8ofyZFiWQ2pYta7mEh5nCkrS2Fgr/5LKux9Z6n94+9kxc8SgRbwBvpIWipjqcOXkIQ0fEY4jWumJSUdAdHhQTTGmBBKcFilBoVvJABZ9sYufa3USam/FkBMkbPAxFaXfuIsFS4yhG749hqWh4YwaDqyqJZeRiONxE4gl+98c7eP0/z6GoKg6Hg37FpVx57Q2MGD2WfGq7FTIUEythocdNnAeQdV4IgdtVhNMZZPTo9uv6/e9v4Jhjkhn03a6iz+X5+ZURsTfHZLMoq30RnyoNiqxKSmIhBjYXceSeIRxdkUFzVQXV0c0sKfsl77wZxJMxlPPP+x9yC+11a/+tSEsS396E1ZJACThxDcpMW4x6sHm1ppHvri3vNENV51dxuhREvLMVMTF/FReMeI5sd2NqmxYrQ2ycx0p9GtXuHuyjfXIFdsX+pmral26DQXpqYxg46+qABPGC/qmx9pXu3J/1ZHMPP+VH3JEmZBsZSb3I7dQ+hRCEfQGePes7qU3+UBNfW/0+01pdo1JVkZoTlKT9FIyGkVKgmC5U2f44LV+3lU//8wiRxsbUNk9mFpO+dh6l4ya2ng9AYgkTRXa/0Dl1XZ4sfJqTAAKpSy770VWEwyEee3E+Ga2JgN9963XKNm9gxOix1MeyUiJmmiZqx8KerQurLfPAlwm3hc+/9fZT+LztFuv+rhP7rHxlRKy2ej0l4WfwOLPwaLk4HfnojkHscg9gqbeYp4tUcsbXMDLSwMS9ozl+SwGhyp3sKVvN4zuvwizIYcb07zDlmHG2dfZfRHRtLY0vl2E2tZeVVzOdBM8cgmdMDw+8A8SUkpu3VHQZvCGFwBgZTBWkbGNi/ir+Z/zfO7U3XA1Ujr+P3TvzgdG9nrtboZKyz6maGrwBKjJz00Sqp2CQNhdmd21mq25GVucgHemBCL2JYs/uTwWkxT+5jEksS7kWG/sS4rcPIV8G70w/nbO0GBrJbCf7IpBINYZlulGkxo61H7Pon3/q1C7a1MAH/3iAYy+9sl3IACks6IOIRVQHjVgUo1C+vYy333iF+UvWpgQMYNaJpwLwn2ef5I2Xnqe4wM+WzVu4444baW5u4dZb/4RhWGT68/jdbXczrWAi7777Ltdddx2ffJJMI7Z27VrOOOMMysvLU4udv/3tb3dZigXA7zsCj0d8plD+z8JXRsQu/nQIYXUk0cBm6vyN7MmoYKe3AlN5nXxnA/n+QaieSWz0juGDIQX8Y3ATE0N1zN5xDEdt81GxcSWfbv9f3n8/hzFjzuXkM06wxexLTnRtLXWPb+i03WxKUPf4BnIuGnnQhezjxlCaC3FfBoe3MaRpNct8EwlrfgQWF4x4DujCSGlNyKsUvkZfRMybiHUZUo8QrfkDI8kQ8m5C3RGC5QNHsJx2kQJ6DAY5cf1SGrwZfDJwRJdtXpl5OuaKlQzosFysZ1GsIDOzmspgTi/uT4V6ctkoRzKKdQBkyoa+BaSk9dNaT8zlQXe2BsR3kxjZUmMYlmTZfx7rscvlLzxD8ZgJKRdcs7uBjGgequx+cCaQEMlXn2okK9auov/AQWRmdS/MK5Z9zO8X/oexI/Kpqalj6tRzeOWVhxg2YDrP/usVvnf1t1m/cV2vt6Curq7bUiwAs2fPRtd15syZw//+7//i832+z8WvjIgV+DyUx8bhaxlPZkuCIXsspkkXlmVgsZuKfu+xonAZQnud4QEnPv9MVvqn8t6YLPqPKGf2nv6ctW4KTds3s3vbg9y1+l8cMXIuZ55zqi1mX0KkJWl8uedQ8caXt+EelXNQXYvVCSOZULYhAXETXGqy/IgQDNu2jrPnPwVALst5dayXoS4rzYXYCQHDXEvJi8epcTp7zBU4aP1S9PzSLhcpK8DQ6t2sKh3Wbah7R9pEymW0WrDdzDG9NWpK0jLqcuzJNu+PGs68JTtQ6D1C8kTzDaaqlQToWxBMR+trULycgNpCi8O/X25LhMBUFHTNgdbtPF/yn5ptm4k2NfTYXaSxgZptW8gfegQSiSHihJx1ZMa7f2Fq6RC2qCOxSC/Fsqt8Oz+54hJisRiTph3DhMnTOHLKUQwbWgrE+eSTNYwdO4JhA6Zj6V7OnftNfv6r69izZ0+vl99dKZZjjjmGHTt20L9/f8LhMFdeeSU//elP05ICfx58ZUSsxXQy2VODJUyilk7UgnrDSzM5qAxh0J5SBu4RGFac5swFLBq8EpfzdSZklRLxn8SjJcfxQnETxzbUcOHqc9B37KBi+z+4a/0LDBtxNmd9/XRbzL5ExLc3pbkQu8JsihPf3oR7SPCgnXfH9kZc7+1Jm/eSLgVjRAbHf/gqACXeI8jql8ObvqfJ9va+zkbB4uqqjdwyYFy3AnT84tdwNTfiaG4gPGx8p3B7C9iaX5r80GV1Yzpvk5J4F+619ON6+Ztona9bUzyE0RVlvVRYhre0U3mLUwnIJvrChsqpVMrhuGMGgYoEc/ov5sWBJ3VtkfaC1Yf20Za+jSva3AhAREvmekxoUULU4o9nQYc0VCZJAYuJ9BeJ8WPGs3P7NpobG8kIBikdOIhn31zEf559kvcXvAmAz+dFxr0k4n70SCbSdGPpXiSSuDOOlJKIaaGqalopllis9xeEtmdd//79W8/l43/+53/43ve+16frP5jsV7Tsl5kz//cCfHP70TjSoCaznnpRiU+WM1bdxmR3LcPdewmqDSiKn2DLmZy96lt89+NrGLtOJbbjBYbu+SmD45/yVtZULp85iKdPyaS0ZC5jdw2h7o1/cvdvrmDxe8u+6Mu06SNWS88Ctr/t+sIba6v444vrOwVuiLjJhE+W41E1srJGcnT+2RRaBQA0m3170I6s3ctZ61bgi6c/gPyRFs6a/xSD9+5Cz8gi3pZxfp8Hcio6cT8tlIPFR0PH8vjRp/Z5DC0EkkLUXf5yKRHS4p3i6bxQchpPDT2Lfx03m4EDV/Ej7iC7D4Ur90XpQ650T0bfghk8GUFEcjYttS2qRalz11KvSBoVSb0iqVGtTgIGMHzQEE496TR+9dPv09zULpzRSDj1fxULUyokLJgwcRJr169h1c51VAdVnnr9dfKKiwllZKHnF7GttRQLkCrF0kZbKRYgrRRLQ0MDkUhShC3L4plnnuHII4/s0/UfTL4ylpgvmMWYmTMYM3NG2nbLNClfu45lLy5Aq9IZ54gjNJXdukodBQyuuJCBuyUtvg9YcMRSirwvk5tzCguDM3nvOMmMpiYuXH0WBeXbWL3r93z0UT5nz72eoSO6Tl1jc3igBPqW96+v7TpiWSYVG9YRamzAH8yieORoJEoqG4ciLUbXbiM73oIVdFGYncDn1okFBrMLeNZazNT4EHJ1H0UbBqEfuwXN09D1s12CFc9gYShEv1CEeXU7OwRExChqrEXRHEQH9FAjjL5HJx5KYvuRi7EteCMZqm6luyzbIgn3sQIbyOIefsopvMIs3uZ5eV5rX72IppQoloVmmiC0Ho3LvMHD8GRm9ehS9AazyBucTITcMS2Y17LQRIyEEiIifd3maXQg8CB49O4HuPXe33PxWXNQVI2MzEyycvL47tU/pGLr+vasLgKy87K4689/5gfXXIVlmQQyg/z+kaRY5RT14+Lv/5CJkyYzeEApx00/Ku0FobtSLKtXr+aKK65ACIFhGEycOJE//vGPPd/LQ4BdimUfYuEQC//5b5pX76XUUUxMMSmL55CQHhR0Ytpm5o/6iFrfHvJyT2KdZzYCixPqP+WST3Ooq17JLv8ujJJSLvnWTeTmfz5hpjb7h7Qke363tEeXoprpovD6KX2aE2tbuLxtyWIa/vUoeqi9ppc/O5fiUy7gmo8Mjqlcw5WrXyQv1sTukmI+nD492ajTg1Qyav12GgLn4h6wieLpDyS3CoWNjKSRLIKygeFsYNP6GdTVdfPSlEqY2PM1VGTm8vKEY3u9zsORgGyiRbT/nQlpJQWsD+LUlzYliuRPLp3S/BJUd2v77ivPUL52FR///c/ddtkxOjEiQjjV5HKDjrkzElKlSubQROfFwl6thUKHDsLEVOIkQhox3Y0lFJyeBNJygVTSBimBel9GWrqsfXFInZHhbe1HKQ7KGy0mH3t8t6VYDgaftRSLLWI90LhnD6//+Umyap1kurPYntBoMPOSYqZs4+3R77PXv4e8vNNY7Z5NgGZOr1rL1z/NYVf9h1QWtKD1G8X3rrgOl/vA85TZHBq6i05soy/RidI0eeHj5fw6JgiUb0oFZnSWJNimjeCaT5PzXlIIXjnzDKKe7qIBQbFcZNdMRSDoP+BTNhy5iUec56etdQokWjh6y/oes3H0BQt4YtrJ3Vc3Poy5St5DNvWs3nEctUYBHw0de9D6VqTkysoEcwc7yC4oQnM6sNSuX3zimoOQK5lXsXr5EjY9/Qjxhna3pTeYxcRzWteJSQCJEougIMlwxnGr7fOfrbvZKfPbhUyYKFojQo2R57BwC4g3OjH1dmESqsSRITtkCUl+lwlVo8nTeyLeIdGd+M32BdLluyqZfPol1Nbuv/u1r9gi9jlgWSbvPv4ctYvL6e8pZoeuUGsUoKATV8p5fcy7tGTW4c39Omud0+hnVvD1HduYtTpAedNC9vZXKBg8mwsvufiQLqK12X+6XifmInjm4F4FrHn+fJ59/hVu/ualCMvie0/eRSDc3OVLugRcusnx68tRgOr8PBZ2WGvTHf7mwYwW2VQXZ3P9hNaQ8i4CN05av/QzC1laZGBX0YmHqbjNsBbwbf3vLPv4XLbklbJg1JSD13mrJfYXj0JJbgHC4cLSOgc+xDUHze50q0laFg1bNpBobCDL66HfgEHJsPrW2xmQbnQ9TMJK9hd0xjoJmY5gs5oBwkIo7Wv5cjQLrwLxZgdmPH2NmSuYQAotLV9jTHPQ4u49BVT/WCVZRkvq/AmcmMKBmt0fp9N1SILX7KKYnwOKonL8Jd+ES2Dl2+9i/ftjJrstdhiCGmMYc1cPpMW1ipdGv82ozNeIZ5/PvYNn8EbpZuZtPIZJmwy2736Vu3e/z4QplzDn5ON6P6nN54JnTC7uUTn7nbGjef58dv7ox/zx/5KLWkv27CAj3NxtewEkHCoNPjc54RjRTmucLAIZ1UQd0JTIoKUxj6nSyzHOIjy4uX5k6x/3AaaV6g0LcOk603fuZkW/XCKO9vH5ST7UQhw+L5YdWaTMYYVzCicPWoS3vm+LtvtM6zqxZkcyDIMusrJLINRFiRahKGQPT67fU6SFaP39UBD4pBsXGg4tQCKRFLEW3YlLMVJfsQCcSHwiTERJ/2ZTv55W599TaQk0VwJNSyQz51sKUnHT0oVrcl8crdcXxUUTASzU5AXW1aMoCpmZmXg8PazP+wKwRWw/mXDCLCacMIs1Cz/AfGYRk9ywXVew4pO5cPl4ajIW8tKwp5mQncHe4HncMvYIJg9dzmWrTqOwvJKynfeyYsXTnHHmdYwYPfiLvhwbQChiv8LopWmy97bbWTNkODVZOUDfizvGHck/OU+s3WWTk7OTAUM+xecOpbaFY0GGbpyHr9rFp1nqAaeVast8EXa6iDrdeBJRCpx7GJzYhpFw09SUz7bc4k6LiwOyiem8zySWMYKky3WjHMk6xvKi+EafrvWAOIDQd4AQAZ4rPY0TW5bgi0UPrltUCEwBYYfEa4VApuep1FWtx7kmAEsoOBQvPgM01JS1rggVTXFiWAlMqaBbarJ+WAf2nYhQBUjhpkVqmELFSbpwiw5rylTVQFWhIZIBnp7urcQhDXxmlCguGug8l29ZFg0NyYCVw0nIDrqIxWIxzj//fNavX4/X66WwsJAHHniAgQMHHuxTfaGMnX0sY2cfy/pFH6E88T4TPQab415k84lc9sksyotfYk+/+5icN4LNgbn8cJqXWSPCXPrpOTRvWcP8B2/itdJSLr30ZrJyD8833K8CUpo0Ni4jHq/G5conGJyCED2nAKp94AGMvXupKx2S2lbi3dWn88U1hcqgH7e7EJep4M8vZ+So9zq187kaqRr/Z8Sqa6hVjupT3/tGGHaV+aKNtiS5qh5nvuOcTvtbCPAGZ6RlhB/FOkawgffkbBrIOTTuxbZ6KW3/38/jPjhiHMdsXs2CUVMPWBC7o8XrJeLIxB+P4jL0VP99WT+WHKKGo4uYQ6WDAJpdOKM75nexFC+WlsMeWqMkM0GxTDJCTbjiUYQqUR3tSzgMS6U+lkXE8CCiJtLb1SM/Wd6lX7wagCbakil3fV1NTU243e7DZl3sQZ8Ti8VivPPOO5x6ajKTxX333cdLL73E/Pnzez32cJ0T6wuLnv0P9e9sI9uTw8ZYNrp0YcoYK4c+x/KcnRTnz2K192RcxDh97yq+uTyb3TWLqSgI4Swew+VX/Binyw7++Dyprn6TzVt+TTzenrXAKfIZnPtT+o0+p0uXYvP8+VT84IcArBw2kh9f+0uENLnHuoq9T2ahhzW6/uOXrc+K9n1CURh98SZUl9H1s1aCFsumYe0fuHJq75PyZ6z8AEFSzJo8/vZUT92lkSLpKgwR6KaNRTb1/JGr0kqbLGMa9/DT7vv+rHxG8XEn4gzfs4Ot+aU9p6bqIyWK5PYMhfzS/ojWF4WMWBiXngAh+hw00T9s4esi4W6zXo9hJedks53RlCWWnBODLc7k0gOheNG1gm77DzbXk+kPUB2NoSompqUSM/dZOqEpSI+adn8d0qBfbC9BM0QcB3Vk93otOTk5uFwHZ1nGYTcn5na7Oe2001KfjzrqKO65556DfZrDjhnfPBt5rsUr9/6D/pt24HD52BQrYnLZhYwvq2XB2Ffol/E+GXnn8EzBsbx7yh7OLR/F7FVeti9/l3tvv4J+Q0/k/Hnn28EfBwlpmkQ++RSjpgYtLw/v5EmpUhrV1W+yZu3VsM+bccKqZmP1Twl9WEH/Yy/ANSqb8vJytm/fDlLivP9+soRAlZKxWzeSV1/LcMdGcv11aMckKH+rmH0LFwog190fj+onaoaoje1GIskfvxfN3UNGDgGGp57hrCc/NplqVzdh41LiMHQWjpiU/tDuSQxEcn1VSPTwstiaf3C9HIWCpJEsMmUDk8QyfsQdPMSVnefJDob18xmPjzmcrCodxonrl+LW48RGh3lLO6XnvvfT+gu5PDgNHdF67xXLxFL2seAtibYrhBLSET4NT9DbYTKrrYmZEjBVWDiUdgED2KNprfskCS2n5zFlZlHq91IVFcT0ruupYVg4I1CS58OQEocQeGJRFCOEBMxevBBtdMzw8UVzyKMTL7nkEnJycvjDH/7QaV88Hiceb/fnNjc3U1pa+qW0xDqSiMV47va/0K8+k7Cisj1RioJOxLGJl0Z/iJIVJZF9AWXaSIbrm5i3vpFhWxLsiH5A7eAAQ0eezdlfO+OwMde/jDTPn8/e23+D0SE3nFZYSMHPbyRw4hw+XHxcmgWWhgQtmoXzowt4zx0mZqY/ELREggkrV6HqOi/POQ+ZZTE4dwXjM9+mpdxLxeIC9HDSqi72HsGEnONpdlpEiePBRUZCYWX9ArLOfQXN3c3DpgNFq6/kE+tYfjahm+CO1LgPrvusDZ9sISw61+uaxDLWM4r1cgyVFLNKTiKhfvGLpoFkZv54lKs3PsaE8fNZxrSk6HYl2m2LppMfOuV67MoSA8iMhnCayZeQhJQ0BdrzNDo3NuB9axdqS7szUPE78M8oTpt/DemNXUYnJkgKWEtrQIdU3JhaUdq4dF3n73fdwRv//leqntjQQQO5/sabySwZ1u2tGZDjJdOzz8LyaCNmw04MlP2yxBoaGrjmmmtYunQpmqZx9tln89vf/rbX4ztyWIfY33777bz88sssWLAAr9fbaf8tt9zCrbfe2mn7l13E2miureXF//sbwyilwoC9Rj8EOnWBRbx4xBrys4NUBs+jWhQyJbqCy1ZqeHbtoUL/hPrBQUaOPpfTzj7JFrP9pHn+fCp++KPOKYla72Pw9v9hfWbnl6o28mrjyM2FvKCfnnZcCinZERzMB0eMocXb/uaaEdW5IPwiM7KeJrHIR/baSehHnM7Hji2EO4RH+ywXU/0KxuS/9ul6Spddj+kI8/KoDTzi/GbPNbEOBZ3yMSYf+m31uhYbx/Jn7UfJfYfZ7+rM6sV8L+8uAAw0/s73WMIxxEW7xdomygD/5LJO97c7EQvEwriNpEi5w2HiDgd1mdmoW1rwP78N6NqxnHHqQLQjnMRlCMMwMHSVZkeALDWGgkWLI9QpGtFSfFhaftq2n19+GZFwmF//+QEyWrPZr18wHysS5oyvfYPKxhh66wuYaZq4nQ76Bd2dBawNKYnWV9MYM1oXi3fdTFEUCgoKEEJwzjnnMH36dK677joAqqqqKCoq6vrAbjhsRezOO+/k6aef5u233ybYod5NR/5bLbF92bl2PYvu+w9DPKVsjrtpsbJA6mzr9x/eLt5Gaf4YNvrPJoGLGc3LmbfKj1K5m93mpzQOzmbEyHM4/exTbDdjH5CmydY5J6Dv2UO9z03coeHSDbLDseTfpBCIvAwqflnTZebQvNo4o9eHuMv6LhHh6/KhvC2niPmju19PddW6f/ONPz9P7Rk38I5ve2u7joOEvLztjBj1Qa/XoyR8FKz/NlXjkxkgDKFxDX+lhYwvVjCkhYcIx8p3eVuchuwlOu8LQ0p+xB1AZ4HyyWZO4VXm8nxqzs9C4U1O5XFxWapdXyyxQHMzmmEgLUndS80QsrrNTiUCAv/VGam/Z0MqNMb9uHQNaek0edrzH7oUiQokhJuY2i+1fUfZVs479mjeXLeJzOx2y2mI14VfU3nkkUd4+umnyc7JZcOGDdx59z0koiFuuukmDMMgKyuLv/zlL4waNapTPbGPF3/EN877Jks+XsKu3bs49dRT+eY3v8nSpUsJh8Pcc889nHrqqWzdupU5c+awffv2DhWr95/Dbk4M4O677+app57qUcAAXC7XQZscPJzpP2YU8x4YxdJX3yLw8ioGeyJsjGUxpOprDKyM8snw58gI3kpR/iw+zDiBD2YIZjY1c/HKc+hXvp3KbX/nrrXPU1A6gwvmzUNz9M1v/VUk8smn7I42s2FMf6Jqe6CMO2EwqrKWwqYwsroJ51ZB4oh93t+kZMTmENutEiJK1xP1FvDh0B4yrUvJE4NPYOqInaz1VbZu36cTAYlE3wIO5K6jqRnxZOq4zQxPS7H0hSEUovh5S5zxRY+kVx7iKkJ0/j7D+HmO8yllV6oCtILFyfJ1XuMs6nuIwFSkhcNMltURloVmJMXMqDYQoZ5dxLJFYu4y0AYkfz81YZHrbqZOVRAJL4oFLk0SVCVa6vRRdmJgogKCjatW0X/w4DQBcygCn9ouJh988AErVqxg2LBhVFdXM2rUKBYuXMjYsWN54okn+OY3v8natWs7jc+fEUBRlNSvbUNDAyNGjOCWW25h06ZNnHfeeZSVlbF+/XpKS0u58sor+eSTT8jNzeV3v/vd554E+KC/Pu3evZuf/OQnNDY2Mnv2bCZMmMC0adMO9mm+lEw9/UTm3n8tzcMkpWInw90VaMLNUZvn8a2ll6GVbyB3102Mji7k3cxJfG/mIP51cpDS4nMYVzEM871X+eP/Xc5f7r2bUHPf6il91dj23uMsH1hIVE1/P4s5VJYPKGBPZnLBp9qgpWbPLSnYWD8UbV0Qhy6JiM6u7zZ6zfYuBM3eLDZ9L4Y7d0u3LhnNEU9Lwm6g8Tqn8wjf4XVOR5cauq5RoVZjuNuLOR5IdeKvNEIQapvP66oCNJJ/chlWh0ehMxbkxL1hesIfjyJavzxfayZ3ALOPa61lqLMDLFOzCDsiZKCRq3UUsCQ51JL2C9Wxnti2bZx71GSGDh3CvHkX0NTUwFFHHcWQIcllIEuWLGHChAmMHZtMyTVv3jx2795NVVXXWV6EEOTm5qCZAqfTyRVXXEFBQQEzZ85M1RPTdZ2PPvqICy64gOXLl/OTn/yEM888E8PovXzQweSgW2IlJSUcRpmsDjuEonDa/3wrGfxx2/2MMSVNQmFHopRT1n6LuLKDt0YvpND/Frl5p/B21izemWlxTEsL564/lUG7W9i5Ywl/3X0FMjeb4465jMnHjLHnzQBr7YssXrUBcNJJPVqtpBWlRfRzSEY2DSDKG3y6ZxxPbfo6TfFMPnVdCUBAdP8A62u292aXl2NGvceG9TO7SM5rMWToJ6lhPcVFvMpZyA6RYU/wLU53vMQFAx9PO3IPhX06v80+dPvSkYzA3GCNZLi5karySVRHLuKZcf26bg94EjFcrZGJvnAYh64jXRKpggj27ZEq/J3HowlwCYnX1XVuRh9h8tlDHbmMGD+enWVlNDc0kJUVYNIgP0vfepPHn3iO1xfMR02YeBwa1ZU7QFMJhyJdPiOEEGia1mU9MYfLRU6/5Dycy5WeckoIwYABAyguLmb27NkAnHzyySQSCXbv3v25rgs+TB3Z//043W4u+N9rGXvz6dRTyWRXBXlaLQ5rKGes+TYXLP06ZvkKinb9jDGRt1gaGMMPpo3hrtO8GONnMj06m/7r4ZPHb+Wu27/HQ3/9Ky2N0d5P/N+KZVLx71sJGS66NX+EQKqSV4dM4qFdRXz85ok8sOpSGuJBJmVvI0uEEAIGUEEGLewbfg/gTfTtVTtLJDMbDB6yDEh3L2VmVuNyRVIC9gpzkfv8KUoUXmEuT3FR+yWi8A4ndl9Dy+aAaRJZaJpJ8dBP+MfYnqNA45oTkYjha2lG1XT0YolRAGYuiNESEej5hVJkCNTSrsXOrSQzcnSHjzD92cHIgV5OOm0Ov73mUjIa1+ATLVi+BmppQXcIDLcTVXHit/y4YoLxRwxi+fLlfPTRR0gpefrppykpKaGwsJBBgwax/QDqiU2aNImMjAxWr14NkJpTKy4u7vH6DzZ22qkvmGBBPpf86Ua2r1hN2V9fZZIbdhqCGmMkc1cPIaZu4Z1Ri8nwLmBkzjS2+k/kxgm5DBu9mZlVgzhtw9G07NrOnq3v8Lcti5A5GRQUTefrX/s6nsB/53yjZUmqtjQSbo7jy3BRNCyIsmMxoaa+pX769uZXycltwRSS6dGlPNjvAoLjJOFFToyYiuY2OSXvXZ5VzgBMMjNrcDqjJBIezCar57RGrQuER7ABIcDtjpCZWU1TU7sF5XQm3U8GGq9yVnJjN/Nrr3A2Y1jNaNbyIl+j4fOOTPyy08dlB0GSLx0bGUmD0vN6LEtRkJoH4Y5i5qS/UAhF4D7RQ/T5SDdHg/sEz2cO0tJkjFv/+Gse++MDzJkzD1VVCQYzyM3L4cIf38S2TZsJawoVmQpew0mJYyh/vfterrj8u5iWRVZWDs8++yyQFJ3rrruOyZMnM3DgQI47Lj23a3f1xAAeeeQRvvvd7xKLxXC73Tz33HM4HJ9v0gY7i/1hxvoPP2blY+8y2N2fcl1QZxagkCBBNR+PfIP1/j0UZw+j0T+bbdoIArKJac3rOH2LmyG7HVSFN1FvbaIhz4vMDpCTO4XTTjmb/OLgF31pafS0ELmnfWUrqln0zBbCjR1C1oMuTp2+kcT7t/LsznE9njdzUDODplZCsP3XXmkA9388BJe2uVQEzn5D2HOypHlsOU53u4Ubj3t5tfp8nittXdDfQ+h5G+vXz2CVPpnKYC5ubxOlWZvJ0erYxHAWiFP7dL/8srn7zBo2XdOnemHpWUk+ZAb3ix+lNekqOrEoYuB27YBuYqz0jQlib8WQLe1WuAgouE904xjRdYi7IaHeFORrvT+Sqw1B3BKoQtLPkd6+WlcxhIaqeDEULwlcCCQ+K0JOVEEkdBIygulykpdfjKp275ArLy9n8uTJh3U9MdsSO8wYNf0oRk0/ilXvLEI8u5hJHtitW+w1Spi54RKOscJsGvwiH+U8xdBAjIyME1iccTRvT/YzeEIZE+vyOaFsOKP2mlRu3kTjhld4es0bxHP9aJ58xo4+lenHTsHj/+KstK4WIitZWWSedSZqRiaNzz6LsXdvap9WWEjBjTdSK4dR/voOSgTUaoI6QyKFSdTxCYtXrOVsbxNuRSdmdZ36KXNgMwNPrOi03QpC5FtRvP4R+CtG4hg4g/CAbcTG38e+jxunM8I5JQ/DLsk7pUfRQLtllE09l/BwmoAtYxoPHHEFMUfHaMT9D3QKEei9kU06fSmKieASHkbBYhnTeIxL+9S1pujdChiAY4QT7QgHYksGtCgoXg2lv8Dwdy8GjnqB5gAz0LNL0ZAQb81eb0pBTErcHdoH4xnopiTqjKMrMZyKiaZmEFH8hH0KfrdFbiSIEQtTu2cH/sx8fP7eM9wfrnxlLLH7//QLopFq0PxoaiZ+fzEjjpjMuLFH4M86fJJZ7suqd95n3b8+ZLCrPzWWwc5ECQoGpoQW30IWHlFOtauK4qyRhL1HsUUbg4XCUGMrE2v3MGNnJoOqXdRHdlMb20qLo5pQ0I+V6UK4s8jPGcfMGSdSPCivxzeyg0W3C5G7wBSCNUOGYyoqg9RCSgefhdJBDHQzxi6zjA3eKHvVBr7P33ly85iuRUxIRl24FYev+zyFSiyHprV3U+MS6CPuZ5hjKYroHC4tJZimAprCW5zMXgopYA8n8iYa7ZFZhzzHoE3XSKtT1o2ucMkIZ/Af5vI8nzKl2+9qX0tMk5JBsRYMT02v53BE81B1H6YWwfDUIUUX6ZoMUBsESqvBH8p24PJ3X3G81hBEO5Rgaast1kaipQDLcCNIVri2RJgWdwxd0XFoAaJKBgJJTiKCPwwxEULxBMjNy+/ibIeew3ax84FwKEXshasux2GUEDWb0a0mErQQdSaI+33E/R40rwpakML8iRw/6wQKS7MOK2ErX72WRX97iSEUE1Ik2xMFGNKJgo5u1bJx8Gt8mhPHcO2lKONImrzTKNNGYgmVIquS4aGdTKg2OGZHAd6WGPXxKhriu4lQSdivEg34ET4V4fThdhUyoP9kRo0YTb+SHFw+7aDci7aFyB0tsO54f8IUlo4azyWvPU8/zwDcU5ORg92NQ080UdnyGh837+xyv78ozNCzut4HScHZdzFsQDZxKX9lGh/vd3sLhSv5O2HbBfj50pZCaj/ueZasRcfZrbt2XxErjlj4ZZSEr/ffY2e4ECksdG91t23UWlAiyfMKhxfhycZyxjuJniGh0UwXMCBV5RkAS0FpLkzm3ZQSXTqS2TeQSHRaPM3oqoGiZRMTXrxWhIIQ6EYzhttFfkG/z/25Z4tYH7n7jpvYMLCQ3KgkP6xR2OJhUH0mzlicsN5AU6KWkFFNhCrCAQfRLD+qz4PXM5jjjpvLqLEDD4uMGbUVlbx+z2MUhgP4XJnsSKjUm/motNYjEltYN+QDVgZjSNde8vwjsTwT2OkaRYPIwSET9Dd3MiBcw7AGgwlVQQbUe4jHG2nR62hO1BIzq4lqTSTcDhJuNwmvC9WloDg0pOJFVT04tEy83izy8mO4PTpuVyEB72QCfj8+T3LiWloS0zSTiUUtg/DSj+HX13e4GoGaOwzhykTGmzBrtwCS9ydM4e0p07n1b/cAAv/Jv0G4e36pkK0Prw+rX6QisrnT/uCQJgaeUNnlsT1aTFJyBi9yAY/3uf3pvMh4VnK76JxSzebQ4pYRYnRRAbsjXaXR6sFyaxOxwpJS+plOMgyZrHoc2NW1ZdWKJQWOUDGWv6rHdhjgqBQIhxfFl5fehxpDKibCUtmrtBBS0yOQ950T83j6I/ASaWpGjyRwSBVFUUlIMKQDkFgiRoO7BUXxEBcZqMKkMKEjomESbpWCwpLPVcjsObE+8vHgTD7NOpKwaF+575AJCq09FCSiFEWyGNCUxfg9R3FkvYuW2mpqd+8mbH7Kuxs+5NXcTNRAgMKCaZxy8unkFHwxgSe5xf24+I7rMQ2Dhf98FnVZOZNcFk0k2JXIATmSyWWDGS9VTLGRbf0XszrnA1T1BYYFNDI9E4g6h7EyYxjvZObCQMiy6uhnxCmMBihu8TCocSgjq7PIiKpgxIk2tBAxmokYzcTNMKaswVG6goLhCq4WP0aiiWhwM3LnSKq2DqahQaHWqoTWd0Ak5KpF5FfW0ZZVTSs6Ete481A87RkHrGg90TXP8udzL+beO29JtssdltamO4RQkNLiyJw5VEa2IPcJjzciXf+qWyj8k8vaOumyzSvMZTBbUxZWj+2F4FU5l42M7HXMNgcZaREnvWBl5zZdBHv0MWVWblyS0fp7JQAtmtOjhVVvguWuIF/0YidoyTqbiqvz77liusFMLvbItRyE1PQ53aDaOh7FgdtVhMORzOaSkZuMsJRSEm5sQjZH8YpkaZcWPFgJV2udMhMTqBBOgh6BPxpm755dFBSWHlaeqJ74yohYaO9WvI2vEHDE8Dsz8DjycDlK0R2l1DoLWeceTjTHB4Mhz6pmYKyZIU1FjK0exLSKbCJ1e6neXU7L+ud4fMXL6Ll+nN6BnHD8hYwcN+Bz/8JVTeOEyy6Ey2D3xk1seehFihNNZHsKaLBi7EzkocsxjNw5jOE7VEwrQiTzfdaU7qTRsx1VqWeAJ0625wiEaxhhRzErA8NYkJkHJclzZMgmssx6sowEWQk/OVEXmfE8poajjNt7Ko617X90EhPRNtPtAVOatJhN1KhhUFV8eMjL2EuMT9CKjky5Bzsi3Fl4p1zB6TvryG+sT25zdU6xJLGIZm3CcDWhxTPxNAxHoCCEgk/LINddQk0svUhlaI+XREjrNCe2kZE9J9RtbfyQvIoETrYyrPcEvEJQJof33Mbm4COULlb27dvmwP9OVYu0lbWq4YVIfie3n7BUQoaDKAm8Sh8dXU4NsW8plw4IwCFVPJaLqBJHExr53ix8qhPLUEC6kaaC1GSnRcn+rCD+rCCxcITGuhCNXZ1HShoNDcvrIzMSpnrvbgoKS/s29i+Yr4yIXbB6FI3GCaDUoPs3UpNRw66snVS4d5JQIniVegq9CkH3cHANZ49rCJ8WTuDpIpXAuCaGx1oYVdef6bvGM65GpXbnDmpjm1i49ae8mh9E9eUyccJcZsyagqp9vmvIS0YM5+I7r0dKycaPlrDp3wsZQIigJ48WGaVK99Fs5uAPncZxG+IcLZ1YVhjTuYYdxevYHFxNzPEJimiiyNFC0JuP11GKUPNJaLmE1Gwq/YU0BLKZsVdy5YauFvymX/MOtZpK9ztoopEQPnZQjLefi1M9mfjGnQd0nt8SQiCRXFiTASiouUNRAukZsVvyP6F6xBPJVEytaLEs8jfOI1A9GYlFoF8Uj0tDiwdpqXJTF60kx1WC2OVkwwiTRrII0sAINvQ5jVNE+HmAH/apbevF9L2tzeGNlKgSPPuIGCSFTG3xprn9FNNNjXMvEklFZTU1sRg+n5vi4lyU7qYkLK3HaMc2/HoGTilwKJJwKMFv/vgHXvjPc6itpVhKSvpzyy2/YtrRUzod6/J6aGk2wOw+t2Oz4cDpd+MOR6neW0V+QfcZ6devX8+FF16Y+tzY2EhzczP19fW9X8hB5CszJ/bKD//Ajvj41k8SjQRCWBjSgSUl0mzA8K5je9FmNgUVqrUWpLOOPH9/nO5xVLtGsVsZgBQKxeZuRrbs4KgKhanluYQjleyNbKLJsYfmgiBKZgalpbM584xT8fi7KXtwiJGWxYaPlrDi5Q/wNEGBoxChKdQZBjVGJlEreX81YkgEhnQgrTBS7KIlezk7s0NU+1QaNIuQkiAhwkjRwpPlNxLUg4huc3RDs/o6xdqjZIpQalsTft6Qs8jelMXIgef1On4r3oLiag8rl1ISKviUyvH3JTfskxUewL9nKpGcdVjO9rRRWiyL3A3ns7BQ4/6iMWlWVLasZTZv8Zy4oNfx2HxF6DRfJilRJH/xKAwvKMWt9fz3LAFDmHywewkrF60kGmqfw/L7PcyePZ5hw/bJaGGBc48bxd97SrFmwigxMDXBVT/+AeFwmD/e+WeCmcmXsTfffp2WUAtfP/c0FIdGMCsbhyOZViqqS7bVhno5A+DVyDciOCNhlMwgway+vehdc801CCG49957+9S+DTuwo488+cP7GCjzUFWBJUziVoIWC5pMD81mFrL1NUgTMSypYloSi3KqCxexstCiwtGC6m4k3z8awz2eba4xhEQGmbKR0eEtTKqOcdy2QpSmeqoiW2hiO025PmSOn8ysI5l71nnkFn5xC7gjzU18+Nyr7Fm+g6DhIkvLw6E5aJFxGgyNBjOILpMT4gIDTSSQUmDgREqBtKLkqnFmBNvLo3fl1nMrH5Hr+E2yn47Pgv9n787joyrvxY9/zjmzJ5mZ7BshgbCGfVcQV1SsS6lyqXUFae2iXa6tden1d/W2F+29Xq+trVVbt2qv1dYVrYgLWFEWkUU2IQQI2SfbJDOZ/Zzz+2OykpkQYAIJPO/7slwmJ3POGZL5zvM83+f7bf+fyj2XIY287ajXq+kqgbR9nc9tbhrFofN+TsTcTMz42d5MWUPmK8Z3jbb0PdH0aSlGIkZ7MogFHwFp6O6TEdp1fyuL0wE7/tc0kvFiItTjg062X+Oe8iATplgozBveI4j17N/d9ff1FZtZ897HcS/zyivP6hnINDBWSsj2YX1OKaroNEtekj2t7K5t48KvncvW9btIdfZeS/vr317kjZV/JT0jnX37y/ifhx/B3ebnvvv+DTUSwe5w8osV/0PxmHF8vn4dj/zyPl76xxoADh7cxQ9uuI5tmz6novQrLll0NUuXLuWTTz7B6/Xy2GOPceGFF/Y4XzAYJDc3l48++oipU6fGvYdYRGJHP33rf3+A191M7f4DlO8to+lQM0GXjyTNSJ7ciNWQiqToePQgjWEzTWQgMYZ81wiGuXQiGkSMOygt/IIdzrXYlJcosmdhts1mv20Kn43M4akRAcYF/UxpGM4FB6cxsUGlbv9+miIf8pc9awlnJGFJLubiBdcxpiQxGUB9VbfozmZ3cPGy6+jYyxlo8/LlmnW4PtuJ3ByhgEbsxlQMBhNhwni1CD5NoU210KbZUZUkrMaupJh403pFpRZ8Deei0IxZ3oXUvs8qmuQLaYVVNNO3WM8thS3oxj4q90ux096dNBCMl60myaBr7QH22NrTC4PQ0f7tOr4ep8nnt3mCGXzOV9p4POECSrZ/i2nNOnqKRPeCZioajcZGMsKpSN3eQiOSiktuYMOnm/q8jLVrt1NcnNc1tShDW6oVCGHX47foCapeMELIlMyOXRsYUTgyZgBrv1k2bt7CZ+9+ROHwfKobqljyzSX88eWVjB4/gXdef4U7v7+M1z5c3/s7VT+gU2e1oBhtNDY2MmnSJB5++GE2bNjAokWLKCsr6yw9BfDaa68xYsSIYw5giXDGBDFJlklJSydldjqjZ/ecL9Z1HU9DAzs/WY9rUwXGoE6J4sFqdBCQgrjCCvWRLIzqDCYfmMAE3UBEa6Yh+z0+z9+GbvyA0ckSjqTZ1Fun8udhk3khX6U4coDJzZnMLx/BOTVW6g8fpCGwm/f33cFb2Q4MthxmzfgGZ50z9bh6hMWqfGHIySH73nuwX3JJ9N7iBDlLUjKzr1jI7CsWdn5vOBDg0Jc72LNhC37PTpL9NdjdCsbmIpT0caQFNUgagydrc9e0XjcRczP7J4Jv+zWkuGaiUI/T+BRWJfqLIklgs+2kqbURTGkxg3i85+4zgHFE2ns37j56QkUvSiaIGIWdScwEuj7Y0LvSSom0C0y7KGAKCuM5srGIgowKlFmqyYqY8MlmVEL45SCuSlePKcRYPB4/VVUNFBR0pdMHzUaajDaCIUgLRdfgOuiaiu5vwhz2EU5ORlWi0+zdf38OlR/glu/dRCDo56zZc5k94yymzzkb49QxtEaC7FhbyuSSCYwbPx4VuPwbS3jw3+6kvq7nXjcFsIeS2gM71NuMmEwmbrzxRgDOOuuszlYsc+fO7fy+Z555huXLl/d53wPljAlifZEkCXtmJnOvvoq5V0cLsmqayqEdO/ni7Y+JVLZRoviwmuy0aH5qwil4pUxyG67lqgaNiBbGZ1/L5sL9hC3bGW51k5kyHa91Bisz5/Balok8tZJJrXbOqprLWeVptNZUUuffy859K9jwvh3JYSM5pYQFC77ByNG5Rx2lxat8Eamriz7+m0cBjhrkujNaLDiKmnB6niJJcnc+rrd8Sdq+FOzN56Gj4Rr3l/YX7sgXEtDBNfb/SHZNRyWdxvC9pOkPIkseNFKRaaYq9CH55iUxpmP6eO4+9Jn2fqwjq34WjBWGrmv4KyP0gz0SfGR6JztEzC1xnyMjkoZXqaJZCSIbnPjkJCTNh99fEfd7umtr6/mhTJNtjPTqdC+DqErQZJIweppRZI2ILRlF1cBgZNKEKRw4VIa7pRmnI5WiwpF89O46/vq3v/D+R6sASE4yoehhmg3J1FkMRAwy6UD3TQGSJKEoCqoWza5M1cLUBP3Imowp3Ei8/tTd35/Ky8v57LPP+Nvf/tave080EcTikGWFkVOmMHJKNBlEjUTY8fE6XO9txB5sYLQpSESOUBMx0BDJIdl7GQt2hThPkwmZtrJz5HYaUt4kw/gkOfZxRGwzWeecwnupyaROaGJCWxMzaycy/0A2sqeFOtchWiPreHfPP2nLSMFgdZKbO4cLz19A9jBnjx8aXVWpW/Fg7NJNenSHVM3/+3c0t7vXl7sHuSMDmcv1Hjt2/KDX96j2EDUzn0XanoQSTuoxzdeLBBFrE/7UvdiaxwMaTZG76J565XSGQe29nuZP3dv3c3fTfe2rBcfR0977SwSwoa3PdS8dCY1L9VUYpKM3bjQE43fQ7p7ubtUC6Eo6umLFZC+EGBVejpSU1LX2o+kKWb7etUxlPbo3rcqRicfYlRapaBp5Y4pZePHX+Nef386j//V7HA4nAD5/R/X8aMdpLdSKSW5izswJ3L9zFxtrDjCpsJDX/vYWWTl5ZGRlo6oq1RWHScFPVm4G/3H/S0iAvc0JoYOdrVhuvPHGHq1YOjz77LN84xvfwOl0HvW+B4IIYv2kGAxMveh8pl50PgBN1dWsefF1OOhhsjGEYjDiiqjUhLMxR+YwZ98UZugGItIhDgxfw9aMNdiU/2OEIwejbTZ7bFNZNyqTPxQHGBUKMd6dxZzqEZxVbcdbV4PLfwDvnr/yt82v4nemoCRZSU4ew4xpFzMq3Nhn6SYJ0NzuXiMdoHOkUbfiQVIuuqircryusm/ff3Q9wZFP2D7Cyihd3K/Xq+tTbO/tBlr6Ng6M+7+eASvgwFg3tV/PHWvtSxCAzhY2sbIMAS7nLcyBFHQlhGZsi5skZAikYW3ue6+fQY/+7kS6nScjvwBLcgoBb/y2QCkpVvLzu352DYHoxuQ4v3ZkB3U83bqbqLJMc7LMf/3mCZ743//msm9chCIrOBxOMtIz+eH3/5XS/XtRdBPpvgw0yY9i9/HQ4w9x77dvIaLp2O0pPP3E06SpPlLzHdz5059y4fy5FBUVMW/uXCQJZF0h1Z9BWlpa3FYsuq7z3HPP8eyzz/b5Wg2kMyY7cSAFfG188vKb1G0+RB7pJJkcNGkBKkMZhHQbCkE0XSGiN+LKXsXneUGqjfWkpRhJsc2k2TKBcmUUEclImtbIGP8BJjb4mVOZQX6zEY+/loZgJW2Rw7RZgwRak1i4ZXO/r0+XdEKjdFSHjtIiYdovY0gfQ8aP78I2vQTzCAfulo1s2Xr9UZ8r86tvUT/upaMeN+zzO0lqntDr8R5rXr3fYzrfd3plGbZP+YiiusLxkHSVy3mLH32l4vhqBh77GuoueKf9i90ObP85TC9bRPqBq5CQiaRIeC5I6pWdWGmqwy8HUQ056HLXGltV6T42rXwj7rV0ZidqClo4HVvQdtTrL0+S8Sk9L1TRIatFPaJ3q46RMEZZRtM1QroBDZmOclNuqxdZkQkZ0tGRyAz4MfuChMwqmVnDkduLgPtaPajuAHsP13Lp18/vbJg5EER24iBgsSV1Zv5pqsrW9z6iYfVmhtFKmiUbD36qQna8Wjb59deTX68R0YJ4HB+ybfh+ai07yTC5yE4egWKdSrl1PBsKh/GnQsjSainytzKqJZ+pdWOI1KbxUfgwCzl6EAsWa2gp0PIvEbT2rR7JdTNw7L4eYzgN3xcqvi92oDhMlE77kl59R2KIGD3IoaT4n2Lb1U76U+cG5A59rXl1fYCW+JzZvCDd0mtP1w08y4sd6ZXxUqhFYDstmXU/QSl+5l4s8/S12PD16DCg7hhG6KuDOGZ/F3lbEfXjj5gRaP/xaRz1Bi3DPibrq+ux+o9IBCOaieiXg0jI6LKlx9fzR49h9pWL+HLNhz1GZClJyZxz1jSGZ+cTdEuoYRnFRL/Wfw0aR2yGllAlsOTasKigqTqyImE0Rw8K+f14m1sxRFQMkoEQENGtpPksBI2tqKZaJFM6dZYk0hSdFI9Kfd1hMrKHoygyNnsKbn8IqxTu78t9yoiR2ADSdZ2D27az/pX3SWqWyTbnEpIj1ISMNKrZgIaBEGHdgEY5VXlr2ZalUmt0Y7H6SLeNRTePo95UTJVcgI6M5eMa5ECE595bQXqgJcZkXbRetZoKrv8Id83mSdEAlrf99va/dltj03V8aV9ROevXx3iDxP8FbP+pytt+O8mu6fhT99KWtpum4pU9DjtyxOUhhd/ys/Zr7p0GLYLUmem7+m/IoLFzDfRF6Zajfs8v9P9HCbt6PJb+v0bSRv66s6C0jkbjyLdoLH4jekCMUVnOvjvRpsyhMG84ZoMJCagx1uNV/KiGLHQ5dnarrqk0VFVhbmwg3ZyMM82KLPf8jbUoSdgMR+8VV2eRaDL1/m13+FqwRILoEtEC5ZKMQbFitzswmY3R3+0WD4GWNsyyiYAmoWFAJ0yTrRnFlIJfSsER8ZLaGiFgDpGZXdiZ/h8JhzEMcKdmMRIbxCRJYuS0qYycNhWAhsoq1r7wOvLhNiYbw8gGhfqISl04kwjFjKgpoKgGVE0lbPqCA8N2sdf5ORHDR+QYm7Bps6kKXoImKTwxeRH/tun5GBl+0d+81sWRaADTJazusRgCTrL2RkvEHFltQ5IkrM1jMPhTo59K420mhn59auw8TofqCX/EMM4WM2Ej1tqW1J7aG29P1zGlLQqnjQwaOwOShsw/9KtoIi128d72bs3j2NPtMVBajFjdY3sVlG4Z1r4xOc6iVEPxG6S1NzKNSCoNhiY8hhCa0jOAZYUaMegRqs1ZgIQkK2QWDEfOH0ZGkwtN713JXouRFRlLJM6Ht2TNgkUzoOs6GiqqFkGjhWZ/C7pRQZYtpKdnkuS00+ZuwdDiR5Y0ArqJdF8mLVozNnOEFkMqusNLaouOq66c7JxoPdiBDmCJIILYSZQxLJ/F90RHQn5PK2v+7zW8X1YzUm4lxZyBXw9SFzbRSCamyDlMLJ9JySEFVdNQpQNsyKyg1laGZPCw0ZbCs0kLuHnr+yjurh9wNTUawALTdJLrZpD11fUYg0evAi+jkLX3+uh6VaxSBBD7l7wvEmAMEjH0rrUYb1+X3ldF8X5WGxdOIzECkozGTTwT/fk5so1K+wedjm7N0ceif2Qf+i7WWeN6PP1RM2Il0ExthC3NNFrT8ZgtIDnQJQtH/gKkqG0kq35aDCm0KV3rXIoajhnAgLiPHykS80dfpz5JR0ZGRseggUmVsYatWCMykVCQiOajIXwYDEbS0nKwOew0VbuwqSH8ugFHIBWv3oLN0kyrkops9+Bo1XC5qsjOHtavazvVRBA7Rawpdr723aVA+7TjlzvY9PqHyHUhJhn8mIw2WrUADRELzWRwOC3E/uHrsHXbu1I72kTdNWFM+yWUFgnVEU3gQO45ddhfKa6ZZLw6jqaLvupcQwNOfPBzxPf3p/1Jn8Ta1+kpTiWNHgGp3Sw28hP+OzqSp9va6REblyGaaZi197pogWhjz9WTvvaCdSdJGimKm4icg48j1+Z0jHqEJDW6ydmo90zfV7T4gSqihdB0FVmKX+wgLHNEUkeUSQ+goKIjE5FkgoqJVsUAJjAQxqoq2EMpJAUlQv42mlyHkUw2MvOy8XvbMDf5CKOQHHTi01uxWd24DU6UFA82j5/mpiZS047+AfhUE0FsEJAkiZFTJjNySnTvRSQUYvN7H+Jadxhzi04kYyurR/2j1/d58IMMoTE6aDqm/RLWL2RUO2TVRjMN+yrUG4v1SyPZa4yERuk05ZqpnOAkf1LV0b/xGBy1/Ul/9LMFvTB02GjDR1dps1gBqbtZbIyWidJ7Z7ECOMovxO6a1Zkq70vdQ8TcghJwYHWPQUbpcy9YLOk04iOJrk9m0WmLLH9D5wxGWOr5tqr2UQ8RwBfxkGx09nq8Y0Kk7sgtZLqKMdKKrAbQpOgSQvRYFaOsY5DNINtoU2x4rApmSwBHyEqKXybY1kpdpBxHai5Jeam0VDdilHRsITtttGK1eWg0JmO0SigeN6HkFEymwT2lKILYqaapUP4ZeOsgORsK52IwmTjryss468rLCEci3PjMrYxqnI7P2EqNvQy9vcnegZBMc0QiZwc4/2bonFZUMsZgPOf4PkFZZizDv+MVduPno/ypZFu95PNqwm4X6Hf7k1hsupfz+JB3uSKBVyScUu2bkH/Pt9mvjzlqJY3uZLReyRsdNEN0ZOTJ+oL6I/YlGgKpZH51HSmuGRgCqfELSx/BQAQLgc7u0bKmYfe2EAlqNEhJWE0R2hRr+22phD1fEAy7MAYMJJtLkGKMuEJagFa1FZvBjqFHxQ6NVtmDxR/GGEhBk0zIuoQpoiORAiTT/aJ1XQUpQMjkw2fwItOMxWAhojhwmZNpMQXICKRg9UfwNFRhsDlwFmTRXFGHUYKkkB2P1ILZFsRltZKvQnN9JVl5RYO6QaYIYqfS7rdg1V3QWt31mD0PFv4aSq6ibKuLj17axfyW63C692MOteJOmsvKqVs5kLkDHYndnxkp+WvPRaxYjST7S7KkYpv9XVZOtfJxloH7d+1GCXyIanYnLKfCedQSwPH5pGTe1a9KzIUIJ0+8ShrdNiGbCMcNSMfDk78eT/76I/ZRRUXMzdRM+T3S9tvJ+irOWnAcWYF6WsLJyJqGKdS13qvqEt6gEXMwQKvvU9oOP4QWroteC2BQMshN+x522zm9nrPF7MOltJEWNGONGNA1EyHZhCIlIyNh6Dbyk9CQ0IiEw/zv7/+X1956raufWP5w7vzJXUycMBldVwma3YQMDVgNCmElnSqrGbupjQyvnaCnhfpwgKzheTQdrsUoSaQEHbTIzUQsqbiSDeQ0m2lqqCU9M3ZfsRdffJH/+q//QpZlJElixYoVXHbZZUd/ERNIBLF+UnWdDW4vrlCELJOBs5zJKMf46aTjOWqDIVrKVpL12b+RrYaYTtcWEL21hsb3vs2nh3/CF/szyWrwMO+Lv2EJujufZ8YOJ6+cW8A70w8T3a/Zc9JQD7Ye/41KOr7UvdxR38KdVUlk1k+g0Xw+jaPeOP7nPMI49pCit+CRjj/YijWxIaR9bctIiDA958YkdC7nTb7FiwN7DTGSknQd9k14kvf3nkVKVSHzsg5jNR19x5EalLGE+ijy63obT90vOTJ6RtQGKup/RUHmv3UGMk2GoFnG4QObP4Skd+0rM0kKEUsKkska/Q3XdTRdRUdHQ+f7d96Gr62ND15fSaojFVlS+McH73GgdBdTJkxAlQxYQulYQuCXGwlb6rAZk2hVnAQcIXLa7Mh+L3W1lWQPH0bT4ToUJBx+Jy1KMz5jBs32CE5v7OLbTU1N/OAHP2Dv3r3k5uaybt06rr76alwuV8zjB4oIYv3wTr2bfyutoibYtfEv12zkV6PzuTzTeZzPMRV56vMkN7/IcM8G7m5sZrJV5csxaUjGCMk8zXklIOeC5jPAtq71H3PQzY3vuzH7ZTK6VbfpqMyhD3ND6h6szWOjmzFj9P2SYuwwi9UCpSWUjKVlxLG8XJ3iVd2Q0VjGU/xWj7EfrD9EABtc2qcD9TjJCR1rWzP4nJ36BNZxPkEsjGUPl7AKQ6868QkW58dFkiDZGKYufQtsTGdv/Rgm3LAfg0WN/yOmS0RC8X/+dF2lruFxYg7/2lU3P4GSNherZkJRnTh8QayBht7Xp6sY/W7CRh1jWiomm61zn1lpaSn/WL2KiooK0tqTLzRV5YrFV+D3+PjrSy/wxttvkZGewZ7Sffzn/f+Nx+Pml/9zPxE1QkpqOv/2yKOcXVTI5x9+xL89tIKtW7fRUuFi194DXL/8Wj7asYE9lW5uOP8cbll2S69+YpqmRZvVeqONNt1uN8OGnfyMRhHEjuKdejff3nmo149kbTDMt3ce4k8Ti44ayLqeo+d8haak4km7nbAryDspu2GMB474hdac4P5OBOmPBqzbZHSgKclCwGhg7i7Q8SIB/qlat8ocFTTxawyBVFJqzsKTu6HXesCR1TTitUDRTF58mTv68Ur1FGsPWEfVjRQ8qBiYw6dsZN4xP7dwChxlOvCHPEKK7sFNKnbcSEALzl5rW5PZwWSO/edpIC04kIu3Lpq8UPlJDkUXV8VNgNVDfSc5+II7iai9A1J3aqQByVOJwToLJLAE+55eN/l9mJN69h/cunUro0aN6gxgALKiYLPbsdntJGU4WL95Ix+9u5pxw0dT39jA7AXf5NWXVjJmQhEvvv0Sdy69iVc3bCZkNSHrUO+qIj0vB9O+vUg6WLwGLFoTzU3NMfuJZWRk8MQTTzB9+nTS0tLw+/188MEHfd7LQBBBrA+qrvNvpVUxP1N1hKP7SqtYmOGIO7XY8zl6HjP64B4u/PQd7L4WSq6LTk/0epr2DZctiyO4DznZk5tJwNT1z1aak8741Gr069t6nTtibqa56N2Yj1dP+R1522+Pph33twVKP9cM4u0BayItWo1DjKQGN11HQu+xZy8ZDxEMBOhZ5y8ZD9/mibgZhENCYwiIBqeWg3YOvQ/5c+swJXd9oNQ1CHkNyDGqZnQXUZv6dUot4sIqh5DCIaSj7BXTw2G0Nh9Kcs/KIN2DWllZGddccw1+v59zzz2XefPmcc455zB7/lw0TePtNauYXDKRKePHElaN3HzZMn511wO01Byk2WhFkyVMfo2mpnoMdiuSBGbVgs1viNtPbOLEiTz++ONs3ryZsWPHsnLlShYvXszu3bsxGE5eaBmQM5WWlnLzzTfT0NCA0+nkueeeo6SkZCBONaA2uL09phCPpAPVwTAb3F7mpXaVjlE1nU0Hm3B5AriMxHyO0Qd28fXV0UK6ybm+Hr8wvUigpcH+s1IIVPecsgmYZIKLgtGyh/HKYPdRlb6jJFS/WqD0I/b03dtLjt0+Rhg82tevfsj/dI6sOkZTALv1EvYwEYASdjKe3UfNIBxQoSSGbb8NX9qeXiXNjkbXIdxmwFvbMzC3HLTTciiF5BwfBlsEgyGXcQuMqCkyYV1HlhQ0PfY9G5T+ZQUbDQ4iRo3kJAsR39GP1yM930OmTZtGaWkpzc3NpKamUlxczLZt23juued4++23AUhOjm5XkGWZ5DQHitmApoewSjp+3YikS0jhNmxKhKAGwWQrprY2PG1edMAkhTBEYhcnliSJ1atX43A4GDs2uoXhyiuv5JZbbqGiooIRI45vCeJ4DEgQ++53v8utt97K0qVL+fvf/87y5ctZv753G+zBzhXq31x99+NW7azhgZW7qW3xMSa1jJRsP+OHF/AV4zvXDCRN48JPoxW0JcBg6995DDa1V2BIzvUfNQDGe7yj71d/N3z2x1H3gIlR2OByxLzZ0fZmTWQnE9l54ueNGLFXn0PEWk/AcRDN1HsmIXp9xC2DpgPPt4RRk98jzW/ncr8JoyXUr9rQHZ+lqj7LBj3WN0h4a6IjH1ta1xu5rklIZh16F6GJHmueiEnJIqTWE29dzGzKYfjYS5EkBdUb576PIBl6TmOOHj2ar3/96yxfvpxnnnmms5dXW1vs5zv77LNZvnw5Lp+b3LRM/vG3V8jLzWNMyjj0XJ3q8kPsa/Mz3prESy/8GUmWUfUIZikSt5+Y2Wxmy5YtuFwusrKyWL9+PZqmkZ+f3697SpSEBzGXy8WWLVtYvXo1ANdccw233347hw4doqioqMexwWCQYLDrp6G19QSy6gZAlql/L0/Hcat21vD9F7cwLWs7d5z7KmkWd+cxjaTzgr6MqpZRFNQextZtRBLx9e88sY7rbwCM+5ztyR6JciJ7wISTrP1n8Cz9E2awud97sxIh/dDlZBxYBMAO91oOWz4ic3Ij9uFtvfewxwlkpQ3D2BZowqYcIGNvGlXVmTHXs2IN/sNtBqo+y6bloL3vRpoxtChhLBYT5iA9RmSypGAzOinOuos9NT+jayqkQ/T5x4z5f537xeQkG5LRiB6OP+MjGY3ISb1HRM899xz/+Z//yZw5c1AUhdTUVLKysrj77rvZs2dPj2MzMzN54YUXuP7661FVFXtKCs///ilkNMY6x3PL97/NdeedS15hIefPnI2mRXDkZ3C4uoL09PSY/cSmT5/OPffcw/nnn4/RaMRoNPLKK69gMvWjHUYCJbyK/RdffMGNN97I7t27Ox+bPXs2Dz/8MOeee26PY++//34eeOCBXs8xEFXs//TEQ7R4apDlZMymDDLSi8gfNoriEcNIy0zBZOkdIFRdZ+b63dT5g+TXHCLJ56HNlkJlbhG6LCMRzVL8/OwS0OGchz5ggryKa2e9AcT+Rdqz+zwaG4dHvx4OYa47jNHbTMl1+zEmReJ+igy3Gdj9f6PQkKnMLaLNlkKSz8M4djHmyvLjfl0KPr8LS/NoSi/+DqAf116wI7ss96fCuHCSxXij7uixNSDp7X2MoORQMqM+/i3oEj7VwzsVT3QWrkbWKJqiU5RTgNGXhSHgpH7cX3tMd8vBFLL33MjKyp3U+yvIajKjtJ/MMaK113pWIGDDtamAkNuHwaYS8bVPIepS5+syrbyOgFFhT35mr0u2pWUw/VvLyMvJxqgoRIxmkCyYNBOyHkFCQ5EkrIql85YbPB9SVv9fhCJ1nc9jNucyZvR9ZGVd2uP51ZYWQhUVcV9KU0EBiiNxHzQ7hEMh2mqaUTGiotBsayJsTkdBJbc5QNAcxh/QmDlzJg0NfSernIhBWcX+yN3d8eLkPffcwx133NH599bWVgoKCgbikrDvKCU1kElYbSasH8Yjr+NLo8YWs4mwxULYYkI2SyhGBV1JwqA4SE7OZXFYJrT5A+xtXaPE1iQ7a+ZdTunICfxydD6KJPH2W+9x6a6nmfPN7UCM5aD235eRxZ/T2DgMkNENRgL5xVBVRtVn2X1+iqz6LJt9RRP4cN7leJOjP9CyrnJh7aeMDDyOYg7H/iDZx5tJR/daX+pXIB3fZ5nYleij9dzE1OFJFjelLvpvezv/g1tPo46cHj22BuIyoP1HKlZrkz1LO6fwtjZ+2BXAdB1UCeO7LaRPuwLF7ESSJFJcs3psEbE0jcGvtmGsWksuPXt5daxnJeX4MKToeFLH0tKajdXrY8bGj9mbk94jMcoSjlBS3UhOSxs6cCAzlaBR6eNnV8akO5F0HaMURpElJF0hrIejdTxkC2iQkXIR6cnn0xLchmpuxpKci9M5K2bFDsXhwASEa2t7jMgkoxFjTs6ABDAAo8lESl4anuomdCScvjRa5SZ8xkya7GFSW0LUe7wDcu5ESngQKygooLKykkgkgsEQbRFQUVHB8OHDex1rNpsxm48sDDYw6vNyKSvKIsM/jKw2E7meZDK9VozhCGooQMDfhj/ixa96CEQ8hLVyvNp2TCE/zWlBGuwq1qBCdpOZlLZWrlr9EjWTJ1O61cRjrUGCe3aQk9vW5/qUJIHF4sPhcNHSktMZ2YLZBbj3uyl/Hwrm1aAkdU1RhHwmKj4fzg6phH0XT2ISX+LWUxnZ0MAv9/+OvFA9rnQTO0pSom8MsarPx3kzsa1XaPRvQi8+vtqIcSvRi3Ypp4STRsbqX7Gd6QSkrumnNBr7XOfqIdaHnqCEcUMmvrMaMZj62EPVTgtLNOxOJXVUa4/fh+6FeNvUVrY2fkiVb1/n180RlQlVDeS0tBHa/less7+HrutIkoyteXz08tojZI/g1+seJNpqkqAGiryHyPDsIaO+AVnXyWv20pRkIWg0YA5HSGsL9KiCWFLdwNbCnLj3ZjHYMEgRVD1MRNfAbCA51UFy+xSaruvoQRVd05FkiUzzef0q2aQ4HMh2O1qbDz0SRjJEpxAHutyTYjSSnJdGW3UzOiZS2uxgd+NRUrEm6WQrKi5X/YBew4lKeBDLyspi2rRpvPjiiyxdupRXX32VoqKiXuthJ9vH45184pzXY0OmQQ/j1N04tBbsYRVHWMcZNJPuTye9TaF8/0o2jKnFZ+1KgbX5FebsTqOwzkbBjt1kGjNwhaJTBsZ+rk+ZTN12+0sSutFMcXobC5Q9JH8Rwu0wEjRJhEMWVoYvpmJUMuflf8w3eKvz28wpKkpKGzRCVmOIibs87BlmR3V2PbXcDNbNMv5ZWsyq9K0X1iM3P4GpJvY6VrzNyh1f6zsLUfT+GihG3c9yniKVppj7sTTkuEVx45FDCvmVFooqKticZaHRbsHXYkfabsFbk0R9RhZ2OZXR5+yNO+DTwhJ129Ko25oJukTNpqzODL+Iz4jVPQaXXI1f/T8aApWdhWtBZ1uxm4ixGSt5ZLe0EanZSs3OZ3GOX4zN0DWV5FM9vYJfXzYUNnHtOm/nRIMEpLfFrkARNjsIF17MsFQLrtZthLTuVTkkFMWMnGTCYk9GMRpjBhhJkpBiLE30hyRJvdLoTwaD0Ygtx4Fc14Jft2D1mtBS/DSareQHdZoaqsnIOrnJGsdiQKYTn3zySZYuXcqKFSuw2+08//zzA3GaY9Ls2ktGy8skmS1YjE4sSgYGQzqakkZYScVjdFJtzqIlxUlAspFX+Tbhyb1HKD6Lyprp9VywJZPCOhvVw53I+2uR6H+CRijUs5XDeEpZlLkFiP6SpbZEpxQ0QlyT/io78u294kHQJLOzJAVpt4esxhDZTSEsX3o5WOdAdYDSImHaLyHpEilv6ngXqniv6Gg42fU8mhMCzv41rEzSW1nIOyzitX5kIbavzou2KQl3FW8wn3/2eKz7y9xXUdzuWrDzKefylX88BZ+UckHdRkoNo/EeNlHps9PZFjwJpKCP8MEkfKFighc0kSZ1/cwEQiYOHByJe6MNm7/bG3+3DD8AL5W9rqHNEmFTSROHc/yAwq4RdbizxnDhhipSyjaw01eKt2ASVkMyftXbI/j156dq2yidJqfMT1/Ten1Px8es8uGzSE4fhtuRRpsUIWyFvFHn40i3kpaTgXNYIQGDkbT83B5rNqcTo9lM2GnF6g6AaiYUaCVkNdOYbCDdEzrVl9enAQliY8eOHXQp9ddtHYtXOxvdcpDWlAoabV4aknw0masIGlQCUghd8pMseXEaQuhqKPo7HGeP1caSJgrqrFT6VTomSr21NkJeQ58JGsGgjZaWrG5Pp7GQtdH//8gBDVA6Kin2L2v7VOS+4iQyG0NIgGNYgJE61G1xEPF3pXg1JoP3Qi1atS7eXrKO32gp/jRhm2TnVb7Fe/rlzOXjWFcVmwhkUX2tWR25EBrnOEdI5/btxaim76CavHjLD1CTbSd57GaMtqPv9Qv7ZZpKnez3jqbCPxybr43za95B1nUqcXYeJ+s6I+qaSAmGMYcjONsCyMDGOZfx/s6zqJkcxIkbN6l8ZRqPPk5BGq0yrLacpLZWiirLGFX+FdZgV1DzG1UO5Hpps6lM9I2hIJxPutlGk15KRoMHRzCZ6fWjUMwGdp6rktLowhz00xYKUB6uQtK6RpMemx2jGsYS9MdtRO43q9SlBalNl/mfq+GW1Tpp3m5Zwc40sn76r4z9xiKUPjbndiQenO5s9hTc/iCmYIjkgB3N6KbNkIbZ5B/UOcdnTMWOLIuCL1gE4SLSmyJkNUc/XWi6gooxOteuBdF1LzWpW1k5sXf/rk4S+KzRX5BLN33GnvxMdEC12jm8dRTF53wVN0HjQNks6Fa3sFCvwiF5Y36sdDuMBM199CKSJIIWBbfD2Dl6sxcESMkP0FZv4rXkC/nbsIksMz+LI6mPINIevND717DSSwqr+9sKRQSvqI4fgF4/GCoWgr0qYcT7AfrF7iApzePRfE0Ed7xMS9su/v6Nqzjk+S0Tmj9llG0TU5K2YzZ1TYGH/TLN+xy0lqd0ZuUl0cq4I/Z7+S1J5J09n7lWG/Izf0at61aqLCeH7HvvoeSSS1hGnHqiNgv/75ILSDMacIUiHG7cxro1DxJsbekMKNlJOdw1+y4WFC7o/L6l/Xj5NE2las8uWpubKDdYCRSOwrhnG/ue/N+Yx0vAkh/8goVFZup99WRemsm0B6YQ3LKNSH09hsxMbDNnICl99/o60zizM2g8XIuMQpLXhppcTYSTk7dwvM6YIObWzcy0NICkESZCQFcJaODXDPg0Kz4tGU2xIWGjNal//2gRJUhhQytfjRqJN78I3WjGr0NodzbFxZswW7o+hQaDNg6UzepMr4fo+1IS8bN/gqb+BYCAUUIFtljM1CsKmarK9OwgN7GKKzyfETQE2M3Rtyw4yuazvriZJvkoDSs7Ui2Ffuuo1r5Y/yvv65dyOFBMUluQCZVlWI0aNaFzqVMzMEea0dPcfFA0lVZzV2AzRsIUehv5ONXPtvABGpKaGDXSjNVbQJarmWTv2yT5PBhqAuxmNMk5Poy2SGc6ua7Hbo9qSLZjm3k2+dNmc/GsGRiV6FuCfsPN+DZ/EfcN//JMJwszHH13dsi+gNvHncsW15ZoILFlMj1rOspRmkTGIssKBROiTWMndDyYexGlSVY+eu4pvE1dKeAp6RlccPOtjJ4zt/f9zpl9zOc+XonofNGXcDjMihUreOmll1AUBZPJRGFhIffffz9Tp0497udNHZaFt7IeTbdib0vBbIxfauuFF17g4YcfRlVVsrOzefbZZ2Mm8Q2kMyaI/ct/LaPuwEG2f7GN+toGtJYAxlYNkyrhxES+bMYiJ9FgUdltbuzXc5ZUtVE9LJ/WwjE9Hm9sHE5jQz4Oh4uktnLktmm0+qfgth9AVrrml9swcUhNjfuvYAr2L1BUf+zk4ZEGVo/oeqLsSIS7G5u5yNdK41GKlnYwf9YAhmuhPxVjxAirXwx6iLl8wnKewkCEQNBGUZmXlMZm0KGNXBR3CbnBdJyKlxdn/hodjaK9o8lvnkFLyjC2jivGb0tmf2ou+1PBnpXOJbs+YbicSYUjlaKWSvTqQ53nTEnP5NxvLueAZKKxqZGCtHTOnzaJTze9Q31dFdawwuj8EhzpmeSPn4AcI6hIikLSUd7wFUnqUW4t5jGywqycWcf12vXH6DlzKZ41h6o9u/C6m0l2psa9p5MpEZ0vjmbZsmV4vV7Wr19Pamp0wm/lypXs2rWrVxBTVRWln6NOWZZRHDbMLQGCmhWTEntN7KuvvuKuu+5i69atZGdn8/zzz/P973+fd95554Tu61idMUFs/4GDrFq1qqsqiAHsRXYWLlzYWdcxEg7z6KOPktGWgTVixa/44+6xcnrh7L0B/nHF9OhjvRa0FFpacvCo+aS552EEXgsmk6x4sEph/Bip01JQmMAdyl+R0Xs8RWuFhYatduRCDc1J/I2jzWDeqbB8B7gVjU1jo5+aXIrCHVkZPOJqIFxnB68Ud62u43mUjfvJZgeMmNj/F1boSdfJaAkyulqjuC5CYUOI5HQbVSnX0Kp4cHtS6ZhOljUzya3FmILpAHxS/AqarGHzK4wrc1Pc+CmRiMzZn0vRDe5JGTjU0RS5c5D1uUgZZq5bMpoRU9JjvomPO+LSLp7/Lyf3tThJuo/SBoNEdL44mtLSUl5//XUqKio6AxhE6xdCtJrHX//6V7Kysti9ezePPfYYLS0t3HvvvUQiEVJTU/nDH/5ASUkJa9eu5Wc/+xmbN28GYOfOnVxxxRV88ckGXBX7Oe+qi1m2bFmvViw7d+5k6tSpZGdnA3DFFVewbNkyGhsbSU9PP6H7OxZnRBDbvXs3r7zySq/HW1tbeeWVV1iyZAklJSVUVFbibWtDQmJK4xQ2ZG2Iu8dq+WqVpowM/LbYBTIBkCQ0g4bNVM5erQCfpDPC5SIt6KHJnIIrI5mIZOCPkcv5ruHtzmWQ1goLVZ9GfzAdf5Np/k4k7nU4/m5A0SU0YOn7Gp+PltBlCV2SkHSdX6elcknZFKwHjIyb9FnctTr73w1IukTJxpVkXnUJ9c40MdrqS5wX8ppPvZRUdS8hJOOrH4tePwYFHT1jO7a2LExqEsaQAwkJXffgklZTcLiaMaXZjJLyueiy+RQPt1NVH8ZrziM5LZ3csSXUlXloaw2SZDeTO9qJLEevYTC9iZ/JEtH5oj9itWI50rp169i6dSujR4/G5XJRUlLCmjVrmDRpEn/5y19YsmQJO3fGr4OZmp9FVW0VTU1NMVuxTJ06lS+++IL9+/czatQo/vznP6PrOuXl5SKIJZKmaaxatarPY/7x+hsUtHrwWLvSZ/N9+ZzlOovt6dvxG7rWtqyqlX/5zMKcffWUD7fGerpefMl2svdv47l9r5IZ6Cq2W29x8MTkRTyUdx0A3zG8g6zp1G3p2KEvYd0mwR8N3XqFRcnN0QBmbW+WKQMZHhhfobO7MPrLoUsSaY3FtOWMpa0Z9uw2Ujzqc8zmrrLZIZ+J6k8zsZR6sBFA0XVuf+V5/v3Wfz1zswqPVksvxnpgij/CpVsCjK+KXQNPkkJUyTJq0yjsrWuYu/gaUvNHk2Q3k12cQs3eETGnw46sX5M/djDniQnH2/niePSnFcvo0aMB2LhxI1OnTmXSpEkAXH/99dx2223U1NTEfX5ZlrFnpsdtxTJ37lz+8Ic/cOONN6KqKldccQUOhwOjsX/LF4ly2gex8vLyoxYW9oZDbPn5nShpadBtLjnfl0+eL48GSwMBJYBFtZARyGCm6ws06rEG+mhR3k1a2R6mfvl/vR5PD7Twb5ue51ezb+ahvOt4KO0mrtHX823/yz2Os26TsWw3Ehqlozr0HnvAep3L0+0NVodif/syuCRF1+oah+FwuDCZ/ASCVvZqo1GVRvxjyrlg21YUXWf+ts189+9/5dmrFhEy9y9Qnzba6wkWs59n9VvxSL1L/kg6TDtYx0hXCwGDg7Y2E1PrdZK13v8eJlqxaF/S4GshS2/Dag9y0Q+/0yvpQIykTg/H0/nieBxLKxagvfJJ7M3ZBoMBVe3KZg0EYm8GP/L7AK6++mquvvpqAGpra1mxYgXFxcUndG/H6rQPYh2ts4/Gb7GSX1pKy9R0WpPoDFgSEpmBrqKgdrudKbfdRu1P7iCjvgGrz4ffau3xqV0DahwZ+Exm0txNTNj1N6D3spbcfux3d7zJ+pwSQuMzqdnRuwApgKRLmEuPPiqyd+tNlBHIwIT1iBPLtLTkcCAjl0/HT6bNEg1S7wBPNTdy/Xsf8+XYc/lsfNLpH8B0nWSfhwJ3A5Ku4wj4mFBVSrrDhclo4Huhv3K4bSFes4WqdCOSrpDeojCrLIRBM9FGOu9bw5Sa/WxIgWERiSRV4rzADkZq+ykeVczkH/2QurKRgyrpQBg4x9r54ngdbyuWPXv2MH78eP76178ybNgwcnJyUFWVgwcPdq5lvfDCCz2+N14rFoCamhpyc3NRVZW77rqL2267DVtfSywD4LQPYt0/jfRlb3YLD10p0Wjf1PmYNWJlSuMU8n1dJVcWLlxIWkkJu3+xA+eKZ5m+ZQufzpvXOfV2ICOXT4u7gsOUfbsxReL3DJKBLL+bklAlX1iH0+g8semilm4/P9ZI7OoCBzJyWV3SO/Os3pnGo9/8Rv825A5l7dOBF+3exPjDYYyqjUBSNbocoSPIy2o06WJMMDrlM/0AgEqIMB6jm2HWEM8bnLja6xnpEgQNcF7bNkzNn5K76PvM/NblgBhlnUnOciaTazZSGwzHXBeTiGYpnuXs3/tSX06kFYvT6ezME8jPz+dnP/sZM2fOpKioqFe3kXitWCCaIXn48GFCoRBf+9rXWLFixQnf17FKeCuWE9Hf0vvHQtM0Hn300fhTirpOveEA/yzYGv17j8X66B9nuc5ivGF8j0xGgHUv/Ddp//kMVcPy2TJ9OruGF3cFh/bnufDzT7nvmd8d9TpXfnM2j5z/r8iaxku/+CEZ7ibi786I7/7rZHYXyki6ToY/k3PrzuvxdQ34y5xLaTNbTv9gBTHvxxwKct6+bYx01ZFRPy+aXIFO2NSCJoeQNVNn0kXn06DTYHAx1rcZpzGN4pRZmA0WtqPSiE6S6ifS8CE1/ipmL1rK/GsvO9l3KgyQeK1C4unIToRY3cRISHbiyXLo0KEzsxXLYCLLMgsXLoyZnYgercK2Pf3L6N9j1X0C9g/fz+OLH8d4RHfVc2aMpWYiSDuryK6u4fkVv+v1PI2O/o2s8nKjdRo1WeZ3S27mgaf+Fw16BDIdUCWJnaPG0ehIJb2lmUn7v0LRo7XkGlNgT4EEOuR688l0T8AvG7Fo4c5foBpHRucoMaahFMA0DeQ+Qn3757MZB3ejt9dyzHM3kOduQAbsrSWdgUpCwhRyxn4adBzsIFj/AdVANdA8ejs5zuHMGHkpTXU2GkIeHNmLWbJgNoY+ShgJp7/LM538aWJRzH1iv0zgPjEh6oz4bSspKWHJkiU994kBVp8Pe/VGWkb2XXG9IdjA6s//xmVzvtm5nqHueoMtK7+LZ7iJ3J3J7CoeS1OMgLVj1DhczrSYIysdaEyy4HcqJNlcpKsuGuUMPpk2m3+/9V+5/ZXnyXI3dR6/es45PP31a6lP7UpfzWxu5LZXnufcbZ/z5NRLSa9KZ2ZEI6nzM2AYXW9P75XAZxrcJWT6RdexBv1cv3E1tY4Mqp0ZNNuSqXFmEuh2f0lBP/PKdjCyvqbHP6+kmUhpHYU5eJTKJNGToQX34/J90P43iJhNpJ1/IxfM+BqSpDB463sLp0q/KpoMAUVFRQM6CkuE0346sTtN0yg/eJD999yDqbaOjPp6PhsPv/360Rfa769xk1I7jhHfeoDybC8PfXQHdYqEpOn88Tcqn0+cy6+W/zDm987fuokHnvpfdLpGVrWOJHbnpRMwdY3u9CSJN+d+k9KRJSDJyJrGpP1fke5upiy/gPK89oTrHlOe0QA8cc02QqqBC4xlvQ6Brlm1KkcGK6ee089X7BQ6SoPHS3ZvYmRDz/RgTYcaZwZhNR+b6iW3uRlzyIHFl4dq8sSdKox/CRqRwBeogU96PH7Fv97D2LPmHf+9CUPKsU4nCsdGTCceQdXUuLXaZFlmRHEx6d/+NlU//gkAqf1sXFqgB5nl2MRbf7yDP47WqItuUkeXJd6dJTG2Nn4F8SNHVrWOJLYUZnd+XZPaKzLYkpm2ayNJeNlWfBaaLLN9TAnoOrZge9przN5dOjvnTWZe2ZdUhzLIbWmIXeBDh+ZGGUMgSMRsOjVThye45pYU9DNv/45oADti87cMjKqykOx1AA7oNkZS4kwVHskQ3k8o3EJWaxW1SftRu2VC91WTTxCEU+O0CmIflH/AQ5seos5X1/lYti2bu2ff3aNqtv2SS+A3j1K34kHGV9SS3qrTmELMN1dJ18lWVWYEgwBckF3G9t3TOZzt75ywe22uzJOPfUVmcyP1ztSuXlrdfDJ1FjtGTeMX//c5zcpbRLvCS+wbUcKH8y7Hm9y1Hymlzc1l6uu8K18FksK5uzbzz4l91J+TJDArfFoyDYCkgJ95ZV/2GKl03Fqx0sjevS7qJw/r+8VMtPYR1ILdm7CGw7SZzPhMFursqVSmZRPutt5oCQU4p3R753F+kwVrKICj3sWYinRCqQb8MXoHWtuGkewdeZyXp4Pmwet5iyl1NUi/cJNnzcUhL8PIKJEeLwiD1GkTxD4o/4A71t7Rq2W5y+fijrV38Mj5j/QKZCkXXYRv8xf8cPP/cT8f9BolSO1vvIvqk3hbPZss3Mw2fMVYuY3sJjO16dHApssSf1oocdsrz3P/rf8aneLrHsg63sC3B/DnNxGujL7s+0aU8OYl3+p1Lx6bg3dZBJKEIRLBeIwDlzazhdUls2NOuVkllWvcX7D5kIfNI0riPEPida5PHXE9VPXcV2cLBcltaYiZmWlvnIjJmIbJCzZvEQFbNaoSQFHNWHz5yMeVz9nV8j7sW8OI+mbG3HYNKbMuxOmchSSJoCUIg9lpEcRUTeWhTQ/1CmAQzSyTkPj1pl9zQcEFPdpAdFTqXlBfj+ep1Ty/UKKhW98tR0QhUHcl/+05u/OxXBq5wvYB1uDeHueJFt79gp+98L88f+XNPZIv7D6NS7b6oiWJkqOJGpok8eG8y9sv5Mgpwq5WJxd+uQHzsb43t3//p8WTKGqoifnWPv3wPvbkjoifan884tQTnHloD9MP74sbYmQgv6WPxWM9WizXFE7t9j0SNl8+sqkVLWSnP31+DYBD9tOoyiB1S3DRPWieD5nWtIfp9/x7dKQuCCdI1XQ2HWzC5QmQlWJh9og0FHloJXYMBadFENvi2tJjCvFIOjq1vlq2uLbEbAthyMxkzj6dc1sCNF7mpV5ROBgezcOtP0M/4q23llT+mLyEXP1lYFuPr20aK/P56C08/fEaXk26h9bkyST7NYY3RJA74qseLdhZmVvUYwqxl/ZgEDaaGN5UjSUUJGA8hnUsSaLNYqPGkREzQMjAvLIvo/vajpJE0VfXYUsowOi6SsyRMHtyi3qk78cdfR2L9ktIbi3uuW9LB01tBlKJFcB0zYcaLsOgB3CYcjDqjbwhudHTk9l/6ULuNjnIr6olVFVGptVLwcTrSZ49SzRJFBJi1c4aHli5m5qWrhJOuQ4L/35lCQsn5ibkHAPVT+xYLF68mM8++4yamho8Hk+P4hKlpaXcfPPNNDQ04HQ6ee6553rss02U0yKI1fvqT+g428wZGHJyCLpqmdrUjGQJMT/07fYA1vMNMvqYTq1vIUn6diSpa/Qn6TrZOsy49TGqKiZS/cbhXucKtM2izfAlXlv/in8eyMpj0+hJPVLHj0VfKfUjG2q4ZPemHhVGujOHQyBB0Nj1HLagn5KaQzj8bb2m/qYf3tuvacE+HVGtv6NdSfd0eF0LoqmNKMY8QGdW0v/hkGvx6w4sciv+cBCjVs9BLZ99HvD7wasksbvoW/zujguZm5bSnupcCMw51isUhD6t2lnD91/c0rsVS0uA77+4hT/cMD0hgWyg+okdi+9973s8/vjjne1Yuvvud7/LrbfeytKlS/n73//O8uXLWb9+fcKv4bQIYpm22PUG+3ucpChk33sPVT/+CXVbnFScnU0NfbQSkCT0iBPVNwJD0oHoQzpkBDK5Zcy3OWybwpULhvOnD6qJeMM9RhBVBpnNafOZ4NvXr2suzzqxXUi2ULDPr49sqKGooSa6CbpbEkVSexCC/q1XQT+mBY+kSXQNUdvpCjbvMBTV2isdPrp2pYFkbA9gGpc6HmaUdT2qnoI3chURfRyqksaa2lIqffs730g+ST+HX183h/npid+6IQgdVE3ngZW7+2zF8sDK3VxcknNCU4sno5/YoUOHOit2LF26tFc/MYAFCxb0vjjA5XKxZcsWVq9eDcA111zD7bffzqFDhygqKjru+47ltAhi07Omk23LxuVzxVwXk5DItmUzPWt63OfonrH41d406EeSmx6Jjqby2vKY3jQdc8TMvuq9NP3tU1KB4txp7PMUoUty5xvxfoNKmWUkWhtImoYuSX1P5cFxfz0p6O8MRH05WvA5psB0DOzuaPPNsMkNgDHkxBRyxt3DFa2crRB9O9CZn7SOLONw6kOXE9QmAQptEQ9bGz+gyrcfAK+SzJa8c7lz6TcSNo0jCPFsOtjUYwrxSDpQ0xJg08Emzi4+/p5bJ6OfWIfGxsaY/cQ66ifGUlFRQV5eXmf1GkmSGD58OIcPHxZBLBZFVrh79t3csfaOzjp4HTreEH8+6+c9kjpi6chYrHx3Paxr6fNYgIu9cxifNQ/Xweh6XH5FJdO3bMHm72jRsoYci4ntU75BXeo4qiUDX5gsgMT+YePQ+yqZdLS1r46v63rchIp5+3ccZ75eAnT8E8TrJK2ZOwOWOXRsRY+tksREq0Ka8SJau+3j+o3u5zVFJcc+DlvScGSbncsWzOX1BWPFgrpwUrg8R29jcizH9WWg+4l16KufWH+vD7qygBPttAhiAAsKF/DI+Y/E3Cd21+y72Pny63zR9gpGs4RkMIOSgkG2YTTaSbalYbYmI8kKsqwgSTJOE7hDR7ZT7qBjI0RqoAHXnmiKuFc2YTQns6dgBFNL96DoOpXthYH9NoCvSAEWa0ZeD01CPeaUw9gmVZZxIDM/TkJF9amrhajLIGlxO1Intxb3/lrHIboOupcUWcJhSKbAKCFLEkEkLBKkG6QevyA6ELIqLLp2GosUiQZvUGSDCadEVkr/Knr097h4Bks/sXgKCgqorKwkEolgMBjQdZ2KigqGDx/e31vst9MmiEE0kF1QcEHMih3h/a+RZJyKL+LBH2klqLai6XVEpAARKUhI0tDb/w/gQnMhr9kvBo7M3It+fY6xAlnu3XoFIK3VzQ2r3yCU0Tv7MEkKM99wkC89mVT3te7WT0WNtZx9YGfsdavjCWDx4vaxfL8mk14/l7C5Ca99P5oS6vyyrJlJah2JyZ8CUu9fro5Pa8VGL5NSev7A67oOEr2mGyUg75oxFI/tTy1EQRg4s0ekkeuwUNsSiNuKJccR/YB1IgZLP7F4srKymDZtGi+++CJLly7l1VdfpaioKOFTiXCaBTGITi3GSqPfMCWdLUUWnCFI89tI9+diDxpwBA04AmYsESOyLqHoErIuU6xDph7gpbCRVr3rZTLLfiZY9jBcjXAgPT9mX66mFAe/veZmLtm1iZGNPYfrkgSFSjO2Bhervfl4k+xx17Sk9l1uR1vzOvo+q36WetK7/Rlj9HTU4NZ+nL11HDIy5mAGpvr0Hi1ODEE7EhJh30oAjLYLQOrK1JT0NkYYvCTpbg56Wkky2HGrPl5TDHitefwIC1ndLkRxmHFeORLrRBHAhFNPkSX+/coSvv/iFiRit2L59ytLEjJDMBj6iV111VVs2bIFgLFjxzJ69GjWrl0LwJNPPsnSpUtZsWIFdrud559//oTvOZYzpgDw4ld+RWXqWDyKEzepqFL/4ndxXSU5ZZ9SmlRH2NSIYjuIhM7Cw5fz2qyraDNb+wwy129c3WtdKi3tMPn2raz+6uKuih0x1rSmVJSyvWB03K/HqsgRUz+DkKyaMfszCVpdPUZPkmqIjprkbgtQWjRZRZfVHt9/ZDp8r0vRdVKlLUyzPkWyEiTTAhtafk2bqpGkmBhpzeT9SAPPqk3YVB8+xUa1JbezlYoMXJBk5dtThzGtJAvzCAeSmDIUBtDxFAA+GfvETgbRT2wQkWsOE2rZhCz5SJU92MwKZoMNg2zDJKcgyUYkvX1fmCQjIaPrYdRgE/vyo5XhDe3vlRn+DNwpw2iz9NGGu4/Nxl5vOuEsBxNc22G1zofzruix8TnZ72Xuwd2MbKgh29Pca7qy35uIdZA1E0mtxbTZy44ITEbM/kwUzYKsGZE1c2c6e5J3RK8GkUDnY5JqxBR29ngsXnX4js9IkiRhxMsFjj8w2vZZZ3JlY/heRif1rOP4jimZKmL3PPvF5eNZOm+EWOsSBrWFE3O5uCRHVOw4Cc6YIHZ16QwawiPQNT+63opkOkzYUk2bxUObKUhYCaJL0a1LuiShSaCoGp/mlkezurv97FlUS7/7csU6LhSy8dVX5zF82ibGrNtN8aGvqMotos2WglnXSTeaOte0uu/jOqZNxJ0JFNG+Web6jKMGnA7xGkSaQs4eQanjsb5IEgw3bWFK0nsMM32BLGkAqGTgDt9KQOvKcNJ0nXpJZztq7+chupYgApgwVCiydEJp9IPBUOgndsYEsWTFS1NERldSgBTQ8jH7dKy+CFmoSO1vrkDnXHal/QCBgt6bkgNKAJu/703EHWJvNo6eoc47EWaUEdxmZHj1QXSgbdTkaPzpNn0Ye82r7wyMIytd9NW5+JjoHsK+taAHQEoC3QeAwbYQWelZhcQitTDN5ibLOAv0WTSGdyHTjEYqQW0C0U8HURo6kgS/ofeCeKLXEgRBOH2cMUHMYzUzPlyKSbGjyCYkWUOXdCJ6mLCuoXXbXaYTXXZy26piPleDpYEZ9RUkBXxHXROLv9lYIhhMwlGk8FTBDeQFasiRW5hkjJ1d1PO5o9+PdEQga78Bm7cQW9vwfjV+7C9d14kEPkMNbIIjw4yUgiRbAZ2xlg8ZZtpBstKIia8T0rtGWkFtco/n6/6yBS0Gsr8xmuuUCKVHrCXkDMG1BEEQTo6EBrF7772X119/HZPJhNls5qGHHuosT3Kqfeuh2wEIBwM0V9dweG8pza4G2hrbCLb60cNae08pHa3jT7n3tBYAEuxI38bcspG8H6uAbsdm47KjbzbONjfjtLRQJeVhki3AgaPei6QbsPiyCdrq0LolW8iaqXP6sP1COLF8+fZn0TXCbe+ghUtjft1oO58UpZlz7M9QbNlARO89VdjrHkw6lmIF6+RiFIelM0FjIYi1BEEQ+i2hQWz+/Pncd999WK1Wtm/fzvnnn09NTc2gaultNFvIGjGCrBEjjnqsqql88Oq2mBXyq5Oqwfsa5+8J8fnImT0SL4yRFrJcrzGy4eh7QcxmH98a9yqPb1+Ov5//HCnu8ZhDqSR5R2Kf8ieqtxiwOZ0YAhehh7tn8XRMjPYvAMTbsxVuexstvL/3tcsak9IlRiS/gS5dRpJ0DvWhq3pNFeq6ju5vJnzoTZyLrydp3ow+swpPh7UEQRBOjoRWJbrsssuwWqNv5pMmTUJV1T4XBYPBIK2trT3+G0w6yllJSL1m0NCjgSxr7+t8+2+Pcc0HL7Pwn2+SV/sgzpofE4qswaf4YtZy7HgCs7kNh8PFjOwv+cGUpwkaVdp0I3E3PejRNPaOtS0JCX9pKsbWZtQaBS2UwpEX2t8NFDF3Wugewm0rYwQwHasc4rujPuXc9HWsl+dynz4Wv3Ye/vYaht0uGUmClPOzGPn6H0m/6SIsxU6RFi+c/jQVDn4CO/4e/VOLM7NznMLhMA888ADjxo1jwoQJTJs2jUWLFrFt27aEnqcvixcvJi8vD0mS8Hq9Pb72ox/9iKKiIiRJ6leNxuM1YKX1nn32WYqLixk2bFjcYx588EEcDkfnfwUFBQN1Oceto5xVurFnfb+koMwFWzIprLNhamtlbM0Wrsx/nV9MmEW0EBVsT98O0DuQtQeMkcWfd7ZymZH9Jf917gPkDt/Z/tiR3xP948i+Wr46DZCim4aBI0ddkiR1Bihd1+PXL9P9hNtWEvK8Qsj7DiHPKwRbno4ZwAAuzt2PJGdQEb6be7Tp/JMIv8BPwxHXbXCYSb+hhNSrzxa9uoQzx+634NGJ8PwV8Ory6J+PTow+niDLli1j69atrF+/nl27drF161aWL1/Orl27eh3bvaxUIn3ve9+LGzQXL17MunXrKCwsHJBzdzimzc7z58/vtRO8w9atWzuD0IcffsiyZct4//33GTt2bNznCwaDBINd2Xutra0UFBQMyGbnE6VqKm+t/TPrPnqdFFM9Myc1EWkzEfEZMNgipBc6GDv2PizbJF77/U94boFMo10iry2PKY1TsKlde8qkcBCz6zBTF32OwaL2ygtpaCigbP8sQqGuKtG9NxJrJMuNeN0vEJKGYUpZctR7kAmhahqS3DW9q2s+IsGtRyRs6NgUC1PSFrKt6SP8qqfzeJtiYU7GcIYlFxPUSvgFIf5Jt3U5YAoKd84dwfQJ2WIzsjDkHfNm591vwSs30Xv6pv33YMmfoeSqE7qm0tJSpk6dSkVFRcxK9ierFUvnnUlSr6aYHYqKinj77beZOHFizHs5qZudP/nkk6Me8/HHH7Ns2TJWrlzZZwADMJvNmM3H1+zxZFNkhW9cuIyvn38TVXt24WluRCmqwZ6ThMWSjdM5CzTYv2IBc2p1Zu1T2VMg0ZxcgdNbSUYgncN5OdRaFRSfBwmo/CSHooureuWFpKdXkJZWyZ73LiMcmoukmTCFu7coiY6+zrE/g2Yp5W1X369zhwvtvydDGcY/m+dyuG0nmtaCFqmi5y9bdA1tWvplDEsay7CkMTQEKvGrXqxKMhmWYciSTIWm8RsCbLdK4O/67myHhe9eWcJckUkonIk0FVbdRe8ABp3r06vuhnGXw1G6avRlsLdiOZkSmtjxz3/+kxtvvJE333yTKVOmJPKpBw1ZViiYELv4ZdvmTURqa6PH6TChQsOWGcJgUYkQIKfSwbvD2qf3JImWg3YOvQ/5c+sxJXdV0wi3Gaj6LJvw4YPIRgWj7YIeo5lkubEzExALzFV3szl4+VGvPVlpBGkOs5xFzHAMZ497PftaGwhpXensVsXO9PSLGJYUDYyyJJNljRbi1dFpRec+2tiGigb85bo5yLIkMgkFAaD8M2it7uMAHVqroseNmH9CpxrsrVhOloQGseXLlxMMBlm2bFnnYy+88ELnC3e6i9TXAxJKxmhseVZSi6qwWXd2bqRW9W1obZPY2dBClc+BLsm0HLTTWjGGpMwyDLYIEZ8Bb60N9OgPqBbeT7ClDNmQT5YtifMz1pNr2tNZ+QJglnMDe+obaNPSiL3MGZ16zDXtoTF8PRANThNS5zHeeXbMkdaRmYpatBQxvybAFlQkorXgzipOF0FLEDp4e2cyn9BxcQz2ViwnU0KDWGlp7H1Eg4Wuqvg2f0Gkvh5DZia2mTM6kw00Taem1E1ba5Aku5nc0U7kfr45R8Iq6z7ejG/tViZe+iCyNTrEbwZag/WkGR/ErOxDkVqZkPwpE5LBE7bxcfM17GtqJElJwVvT19BcR4tU0uzVyM3bxZGXJUsa8+1Ps8r9c6JTjd0DWXTqcV7KM2iktae/d//e6EhL13W0SIDw3nfRDUaMYy5E0bp+POrR+Q0B/klEVNAQhHiSsxN7XByDvRXLyXTGVOxoXb2auhUPdk73ARhycsi+9x7qM6fyycultLm7kkySnGbmf3M0xdOyej2XrukcLnPx/pq3aHFvQ3f7GN2SwfTkK7tnlwNglPdikvf1WvdKNvi4PPMFpjvuIayexd/Lfwf0/QkoqMlU+RwUJPXsOq3r4DSV0mTZSEFoFG1aVxX5ZLmReSnRqcfG8L3ougzE2A8mQXNqM9qSc5m0+GIMBgPBgy1s3e3iT9sqWdPmp2PsJypoCEIchXPBngetNcReF5OiXy888am4wd6K5bbbbuPNN9+ktraWBQsWkJyczP79vfebnqgzohVL6+rVVP34J703TUkSrowp7JzwbeJtCL7kOxOwpCl8vvkLKqu2EA5VoHpD2Jo9JHvAaSgm2zqS3KRRKMhHlHpSyTEvR6EhXmUqVD2D2tDTfOJaQ3XbF0e9l6/lfcV4R32P5wA4EL6bi7XJXCxt4gdsxKopJMnN5Jr24FctbG+exw7fcA44pvJNSyFZ3UZrss1A6tWj4/bkUjVdVNAQzljHn50IMTuKJSA78WQRrVgGAV1VqVvxYHRO+Miv6VBavDhuUQsdnVV/2gTKX7D6QqRrDpIM+WRY8kkyZ/JViYePCjyoOLlne+9MI7O8C4MU/x9fksAgNWCWdzE2ZUy/gliyIdTj7xopNId/iFmbyxTaeE+fzfvMZLa8hyypGc07n9JgNl5DMtVZ0b5cH9PGyyRjsBpJmZdHyoXD+0yDFxU0BOEYlFwVDVSr7uqZ5GHPg4UPDZkANlSc9kHMt/kLIrW1McdZbucogpbUGF+JkpCQ9CRKDN9ELQiyJ6uZ9elhDtgtHDKn0iSPQtEjXFdV2fk9uq7TGNEJ6GBXVNKNco8kjFgs0kYyLMuxKik99mT1pJNiCJJv6zmV2BS+i6A2FYD09rvUkNmgTYgGZmP7f533BPfMG0l2SY7YwyUIA6Xkqmgaffln0SSO5OzoFOIJpNWfCqIVyyAQzRiMLWjq35Tlo1NS2FgUrbVo0MMM0w4xIvA5w0NfUddaSn3LNOCbVIc0dvhVAp0zCBPZ1PYk8+1PR9Ph40g2vElQn8D09Iv41PVGjCOiT3hB9oHOpA5dj/bkCmpdmZ+N7cdl6RI/lMz8liD13aYzcmwm7r96oljLEoSTQVZOOI1eOLrTPogZMjPjfs0c6l+txhT5K2a07MITLKfF10wokokeSWJCo87Cyusx6KM4ZFPZ7u894mrT0ljl/jkLnf/VZyBzGp9iWNLTzMtaxJbGD3uMyFIMQS7IPsBoeyPQtQ7mDt8KKGjo1KOzXY8AEt/TzVwgmTgXI9tRabYqFM0bxjkXioaSgiCcXk77IGabOQNDTg7hGFOKTvd+zIFmgmZnzJ5gOjo+o5dm72cU1kYoaCwgyf0vyFIehvb6hipmFCnILn+YXqmJQDTdXWNd6y2MMG+KObUoSWAgujY2LGkyebbR0b1bEQ82Qymjk18iuVufse5dkTv2b/2GAEm6xLXhVoaHKigfl8W0+edwSYpJTBsKgnDaOu2DmKQoZN97D5U/+nGM/A2d0fv/xs4J30FH75FZ2JG0aQslc+WOG5EllYhuRpcVZCKkGRpJN4RIlmzUBnxU6sP7uAoZr5ZJTWg8+ebexTm7jmqO/tlt79b6mn2sqZ1Gvq2FAls6w5POAXkWHQGzTdN4PxRhhOrje4qX/XoFcx/8ITZ7StzzCIIgnC5O+yAGYL/kEuqmXoRzzxdYgu7Ox4PmVOqyZ0ebJMeo/g5gknw4lRbsSpgU2YisGfGFm3CpbuqcRoZ/fSKpWj6Vz8YujNxdmxY/iQRAo+fX/Qc/YuKX75KXZCFoNNBocbNywU4q7WVcUjUZU9toGiMSEyyVtIXdWK+Ywk2X3tjPV0UQhIGkaipbXFuo99WTactketZ0lCGW2DEUnBFBDKB2xHjU1LGktDajhENgSUZNG06hLFNAHegSXtVERFcwoGHVA6i6F5/uo0X1U2+RUEdlM/n8GeSPGYMkd+2zqtrb3K9rsMmxj+tK0uhZTaNq3Dhem/8tGv3p2JKa+SrlEwqaZObuuoJWyUq6wcVUc5hDyU38y3/djtFkOv4XSBCEhPmg/AMe2vRQj4a62bZs7p59NwsKFyTkHOFwmBUrVvDSSy+hKAomk4nCwkLuv/9+pk6dmpBzHM3ixYv57LPPqKmp6VHFPhAIcO2117J7925sNhs5OTk88cQTFBUVJfwazpgg9q0HbwMgEg7T4nJRVVpGW6uHgM9HoC2EQVFIchhJcjhIzUons6gQS1Jyv2qE5Y52YnOY8LUEibXhTEfHbG4i17Sn15TmkUka0cd0PKYw3z5vGLKUy5iWv7KvvoYlmxeSEpqALKmUWKuo9tcw7PtXMXfC+BN7cQRBSJgPyj/gjrV39Ooj6PK5uGPtHTxy/iMJCWTLli3D6/Wyfv16UlOjszgrV65k165dvYKYqqooA9DP73vf+x6PP/442dm9y2jdeuutXHbZZUiSxO9+9ztuvfVWVq9enfBrOCMqdgwUTdP5+KMNbNnyCkptMsa289sjVLe1tfb/PTz1Fb7hWMW0g61YQl3JHRG9K0kjemT0O34+1UqjcwONtW9xds0oxh/6Okgm8o2VpOohWkbLXPmjZYOqEKcgnI6OpWKHqqlc+uqlPUZg3UlIZNuyWXXNqhOaWhxK/cQANm/ezLXXXhuz7JSo2HGSaarGxk93sOHzv6F6a0mpaqYwXMiwlJHsynPhbk7DEujaXewzt7I57zMmHnCwpfUJ9mbuZ7h5G7maD19rPimGC7DR9Q9UZ5Z4eqyfRuVxQofgxi9vwqTlYJZ9lFjq2R+q5Px//w7OrPhbBwRBODW2uLbEDWAQ/ZBa66tli2sLs3JmHfd5hlo/sd/+9rdceeWV/T7+WIggdhS6rlN1qIEP1/yDhsataB4PKXVucoKZ5NgmoWTmsGpsHb/LtvKVaRxOtYnZldsINpYRwcj0ShvnH7gIXTJgkdoo9jhobZjMO3m7SXMfZqT5AJWj7GzKzOOgJQXMq6mo/5TLd08hv+EidIwUmw+jhb1o8wq5cbFI3BCEwareF7+4wvEc15eh0k9sxYoVlJaW8sQTTxzrLfbLGRPE/vHWm5QfLiczcxTFRaMoGJ6L2WyMtluRIOALU1fXQFVtLZWVe2h270OPuFH9YaxNHpI9GoVyIdnWGchpGawb4eLlPDPbbAUEpLGMiOxjesuTVDV+hdeXw/wDZ2FWx6FjwKJ4GGP20BpoZnNSObJSy6hDwzANP4dHZobZZJvCiMg+jPWPklabxvKd30cmGbvSxChDgFKji2/++keY+lN8VBCEUybT1r8Zkv4eF89Q6Sf28MMP89prr/HBBx9gs9n69T3H6owJYnVr/05Sk4ZbWcN6o8Q6swmtfU5alyRkVcUQCiNHIljCJvLIJMmYjcOUSbIliwPj/GzOd/NlupU9lmH4pTFkaHVM8H1AU+vHhNqSGdGUwmVlt2GUk1ExkSw3U2z20exvYp8jSHPaPnLKQhSlXsKrF7XwZk60lNXM1ueodh3gqj3zcfqmA1BiqaIx0EDqTedx45yb4t6XIAiDx/Ss6WTbsnH5XL0SO6BrTWx61vQTOs9Q6Cf2yCOP8NJLL/HBBx90Xt9AOGOCmM1UgG/KCApbU3C2mTGoYdC0aOaFroOioCcb8Zt0quytlGZ4OGyHapuFgxYnDXJ0WJ6vljPBtxpP2waaPBGMwUyu2TcPS2AeigSabCTLWEW+wUi1v4qavGTK1X2kHHQxSZpF+VQHP5uQwkHDFCYH19FU+ya5riIWln0PSbKQaaghWwpRl+njmnv+tUcqvyAIg5siK9w9+27uWHsHElKPQNaxF/Wu2XclZL/YYO4nVllZyU9/+lNGjhzJBRdcAIDZbGbjxo0nfN9HOmOyE6//+//wYfpFAJj0AE7djUGPIKMjoRGUzHilZHxS1xDcovvJ0mpIDx1AD+yiwbePSMDBiKCdWYeySWm5CKNsRMWMgSBF5jrsuoGySAWjLpvJZ3s/wlB5iALPcJy5U3hqVitrU2aRo9eQ3vRnIo0SX99xKWZ1GEYpRImlmUP+Shbccx3ZhYUJvX9BEI7PMfcTI/Y+sRxbDnfNvith+8ROBtFPbBCpdpcxXv8nVkMmspKOpqShSwZ0ZEBC1kPIWguorYTURjyhSjy+CGEtDS2STEkzFFUsQlEnYpR1IphAlsg01pBnkPEFW6g2tjDpe99k26o3+OyzF8mrsVCQdjmvXdjE27l5+BnJjLbXqKz7grkHJpPTFE3cKDIdxqQGCUxycv0t95zql0oQhBO0oHABFxRcICp2nARnTBC7ZpuRZoeDhqRWGpK8NJkOo0rRwb4GmHQJW0THEdBIb5PIbpqPwT8RSUpDkXVkNCJYkGSVTGMt2QYJPaJyWK0mNG8M865cxvPP/5GX/vwfZB0OMSnlfD49O8h/FzupUiYyMbgBb/1rJDfmsXzPD5AlG3almdHGAHupYfGvbhf1DgXhNKLIygml0Q8Gop/YIDJcGYXSOIbspjAAmq6013+PzlNLaMhoIGnoukIECyjRx1OVejKMQVIkK4Gwh0q1npaJuVxw3dVM0Yz8+bnfs/nXPyDzcJDxSfM4MNnIv0+wscs0ixGRfUxwPUDQbWTxritICo1DknTGW6to8jeQfO1cbpov0uYFQRCOxxkTxHRdYpLVhV9XCWgSfs2IpsvRvA4kFEnFLEUwyxoWScImmdE0iWDEjUtz40qSybhgJjPOuxqD0cj+ryr5w5MroL6GzFqNiclncXiSmV+PN7DFMpl0vZ4Zzb+jurGOhWUTyHRfgI6RXGMlaYSpzw5x9c9F4oYgCMKJOGOC2Pjrz2bPp5/jdXnQvGGMqoyMhKRHs4ZUScMra3iMMkaHmYKJIymZOxtnTk7nngi/J8gLf36Rxrr1mGtayPekUWg/n53Tw/z3aANfWKbgxM0MzwtU1O9klGsEXyv/AUgmkmU3Y8x+SkOVnHvfctJyetcaEwRBEI7NGRPExsyZwZg5M47pe3Rdp7aimbf/8QqtLV8iNXpJcwUoMU0mI2UUH5zTwOMFVvaappGmNzDD+xcqXNvJbMnn0n23oUg2jHKYceboni/p/Anc+PUbBuYGBUEQzkBnTBBbv249VVWNFBeNYXhhLo40G5IigRYNVj5PkMOH69h/oJSqqu0EAuWoviDWZg/2FonRxtFkW+dweLzO30d52eRMp0Eex3D1ANPdj1PZWEZ6ax6XfvUDDHIyuiQz2lyFFAnQPMLA13/4EzF1KAhnEF1V8W3+gkh9PYbMTGwzZyANQCX5M90ZE8S+fPv3KPVtrDdKfGI2E7aYQJI6ysxjCIUxBgIYgxoOLYU8wzDSzHmkWLLYM8HLu4VedjhtlBmKMRNkTPALcrx/oKElyMj6dC47dBsGKQlNVhhuqsKh6xyy1rP4vtuwJA1MuRVBEAan1tWrqVvxIJHa2s7HDDk5ZN97D/ZLLknIOQZzPzGASy65hNraWmRZJiUlhccee2xAruuMCWK5/uFYCieT6jVjiISIBALRemJISJKEIpvQk824s8PsS2vm88wgB+1WDlnsuOQxGPUQIyNfMb31GarcXxIK5nLe4RwyG65EkWQ0yUCBqZI0JMqkaub8/CbOGZZ/qm9bEISTrHX1aqp+/JOuZoHtInV10cd/82hCAtlg7yf2yiuvdJabeuONN7jllls6q3sk0hkTxF6bm8aqrHGd1TocqoSsa+iShI6ET7bRJKfil6LlVBQ9Qp5WQV5wD3mBl6ht3U8gnMMYt4lLy5ZhIA+QkSWVYksNSZpMGdXM+un1zC0cfmpvVhCEU0JXVepWPNgrgEW/qIMkUbfiQVIuuuiEphZLS0t5/fXXqaio6AxgQGe7k5PVT2zBgvjVR7rXS2xpaUEeoOWUMyaIVformOZ+DEXJQFPSCSlOdCQkPZpknxSpJletR1Mb8IdrafbVEQmlE1FTmNwg8bWKW1AoQJE0VEykKE0UmnyEQwFqTG4uv+Nm5oqMQ0E4o/k2f9FjCrEXXSdSW4tv8xckzZl93OcZKv3EbrrpJtasWQPAqlWr+n+Dx2BAgtjatWu56KKL+M1vfsPtt98+EKc4Zhft9FGb5qEhqY1G02ECiooeDWMAWDQZexgy21RyWtJwNF6JzDAUOVrGU5VMmCQfBaYGUiULVYFKGvOSuPyHSzFZrKf47gRBGAwi9f3rE9bf4/oyFPqJ/fnPfwbg+eef58477+Qf//jHMd/n0SQ8iHk8Hu666y4uu+yyRD/1CZkYnkx6RbT1iaYbUDHSUa0DQCaMQgQkHVU3ockGQMOhNJJtDJAsWWgK1FGv+Bh389eYM+XaU3MjgiAMWobM/vUJ6+9x8QyVfmIdbr75Zr73ve91tntJpIRPUt5xxx3ceeedZGRkHPXYYDBIa2trj/8GiqZrjDA3kmtsJNXQRJLcik1uJUluIUl241DcZBsbKTI1MMnqYoa1gQnKQUxaOa7kerKXT2Th4z/kxkfupmjK0XvpCIJw5rHNnIEhJyea+RyLJGHIycE289j2rB6pez8xt9vd+Xhf/cS2bdvW2aKlez+xESNGdPYTA+L2EwP63U+stbWV6urqzr+//vrrpKen9zn9ebwSOhJ79913cbvdLF68uPPTQF8efPBBHnjggUReQlw5F41j//ov0drCmCIKaZKxR7+fkB7BQwSPVSE518nUBVMZPmE8sqg6LQhCP0mKQva990SzECWpZ4JHe2DLvveehOwXG8z9xFpaWjqnN2VZJjMzk7fffvuYR3D9cUz9xObPn9/rxemwdetWrrjiCt5//32ysrJYunQpM2fO7HNNLBgMEgwGO//e2tpKQUHBgPQTEwRBOB7H00/sZOwTOxlOu35in3zySdyvrVu3jpqaGmbPjmbcNDQ0sHLlSurr6+OOtsxmM2az+VguQRAEYdCzX3IJKRddJCp2nAQJm04855xzcLlcnX/vz0hMEAThdCUpygml0Q8GQ6GfmCjmJwiCIAxZA7bZ+bnnnhuopxYEQRAEQIzEBEEQhCFMBDFBEARhyBJBTBAEYQBomk7V3mb2fV5L1d5mNK3fu5n6JRwO88ADDzBu3DgmTJjAtGnTWLRoEdu2bUvoefqyePFi8vLykCQJr9cb85gHHngASZL6VafxeJwxBYAFQRBOlrKtLj55uZQ2d9c+2CSnmfnfHE3xtKyEnGOwt2IB2LJlCxs2bGD48IHr7DGogljHvuuBLD8lCIJwLEKhEJqmoapqjxqD8RzYVs/qP+7u9XibO8iqJ3dyyXdKGDn1xGondrRiOXToEHa7vfO6vva1rwHwzDPP8PLLL5OVlcWePXt49NFHaWlp4b777iMSieB0Ovn973/f2YrlrrvuYuPGjUC0FcvXv/51ysrKOHToEHPmzOGmm25i3bp1tLW18eijj3a2Yrngggs6r+nI1ycYDPKDH/yAF198kQULFsR9/VRVRdM0vF4voVCo8/GOOHC0ehyDKoh5PB4ACgoKTvGVCIIgRBUWFvLEE0/g9/uPeqyu6Wx7Ofa0Woe1/7cHt1qBJB9/Cab333+f/Px8ysvLKS8v7/X18vJyPvnkE1588UV+/OMf09TUxPXXX88TTzzBqFGjePfdd1m0aBEvv/wy+/fvx+fzsXXrVgD2799PKBRi69atVFdX09jYiN1u5/HHH2fHjh1ce+21vPHGG1itPbt3bN++HZutq4v9Y489xrnnnovb7SYUCrFnzx7C4XDM+2loaODyyy+PeS8ejweHwxH3tRhUQSwvL4+KigpSUlISXmOro6RVRUWFKGmVAOL1TBzxWibOQLyWoVCIuro6ioqKjlp2qmqfm1Db9r6fr00nO2Uk+WOcx31N+/fvx2azMW3aNCDaimXJkiX4/X7mz5/P3LlzmT9/Pl//+teB6DTjjBkz+Jd/+RcgWgX/f/7nf8jJyWHUqFE9nstoNGIymZg8eTLV1dWYTCZ+8YtfIMsy06ZN4ze/+U3nc3Q3ZcqUzsr569ev5/Dhwzz99NNIkoTJZGL8+PFMnDix170EAgEOHTrE5s2bMZlMnY/ruo7H4yEvL6/P12JQBTFZlhk2bNiAnsNut4s3igQSr2fiiNcycRL5WgYCAerr61EU5ajrSgFv7JFGrONOZI1qxowZlJaW0traSmpqKmPGjOnRikWWZVJSUjrPIcsysiz3OqfBYMBsNvdYM+sYLXU/VlGUHp2ZDQZDr+fq/vqsW7eOvXv3MmrUKAAqKyv52te+xp/+9Kdebbo6njs5ObnXh4S+RmAdRHaiIAhCgiTZ+1cLtr/HxTPYW7HcfffdVFdXc+jQIQ4dOsSwYcN47733BqTP5KAaiQmCIAxluaOdJDnNPbISj5ScaiZ3tPOEzzWYW7GcTMfUimUoCwaDPPjgg9xzzz2icn4CiNczccRrmTgD8VoeayuWsq0uVj0Zf0/Uwu9OTFia/UDSNI3PP/+cyy+//JS0YumvMyaICYIgHI/jeZONtU8sOdXMOUsSt0/sZDiV/cT6S0wnCoIgJFjxtCxGTMmkptRNW2uQJHt0ClE+gbT6U2EotGIRQUwQBGEAyLJE/tjUU30Zpz2RnSgIgiAMWWdsEFu7di2KovC73/3uVF/KkHbvvfcyfvx4pkyZwuzZs/noo49O9SUNKaWlpcydO5cxY8Ywe/Zsdu/uXa5IOLpAIMCiRYsYM2YMU6dOZeHChRw6dOhUX9aQV11dzebNm/tVreRUOSODmMfj4a677hqQPQtnmvnz57Nlyxa2b9/OH//4R6655hoCgcCpvqwh47vf/S633nor+/bt4+c//znLly8/1Zc0ZN16663s3buXbdu2ccUVV3Drrbee6ksa0tra2vB6vT2qaAxGZ2QQu+OOO7jzzjvJyMg41Zcy5F122WWdNdQmTZqEqqqDfiF4sHC5XGzZsoUbbrgBgGuuuYaDBw+KEcRxsFgsfO1rX+ssV3fWWWdx4MCBU3pNmqZSsetL9nz6MRW7vkTTjl48eLDQNI3Dhw9TWFh4qi/lqM64xI53330Xt9vN4sWLefvtt0/15ZxWnn32WYqLiwe8dNjpoqKigry8PAyG6K+hJEkMHz6cw4cPU1RUdGovboj77W9/y5VXXnnKzl+68TM+eu4pvE1dH+iS0zK4cOmtjJ4zNyHnCIfDrFixgpdeeglFUTCZTBQWFnL//ff3asVyrKqrq0lPTz/qXrvFixfz2WefUVNTg8fj6aydCHTWmuxIm7/nnnv45je/eULXFctpF8Tmz5/fa7d6h61bt3L33Xfz/vvvn+SrGrqO9np2dBz48MMPeeCBB8Rre4yOLHQttm2euBUrVlBaWsoTTzxxSs5fuvEz3npkRa/HvU0NvPXICq66496EBLKB6ifm9Xppa2sjPz//qMcerZ/Y3//+95hFfxPptAtin3zySdyvrVu3jpqaGmbPng1Ey/+vXLmS+vp6HnjggZN1iUNKX69nh48//phly5axcuVKxo4dexKu6vRQUFBAZWUlkUgEg8GArutUVFQMaAPB093DDz/Ma6+9xgcffNCjLcjJomkqHz33VJ/HrHn+KYpnzUGWj78AcEc/sYqKis4ABnSOPp977jn++te/kpWVxe7du3nsscdoaWnh3nvvJRKJkJqayh/+8IfOfmI/+9nP2Lx5MwCff/45119/PatWraKqqopvfetbfP3rX2fXrl34/X4ee+yxzn5iCxYsOO57SJTTLoj15ZxzzsHlcnX+fenSpcycOZPbb7/9FF7V0PbPf/6TG2+8kTfffJMpU6ac6ssZUrKyspg2bRovvvgiS5cu5dVXX6WoqEhMJR6nRx55hJdeeokPPvgAp9N5Sq6has+uHlOIsXgaG6jas4uCCX0X0e3L1q1bGTVqFGlpaXGPWbduHVu3bmX06NG4XC5KSkpYs2YNkyZN4i9/+QtLlixh587e5bEyMzM7W7HY7XZaWlo455xzeP7559mwYQOLFi2irKyss35iX66//no0TWPOnDk8+OCDZGaeWDPQWM7IxA4hcZYvX04wGGTZsmVMnTqVqVOnsmPHjlN9WUPGk08+yZNPPsmYMWN46KGHePrpp0/1JQ1JlZWV/PSnP8XtdnPBBRcwdepU5syZc9Kvw+tuTuhxfek+FV1WVsbUqVMZO3Ys3/nOd4Doh/bRo0cDsHHjRqZOncqkSZOAaHCprKykpqbmqOcxGo1cd911QDRhJicnh+3b++6ZBtEPuNu3b2fLli2kp6dz8803H/M99scZNRI70nPPPXeqL2HIKy0tPdWXMKSNHTuW9evXn+rLGPKGDRs2KNYTk539q9DR3+PimTZtGqWlpTQ3N5OamkpxcXGPfmJAjyQLXddjNhqWJAmDwYCqdmVOHrlFRpKkXl2c+9O0uGNa3Gg08pOf/IQxY8b0/waPgRiJCYIgJEj++Akkp/W9dSclPYP88RNO6DyDvZ9YW1tbj+t66aWXenWCTpQzeiQmCIKQSLKscOHSW2NmJ3a44OZbTyipo8Ng7idWV1fHNddcg6qq6LrOyJEj+fOf/3zC9xyLaMUiCILQh+NpFRJrn1hKegYX3Jy4fWIng2jFIgiCcAYaPWcuxbPmRLMV3c0kO1PJHz8hISMwoScRxARBEAaALCsnlEY/GAyFfmIisUMQBEEYskQQEwRBEIYsEcQEQRCEIUsEMUEQBGHIEkFMEARhAOiaTqDMjW+bi0CZG11L7G6mcDjMAw88wLhx45gwYQLTpk1j0aJFbNu2LaHn6cvixYvJy8tDkiS8Xm+PrwWDQW6//XZGjx7NhAkTOvvmJZrIThQEQUgw/84G3CvLUFtCnY8pDhPOK4uxTkxMM96BasVyLPpqxXL33XcjyzL79u1DkqR+1Wk8HmKzsyAIQh+OdTOuf2cDjS/G7sEHkH7D+BMOZKWlpUydOpWKioqYlexPpBXLzp07ueKKKzh06FDnZuelS5fyySef4PV6e7Ri6SBJUo+mmB39yCorK3vUcIzlRDc7i+lEQRCEBNE1HffKsj6Pca88cMJTi/1txXLfffexefNmiouLueGGG3j++ef58ssvufXWW1myZEm/ztXY2MikSZPYuHEjTz/9NNddd13cGo0dysrKSE9P51e/+hUzZ85k/vz5fPjhh8d0j/0lgpggCEKCBA+29JhCjEVtCRI82HLC5zpZrVhMJhM33ngj0P9WLOFwmAMHDlBSUsLmzZv53e9+x7XXXkt9ff1x3WtfRBATBEFIEM3TdwA71uPi6d6KBehsxXLPPfd0PpaoViyxHK0VS2FhIbIsc/311wMwZcoURowYwa5du45+c8dIBDFBEIQEkVNMCT0unsHeiiUjI4OLLrqI9957D4Dy8nIOHjzI2LFjj+t++yKyEwVBEBLEPMKB4jD1OaWoOMyYRzhO+FyDuRULwBNPPMEtt9zCXXfdhaIoPPXUU+Tm5p7wfR9JZCcKgiD0YTBmJ54sQ6EVi5hOFARBSCDrxAzSbxiP4ug5Zag4zEMqgA0VYjpREAQhwawTM7CUpBM82ILmCSGnmDCPcCDJfSdEDDZDoRWLCGKCIAgDQJIlLMXOU30Zpz0xnSgIgiAMWSKICYIgCEOWCGKCIAjCkCXWxARBEAaApmmUl5fj9XpJTk7urGIhJJYIYoIgCAm2e/duVq1aRWtra+djdrudhQsXUlJSkpBzhMNhVqxYwUsvvYSiKJhMJgoLC7n//vt7tWIZKIsXL+azzz6jpqamRxV7t9vN+eef33mcz+fjwIEDuFyuPosWHw8RxARBEBJo9+7dndUwumttbeWVV15hyZIlCQlkg7mfmNPp7NGc8+GHH+bjjz9OeAADsSYmCIKQMJqmsWrVqj6PWbVqFZqmndB5SktLef3113nmmWc6AxjAlVdeyfXXX89zzz3HwoULuemmm5g5cyabNm1i1apVTJ8+ncmTJ3Peeeexe/duANauXcvMmTM7n2Pnzp0UFRUB0YodGRkZ/OxnP2POnDlMmDCBjz76qPPYBQsWkJWVddTrffbZZ1m+fPkJ3XM8YiQmCIKQIOXl5T2mEGNpbW2lvLycESNGHPd5+ttPbOvWrYwePRqXy0VJSQlr1qxh0qRJ/OUvf2HJkiXs3LnzqOfq6Cf28MMPs2HDBhYtWkRZWVln/cSjWb9+PY2NjVxxxRX9vr9jIUZigiAICeL1ehN6XF8Gcz+x7p555hluuukmDIaBGTOJICYIgpAg3Xt4JeK4eAZ7P7EObW1tvPzyy9xyyy39Ov54iCAmCIKQIIWFhdjt9j6PsdvtFBYWntB5Bns/sQ5/+9vfmDx5MuPGjTvWW+w3sSYmCIKQILIss3DhwpjZiR0WLlyYkP1ig72fGMDTTz89YAkdHUQ/MUEQhD4cT7+rk7FP7GQYCv3ExEhMEAQhwUpKShg3bpyo2HESiCAmCIIwAGRZPqE0+sFgKPQTEx8LBEEQhCFLBDFBEARhyBJBTBAEQRiyRBATBEEQhiwRxARBEAaArqs0N2+gtvYtmps3oOvq0b/pGITDYR544AHGjRvHhAkTmDZtGosWLepRPX6gLV68mLy8PCRJ6lVK67333mPGjBlMmzaNiRMn8vzzzw/INYjsREEQhARzud5jX+l/EAzWdj5mNucwZvT/Iyvr0oScYzC3YtF1neuuu441a9YwefJkDh06xLhx47j66qtJSUlJ6DWIkZggCEICuVzvsWPnbT0CGEAwWMeOnbfhcr13wucYKq1YOkpitba2kp6ejtlsPuF7P5IYiQmCICSIrqvsK/0PIFYhJB2Q2Ff6SzIzFyBJxz8yGuytWCRJ4pVXXuHqq68mKSmJ5uZmXnvtNUwm03Hdb1/ESEwQBCFB3O7Pe43AetIJBmtwuz8/4XMN5lYskUiEBx98kDfffJPy8nI+/PBDbr75Zpqamo7rXvsigpggCEKCBIOuhB4Xz2BvxbJt2zaqq6uZN28eALNmzSIvL++Y+pD1lwhigiAICWI2x18fOp7j4hnsrVgKCgqorKxk7969AOzfv5+ysjLGjBlzXPfbF7EmJgiCkCBO5yzM5hyCwTpir4tJmM05OJ2zTvhcg7kVS3Z2Nk8++SSLFy9GlmV0Xefxxx8nPz//hO/7SKIViyAIQh+OtVVIR3ZiVPe31+gU3KSJv09Ymv1AGwqtWMR0oiAIQgJlZV3KpIm/x2zuuXfKbM4ZUgFsqBDTiYIgCAmWlXUpmZkL2rMVXZjNWTids04orf5UGAqtWEQQEwRBGACSpJCaetapvozTnphOFARBEIYsEcQEQRCEIUsEMUEQBGHIEmtigiAIA0DVdTa4vbhCEbJMBs5yJqMcpdKFcOzESEwQBCHB3ql3M3P9bq7ZVsb3d5dzzbYyZq7fzTv17oSdY7D3E1u1ahUzZ85k8uTJnHXWWQNScgrESEwQBCGh3ql38+2dh3rV66gNhvn2zkP8aWIRl2c6T/g8g7mfWHNzMzfccAOffPIJ48eP5+OPP+b666/vV9X8YyVGYoIgCAmi6jr/VloVtxELwH2lVagnWChpsPcTKysrIysri/HjxwNw3nnnUV5e3lmiKpFEEBMEQUiQDW4vNcFw3K/rQHUwzAa3N+4x/dHffmL33Xcfmzdvpri4mBtuuIHnn3+eL7/8kltvvZUlS5b061wd/cQ2btzI008/zXXXXRe30HCH0aNHU19fz4YNGwB4/fXX8Xq9HDp0qN/32F8iiAmCICSIKxRJ6HF9Gcz9xBwOB6+++ip33303M2bMYO3atZSUlGA0Go/rXvsi1sQEQRASJMvUv7fU/h4XT/d+YqmpqZ39xJ577jnefvtt4NT2EwM499xzWbt2LQDBYJCcnJzO6cVEEiMxQRCEBDnLmUyu2Ui8t3gJyDMbOcuZHOeI/hns/cSAHqO8X/7yl1x44YWMGjXqmO6zP8RITBAEIUEUSeJXo/P59s5DSMRqxAK/HJ2fkP1ig7mfGMB9993HunXriEQinH322Tz99NMnfM+xiH5igiAIfTieflfv1Lv5t9KqHkkeeWYjvxydn5D0+pNlKPQTEyMxQRCEBLs808nCDIeo2HESiCAmCIIwABRJYl5qyqm+jBMyFPqJicQOQRAEYcgSQUwQBEEYskQQEwRBEIYsEcQEQRCEIUsEMUEQhAGgajrryxp5c1sV68saUbXE7mY61a1YqqurufTSSxk7diyTJ09myZIlNDU1dX69tLSUuXPnMmbMGGbPnt1ZcDjRRHaiIAhCgq3aWcMDK3dT09JVwinXYeHfryxh4cTchJzjVLdiURSF++67j3POOQeAO++8k7vvvpunnnoKgO9+97vceuutLF26lL///e8sX76c9evXJ/QaQIzEBEEQEmrVzhq+/+KWHgEMoLYlwPdf3MKqnUcvuns0g6EVS3Z2dmcAA5gzZw4HDhwAwOVysWXLFm644QYArrnmGg4ePDggVezFSEwQBCFBVE3ngZW74/YTk4AHVu7m4pIcFPn4Nz73txXL1q1bGT16NC6Xi5KSEtasWcOkSZP4y1/+wpIlS/rVpLKjFcvDDz/Mhg0bWLRoEWVlZZ2lpyA60vv973/PokWLAKioqCAvLw+DIRpiJEli+PDhHD58uDNAJooYiQmCICTIpoNNvUZg3elATUuATQeb4h7TX4OlFYuu6/zgBz/A6XTywx/+MOb1dRw3EEQQEwRBSBCX5+htTI7luHi6t2IBOlux3HPPPZ2PnaxWLD/60Y+oqKjg5ZdfRpajIaWgoIDKykoikUjn+SsqKhg+fPhx3G3fRBATBEFIkKyU/hWw7e9x8QyWViw/+tGP2L9/P6+//jomk6nr/rKymDZtGi+++CIAr776KkVFRQmfSgSxJiYIgpAws0ekkeuwUNsSiLkuJgE5DguzR8Rfy+qvU92K5dNPP+Wxxx5j3LhxzJkzB4ARI0bw+uuvA/Dkk0+ydOlSVqxYgd1u5/nnnz/he45FtGIRBEHow7G2CunIToTY/cT+cMP0hKXZD7Sh0IpFTCcKgiAk0MKJufzhhunkOHq+Iec4LEMqgA0VYjpREAQhwRZOzOXikhw2HWzC5QmQlRKdQjyRtPpTYSi0YhFBTBAEYQAossTZxemn+jJOe2I6URAEQRiyRBATBEEQhiwRxARBEIQhS6yJCYIgDARNhfLPwFsHydlQOBfkxFaSF8RITBAEIfF2vwWPToTnr4BXl0f/fHRi9PEEGez9xH70ox9RVFSEJEn9KjR8vEQQEwRBSKTdb8ErN0Frdc/HW2uijycokC1btoytW7eyfv16du3axdatW1m+fDm7du3qdWz32oiJ0tFPbO/evXz55ZcUFhZy9913d3598eLFrFu3jsLCwoSfuzsRxARBEBJFU2HVXRC3GQuw6u7ocSdgsPcTAzj33HMZNmzYCd1nf4g1MUEQhEQp/6z3CKwHHVqroseNmH/cpxns/cROJjESEwRBSBRvXWKP68Ng7yd2soggJgiCkCjJ2Yk9Lo7B3k/sZBJBTBAEIVEK54I9j66a9UeSwJ4fPe4EDPZ+YieTWBMTBEFIFFmBhb+OZiEiEbMZy8KHErJfbLD3E7vtttt48803qa2tZcGCBSQnJ7N///4Tvu8jiX5igiAIfTiufle734pmKXZP8rDnRwNYyVUDc6EDYCj0ExMjMUEQhEQruQrGXS4qdpwEIogJgiAMBFk5oTT6wWAo9BMTiR2CIAjCkCWCmCAIgjBkiSAmCIIgDFkiiAmCIAhDlkjsEARBGACqprLFtYV6Xz2ZtkymZ01HEdmJCSeCmCAIQoJ9UP4BD216iDpfV43EbFs2d8++mwWFCxJyjnA4zIoVK3jppZdQFAWTyURhYSH3338/U6dOTcg5+lJdXc2yZcs4dOgQZrOZcePG8cQTT5CWlkYgEODaa69l9+7d2Gw2cnJyeOKJJzqr4yeSmE4UBEFIoA/KP+COtXf0CGAALp+LO9bewQflHyTkPIO9n9itt97K3r172bZtG1dccQW33nprwq8BRBATBEFIGFVTeWjTQ+gx+ol1PPbrTb9GPc37iVksFr72ta91Fgo+66yzevQaSyQxnSgIgpAgW1xbeo3AutPRqfXVssW1hVk5s477PEOtn9hvf/tbrrzyymO+z/4QIzFBEIQEqffVJ/S4vgyVfmIrVqygtLSU//zP/zz+m+2DCGKCIAgJkmnLTOhx8QyVfmIPP/wwr732Gu+++y42m+3YbrKfRBATBEFIkOlZ08m2ZSPF6ScmIZFjy2F61vQTOs9Q6Cf2yCOP8NJLL/H+++/jdDpP6H77ItbEBEEQEkSRFe6efTd3rL0DCalHgkdHYLtr9l0J2S82mPuJVVZW8tOf/pSRI0dywQUXAGA2m9m4ceMJ3/eRRD8xQRCEPhxPv6tY+8RybDncNfuuhO0TOxlEPzFBEIQz0ILCBVxQcIGo2HESiCAmCIIwABRZOaE0+sFA9BMTBEEQhAEkgpggCIIwZIkgJgiCIAxZIogJgiAIQ5YIYoIgCANAV1XaNm6i5e13aNu4CT3BleTD4TAPPPAA48aNY8KECUybNo1Fixaxbdu2hJ4nnurqai699FLGjh3L5MmTWbJkCU1NTZ1fv+SSS5g8eTJTp05l/vz5A3ZdYp+YIAhCH45nH1Pr6tXUrXiQSG1t52OGnByy770H+yWXJOS6brjhBrxeL88++2xnJfuVK1fS2trK9ddf3+NYVVVRlMSm99fV1VFaWtpZyf7OO++kpaWFp556CgC3291ZqeONN97gP/7jP9iyZUuv5znRfWJiJCYIgpBAratXU/Xjn/QIYACRujqqfvwTWlevPuFzDPZWLECPUlMtLS296iomitgnJgiCkCC6qlK34kGINcGl6yBJ1K14kJSLLkI6gZHRUGnFctNNN7FmzRoAVq1adXw3exRiJCYIgpAgvs1f9BqB9aDrRGpr8W3+4oTPNRRasfz5z3+moqKCX/3qV9x5550ndsNxiCAmCIKQIJH6/vUJ6+9x8QyVViwdbr75ZtasWdNZKT+RRBATBEFIEENm//qE9fe4eAZ7K5bW1laqq6s7//7666+Tnp7e5/Tn8RJrYoIgCAlimzkDQ04Okbq62OtikoQhOxvbzBknfK7B3IqlpaWFa665Br/fjyzLZGZm8vbbb8ccDZ4okWIvCILQh2NNAe/ITgR6BrL2N/D83zyasDT7gTYUWrGI6URBEIQEsl9yCfm/eRRDdnaPxw3Z2UMqgA0VYjpREAQhweyXXELKRRdFsxXr6zFkZmKbOeOE0upPhaHQikUEMUEQhAHw/9u7++Coqnxd/M/uhjSGkDTmHYQkxGDohJDw7gsqLwfiKJ5csaIHUiDgiXfEmvGHpUZrcMjIBK2iuDOjhwvcQV4Kbw4ooILXxMEJiEMCZBLEJCoxJBlCCB0hnaaDtJ3u/v2RSUsn3U2n9+qwt/N8qqyps3vh2uv8wde193evR9JqMXzG9Ft9Gz97fJxIRESqxSJGRESqxSJGRESqxXdiRERB4HA4cbHehC6zFcPDdYhP0UOjEf+d1L86FjEiIsEaqo04tqceXSar69pwvQ6znkhBclaMkDlsNhuKiopQXFwMrVaLkJAQJCQkYO3atcjMzBQyhy+tra1Yvnw5mpqaoNPpkJqais2bN/c7laOwsBBr167FV199hfT0dOH3wceJREQCNVQbUbKlxq2AAUCXyYqSLTVoqDYKmWf58uWorq5GeXk5amtrUV1djZUrV6K2trbfWLvgQE4A0Gq1WLNmDb799lucOXMGCQkJKCgocBtTVVWFiooKjB07Vvj8vVjEiIgEcTicOLan3ueYL/bWw+GQd1CSGvLErFYrVq1ahU2bNgXluKlefJxIRCTIxXpTvx1YX5YOKy7WmzD6rpE+x/mihjyx1157DXl5eUhKSgp4nf7gToyISJAus+8CNtBxvig5T6y8vBynTp3Cs88+K3udN8MiRkQkyPBwndBx3ig9T+zo0aP45ptvkJSUhMTERLS0tGDBggX45JNPAluwDyxiRESCxKfoMVzvu0CFjexpt5dD6XliBQUFaG1tRVNTE5qamnDHHXegtLQUDz30kKx1e8J3YkREgmg0EmY9kYKSLd7fNd2XmyLkezEl54kNJuaJERH5EEjelafvxMJG6nBfrrjvxAaDGvLEuBMjIhIsOSsGSZOieWLHIGARIyIKAo1GktVGrwRqyBNjYwcREakWixgREakWixgREakWixgREakWixgRURA4HHacrz2Dr/92FOdrz8DhEHuSvM1mQ2FhIVJTU5GWloasrCzk5OTg9OnTQufxprW1FQsWLMBdd92FjIwM5Obm4sqVK67fExMTkZqaiszMTGRmZmLPnj1BuQ92JxIRCVZ/4jj+umMrLFd+6uwLuz0Kc57KR8qMe4TMsXz5clgsFpSXl7tOsj948CBqa2v75YnZ7XZotVoh8/bqjWLpPcn+xRdfREFBAbZu3eoa8/777wclQ+xG3IkREQlUf+I4PtpY5FbAAMBy5Xt8tLEI9SeOy59DBVEsg4U7MSIiQRwOO/66Y6vPMWU7tyJ52gxoNIHvjNQQxQL0nJbvcDgwY8YMrF+/HtHR0QGv2RvuxIiIBLnwdW2/HVhfVy9/jwtf909fHiglR7EAwOeff44vv/wSVVVViIyMxLJly2Sv2RMWMSIiQSymDqHjvFF6FAsAjB07FgAwdOhQPP/88zh27NgAV+kfFjEiIkHC9P4dM+XvOG+UHsXS1dXldl/FxcXIysqStWZv+E6MiEiQ0RPSEHZ7lM9HiiMiozB6QprsuZQcxXLp0iUsWrQIdrsdTqcT48aNw65du2Sv2RNGsRAR+TDQqJDe7kRvHl39qrA2+2BTQxQLHycSEQmUMuMePLr6VYTdHuV2fURklKoKmFrwcSIRkWApM+5B8rQZPd2Kpg6E6Udi9IQ0WW31t4IaolhYxIiIgkCj0WJMWsatvo2fPT5OJCIi1WIRIyIi1WIRIyIi1eI7MSKiIHA6nLA2dsJx9UdoRoRAlxQBSdP/1AySh0WMiEiwH2q+h+lgA+ydP7quaSNCoF+YjNvSo3z8Sf/ZbDYUFRWhuLgYWq0WISEhSEhIwNq1a/tFsQRDa2srli9fjqamJuh0OqSmxOM0VgAAc8NJREFUpmLz5s2uQ4mtViteeOEFlJaWIiQkBFlZWdi9e7fw+2ARIyIS6Iea73F599f9rts7f8Tl3V8jMm+CkEKm9DyxgoICaDQanD17FpIk+XXYcCD4ToyISBCnwwnTwQafY0wHz8HpkHdQktLzxLq6urB9+3YUFRW5DguOj4+XtWZvWMSIiASxNna6PUL0xN5phbWxU9Y8/uaJrVmzBpWVlUhOTkZeXh527tyJM2fOID8/H7m5uX7N1ZsnduLECWzbtg2LFy/ud9Bwb57YwoULAfREw0RGRmLdunWYOnUqZs2ahc8++yzwBfvAIkZEJIjjqu8CNtBxvig5T8xms+HcuXMwGAyorKzE22+/jSeffBLt7e2y190XixgRkSCaESE3HzSAcd4oPU8sISEBGo0GS5YsAQBMmjQJSUlJqK2VHwbaF4sYEZEguqQIaCN8FyhthA66pAhZ8yg9TywqKgpz585FaWkpAKC5uRmNjY246667ZK3bE3YnEhEJImkk6Bcme+xO7KVfOE7I92JKzhMDgM2bN2PFihV4+eWXodVqsXXr1qA0dzBPjIjIh0Dyrjx/J6aDfuE4Yd+JDQY15IlxJ0ZEJNht6VEYZojkiR2DgEWMiCgIJI2EYcn6W30bsqghT4yNHUREpFosYkREpFosYkREpFosYkREpFps7CAiCgKHw4Hm5mZYLBaEhYW5TrEQRclRLCaTCQ8++KBr7LVr13Du3DkYjUaf5z0GgkWMiEiwuro6lJSUwGw2u66Fh4cjOzsbBoNByBxKjmLR6/U4ffq0a+yGDRtw9OhR4QUM4ONEIiKh6urqsHfvXrcCBgBmsxl79+51RaDIofQolr62b9+OlStXyl63J9yJEREJ4nA4UFJS4nNMSUkJUlNTZT1a9DeKpbq6GikpKTAajTAYDCgrK8PEiRPx7rvvIjc3FzU1NTedqzeKZcOGDaioqEBOTg4aGhowfPhw15jeKJacnJx+f768vByXL1/GI488EtBab4Y7MSIiQZqbm/vtwPoym81obm6WPZeSo1hu9M4772Dp0qUYMiQ4eyYWMSIiQSwWi9Bx3ig9iqVXV1cX9uzZgxUrVgxsgQPAIkZEJMiNhUPEOG+UHsXS67333kNGRgZSU1NlrdcXvhMjIhIkISEB4eHhPh8phoeHIyEhQfZcSo9iAYBt27YFraGjF6NYiIh8GGhUSG93oje5ubnC2uyDTQ1RLHycSEQkkMFgQG5uLsLDw92uh4eHq6qAqQUfJxIRCWYwGJCamhrUEzsGgxqiWFjEiIiCQKPRICkp6Vbfxs+euv6zgIiI6AYsYkREpFosYkREpFp8J0ZEFAROpx0m0ylYrUbodDHQ66dBksSeJE/ciRERCWc0luJvx+9HVfUS1Nb9f6iqXoK/Hb8fRmOpsDlsNhsKCwuRmpqKtLQ0ZGVlIScnxy0CJZhaW1uxYMEC3HXXXcjIyEBubi6uXLni+r20tBRTpkxBVlYW0tPTsXPnzqDcBz92JiLyYaAf4xqNpfiqZhWAvn+19pw3ODH9vxATs0D2feXl5cFisWD79u1ueWJmsxlLlixxGxuMPLFLly6hvr7eLU+ss7MTW7duhdPpRFRUFMrKypCRkYGmpiakpqaivb0dI0aMcPv38GNnIiKFcDrtOFv/O/QvYHBdO1v/OpxOu4ff/aeWPLHecx3NZjMiIyOh0+lkrdsTvhMjIhKk5x1Ym48RTlitF2EyncLIkTMDnkfpeWKSJGHv3r147LHHMHz4cHR0dGD//v0eDwmWizsxIiJBrFaj0HG+KDlPrLu7G+vXr8eHH36I5uZmfPbZZ1i2bJnbOzNRWMSIiATR6WKEjvNG6Xlip0+fRmtrK+69914AwLRp0zBq1Ci34icKixgRkSB6/TTodHHobeLoT4JOFw+9fpqseZSeJzZmzBi0tLTg22+/BQB89913aGhowPjx42Wt2xO+EyMiEkSStBif8to/uxMluDd49BS28SlrhHwvpuQ8sdjYWGzZsgWPP/44NBoNnE4nNm3ahNGjR8ted19ssSci8iGQFnCjsRRn63/n1uSh08VjfMoaIe31g0UNeWLciRERCRYTswDR0fN4YscgYBEjIgoCSdLKaqNXAjXkibGxg4iIVItFjIiIVItFjIiIVItFjIiIVItFjIgoCOxOJ/7WcRUHLnXgbx1XYRf8NZPSo1hKSkowdepUZGRkYObMmUE5rQNgdyIRkXAft5vwm/oLuGi1ua7F64ZiXcpoPBytFzLH8uXLYbFYUF5e7hbFUltbi8zMTLexwYhi0Wq1WLNmjVsUS0FBAbZu3YqOjg7k5eXh2LFjmDBhAo4ePYolS5b4deDwQHEnRkQk0MftJjxd0+RWwACgzWrD0zVN+LjdJHsOpUexNDQ0ICYmBhMmTAAAPPDAA2hubkZVVZXstffFIkZEJIjd6cRv6i/4SBMD1tRfkP1o0d8oljVr1qCyshLJycnIy8vDzp07cebMGeTn5yM3N9evuXqjWE6cOIFt27Zh8eLF/c5o7I1iWbhwIYCesx3b29tRUVEBADhw4AAsFguampoCW7APLGJERIJUmCz9dmA3cgJotdpQYbLInkvJUSwRERHYt28fCgoKMGXKFBw5cgQGgwFDhw6Vve6++E6MiEgQ44/dQsd5c2MUy8iRI11RLDt27MChQ4cADH4UywcffOCKYgGA+++/H0eOHAEAWK1WxMXFuR4visSdGBGRIDEh/u0L/B3njdKjWAC47fJef/11zJkzB3feeaesdXvCnRgRkSAz9WGI1w1Fm9Xm8b2YhJ4uxZn6MA+/DoySo1gAYM2aNfjiiy/Q3d2Nu+++G9u2bZO9Zk8YxUJE5MNAo0J6uxMBT2liwJ/TE4W12QebGqJY+DiRiEigh6P1+HN6IuJ07k0M8bqhqipgasHHiUREgj0crUd2VAQqTBYYf+xGTMgQzNSHQeuhuULJ1BDFwiJGRBQEWknCvSNH3Orb+Nnj40QiIlItFjEiIlItFjEiIlItvhMjIgoCu8OJk41XYLx6HTEjhmF60u3QatTV2KEGLGJERIKV1FxE4cE6XOz86Qin+Ihh+O1CA7LT44XMYbPZUFRUhOLiYmi1WoSEhCAhIQFr167tF8USDF1dXZgzZ47rmKr4+Hhs3rzZdQJ+fX09li1bhu+//x56vR47duyAwWAQfh98nEhEJFBJzUX8cneVWwEDgLbO6/jl7iqU1Nz80F1/LF++HNXV1SgvL0dtbS2qq6uxcuVK1NbW9ht749mIotx22204fPgwvvzyS3z55ZfIzs7G6tWrXb8/88wzyM/Px9mzZ/HSSy9h5cqVwu8BYBEjIhLG7nCi8GCdzyiWwoN1sDvkHZSkhDwxjUaDESN6PiFwOp0wm82uA4CNRiOqqqqQl5cHAFi0aBEaGxuDEsXCx4lERIKcbLzSbwd2IyeAi53XcbLxCu5Ojgx4Hn/zxKqrq5GSkgKj0QiDwYCysjJMnDgR7777LnJzc/1KWu7NE9uwYQMqKiqQk5ODhoYGDB8+HAAwb948fPXVV4iOjsann34KADh//jxGjRqFIUN6SowkSRg7diz+8Y9/uAqkKNyJEREJYrx68xiTgYzzRSl5YocPH8bFixfxxBNPYN26dR7vD+jZrQUDixgRkSAxI/w7wNbfcd7cmCcGwJUn9sorr7iuDVaeGNDzaPE///M/XTEuY8aMQUtLC7q7u13znz9/HmPHjh3gSm+ORYyISJDpSbcjPmIYvDXSS+jpUpye5P0xoD+UkCd26dIlXLlyxTXuv//7v105YzExMcjKysLu3bsBAPv27UNiYqLwR4kA34kREQmj1Uj47UIDfrm7ChI8R7H8dqFByPditzpP7JtvvsF//ud/oru7G06nE8nJya6iBQBbtmzBU089haKiIoSHh2Pnzp2y1+wJ88SIiHwIJO9qML4TGwxqyBPjToyISLDs9Hj8myGOJ3YMAhYxIqIg0GokWW30SqCGPDE2dhARkWqxiBERkWqxiBERkWqxiBERkWqxsYOIKBgcdqD5OGC5BITFAgn3ABrtrb6rnx3uxIiIRKv7CPhDOrDzEWDfyp7//UN6z3VBbDYbCgsLkZqairS0NGRlZSEnJwenT58WNocvXV1dmDFjBiZNmoRJkyYhOzvb7ZT6X/3qV0hMTIQkSX4dNBwoFjEiIpHqPgL2LgXMre7XzRd7rgsqZErPE3v88cfxxRdfICEhQfjcN2IRIyISxWEHSl4GfCWKlRT0jJNB6XliAHD//ffjjjvukLVOf/CdGBGRKM3H++/A3DgB84WecUmzAp5G6Xlig4k7MSIiUSyXxI7zQel5YoOFRYyISJSwWLHjvFB6nthgYhEjIhIl4R4gfBTgK1EsfHTPOBmUnic2mPhOjIhIFI0WyH6zpwvRW6JY9htCvhdTep7YqlWr8OGHH6KtrQ3z5s1DWFgYvvvuO9nr7ot5YkREPgSUd1X3UU+X4o1NHuGjewqY4dHg3GgQME+MiOhfkeFRIPVhntgxCFjEiIiCQaOV1UavBMwTIyIiCiIWMSIiUi0WMSIiUi0WMSIiUi0WMSKiILA77DjVdgr/79z/w6m2U7DLPPS3LyVHsVy/fh05OTkYP348MjMz+8W0iMTuRCIiwQ43H8YbJ9/ApWs/nZEYGxqLgukFmJcwT8gcy5cvh8ViQXl5uesk+4MHD6K2thaZmZluY+12O7Rase39vVEsvSfZ/+EPf8Dq1auxf/9+AEB+fj4eeughSJKEt99+G/n5+UE5IJg7MSIigQ43H8bqI6vdChgAGK8ZsfrIahxuPix7DqVHsQwbNgy/+MUvXGcszpw5E+fOnZO9bk+4EyMiEsTusOONk2/A6SFPzAknJEh48+SbmD1mNrQyPnxWWxTLn/70JyxcuDCwxd4Ed2JERIJUGav67cBu5IQTbdfaUGWskj2XWqJYioqKUF9fj9///veBL9YHFjEiIkHar7ULHeeNWqJYNmzYgP379+OTTz5BaGio/wscABYxIiJBokOjhY7zRg1RLBs3bkRxcTH+8pe/QK/Xy1qvL3wnRkQkyOSYyYgNjYXxmtHjezEJEmJDYzE5ZrLsuZQcxdLS0oIXXngB48aNw+zZswEAOp0OJ06ckL3uvhjFQkTkw0CjQnq7EwG4FTLpn3liGx/cKKzNPtjUEMXCx4lERALNS5iHjQ9uRExojNv12NBYVRUwteDjRCIiweYlzMPsMbNRZaxC+7V2RIdGY3LMZFlt9beCGqJYWMSIiIJAq9FiWty0W30bP3t8nEhERKrFIkZERKrFIkZERKrFd2JEREHgtNtxrfLv6G5vx5DoaIROnQJJ8EnyxCJGRCSc+dNPcaloPbrb2lzXhsTFIfbVVxA+f76QOWw2G4qKilBcXAytVouQkBAkJCRg7dq1/aJYgqGrqwtz5sxxHVMVHx+PzZs3u07Anz9/Ptra2lyn3b/11ltBuS9+7ExE5MNAP8Y1f/opLvz6eaDvX63/PG9w9B//IKSQ5eXlwWKxYPv27W55YmazGUuWLHEbG4w8MYfDga6uLrc8sc8//9yVJ2YymVzHTX3wwQf43e9+h6qq/gcf82NnIiKFcNrtuFS0vn8BA1zXLhWth9MuL+VZ6XliANzOS+zs7HT7TSQ+TiQiEuRa5d/dHiH243Siu60N1yr/juEzpgc8j1ryxJYuXYqysjIAQElJScDr9YU7MSIiQbrb/YtY8XecL2rIE9u1axfOnz+PdevW4cUXX5S3YC9YxIiIBBkS7V/Eir/jvFFLnlivZcuWoayszBX3IhKLGBGRIKFTp2BIXJyriaMfScKQuDiETp0iax6l54mZzWa0tra6fjtw4AAiIyN9Pv4MFN+JEREJImm1iH31lZ7uRElyb/D4Z2GLffUVId+LKTlPrLOzE4sWLcIPP/wAjUaD6OhoHDp0yONuUC622BMR+RBIC/hgfCc2GNSQJ8adGBGRYOHz52PE3Lk8sWMQsIgREQWBpNXKaqNXAjXkibGxg4iIVItFjIiIVItFjIiIVItFjIiIVItFjIgoCBwOJy5824Gzp9pw4dsOOBxiv2ay2WwoLCxEamoq0tLSkJWVhZycHJw+fVroPN50dXVhxowZmDRpEiZNmoTs7Gw0NTX1G1dYWAhJkvw6pzEQ7E4kIhKsodqIY3vq0WWyuq4N1+sw64kUJGfFCJlj+fLlsFgsKC8vd4tiqa2t7ZfbFYwolttuuw2HDx92i2JZvXq1K4oFAKqqqlBRUYGxY8cKnftG3IkREQnUUG1EyZYatwIGAF0mK0q21KCh2ih7DjVEsVitVqxatQqbNm0KykkdvbgTIyISxOFw4tieep9jvthbj6RJ0dBoAv+LXQ1RLK+99hry8vKQlJQU8Dr9wZ0YEZEgF+tN/XZgfVk6rLhYb5I9l5KjWMrLy3Hq1Ck8++yzstd5MyxiRESCdJl9F7CBjvNG6VEsR48exTfffIOkpCQkJiaipaUFCxYswCeffDLwxd4EixgRkSDDw3VCx3mj9CiWgoICtLa2oqmpCU1NTbjjjjtQWlqKhx56SNa6PeE7MSIiQeJT9Biu1/l8pBg2Uof4FL3suZQcxTKYGMVCROTDQKNCersTvcl+Jl1Ym32wqSGKhY8TiYgESs6KQfYz6Riud39kGDZSp6oCphZ8nEhEJFhyVgySJkX3dCuarRge3vMIUU5b/a2ghigWFjEioiDQaCSMvmvkzQeSLHycSEREqsUiRkREqsUiRkREqsV3YkREQeBw2HHh61pYTB0I04/E6Alp0GjEniRPLGJERMLVnziOv+7YCsuVnzr7wm6Pwpyn8pEy4x4hc9hsNhQVFaG4uBharRYhISFISEjA2rVr+0WxBENXVxfmzJnjOqYqPj4emzdvdp2An5iYiGHDhrm+/XrllVfwxBNPCL8PFjEiIoHqTxzHRxuL+l23XPkeH20swqOrXxVSyNSQJ/b+++8jPT1d6Lx98Z0YEZEgDocdf92x1eeYsp1b4XDYfY65GTXkiQ0W7sSIiAS58HWt2yNET65e/h4Xvq7FmLSMgOdRQ54Y0BP54nA4MGPGDKxfvx7R0dEBr9kb7sSIiASxmDqEjvNFyXliAPD555/jyy+/RFVVFSIjI7Fs2TLZa/aERYyISJAwvX8ndPg7zhul54kBwNixYwEAQ4cOxfPPP49jx44NYIX+YxEjIhJk9IQ0hN0e5XPMiMgojJ6QJmsepeeJdXV1ud1XcXExsrKyZK3ZG74TIyISRKPRYs5T+R67E3vNXpYv5HsxJeeJXbp0CYsWLYLdbofT6cS4ceOwa9cu2Wv2hHliREQ+BJJ35ek7sRGRUZi9TNx3YoNBDXli3IkREQmWMuMeJE+bwRM7BgGLGBFREGg0Wllt9EqghjwxNnYQEZFqsYgREZFqsYgREZFqsYgREZFqsYgREQWB0+HE9QYTrp024nqDCU6H2K+ZbDYbCgsLkZqairS0NGRlZSEnJwenT58WOo83XV1dmDFjBiZNmoRJkyYhOzsbTU1Nrt+tViuee+45pKSkIC0tDXl5eUG5D3YnEhEJ9kPN9zAdbIC980fXNW1ECPQLk3Fbuu8TPfyl9CiWgoICaDQanD17FpIk+XVOYyC4EyMiEuiHmu9xeffXbgUMAOydP+Ly7q/xQ438lnWlR7F0dXVh+/btKCoqcp2zGB8fL3vdnnAnRkQkiNPhhOlgg88xpoPnMMwQCUnT/0Befyk9iqWhoQGRkZFYt24dDh8+jNtuuw1r167F3LlzA16zN9yJEREJYm3s7LcD68veaYW1sVP2XEqOYrHZbDh37hwMBgMqKyvx9ttv48knn0R7e7vsdffFIkZEJIjjqu8CNtBx3ig9iiUhIQEajQZLliwBAEyaNAlJSUmora0d4EpvjkWMiEgQzYgQoeO8UXoUS1RUFObOnYvS0lIAQHNzMxobG3HXXXfJWrcnfCdGRCSILikC2ogQn48UtRE66JIiZM+l5CgWANi8eTNWrFiBl19+GVqtFlu3bg1KcwejWIiIfBhoVEhvd6I3kXkThLXZB5saolj4OJGISKDb0qMQmTcB2gj3R4baCJ2qCpha8HEiEZFgt6VHYZghEtbGTjiu/gjNiBDokiJktdXfCmqIYmERIyIKAkkjYViy/lbfxs8eHycSEZFqsYgREZFqsYgREZFq8Z0YEVEQOBwONDc3w2KxICwszHWKBYnFIkZEJFhdXR1KSkpgNptd18LDw5GdnQ2DwSBkDpvNhqKiIhQXF0Or1SIkJAQJCQlYu3ZtvyiWYOjq6sKcOXNcx1TFx8dj8+bNSExMhMlkwoMPPugae+3aNZw7dw5Go9HnocWB4MfOREQ+DPRj3Lq6OtdpGJ7k5uYKKWR5eXmwWCzYvn27W56Y2Wx2nVnYKxh5Yg6HA11dXW55Yp9//rkrT+xGGzZswNGjR3Hw4MF+v/FjZyIihXA4HCgpKfE5pqSkBA6HQ9Y8Ss8T62v79u1YuXKlrDV7w8eJRESCNDc3uz1C9MRsNqO5uRlJSUkBz6P0PLEblZeX4/Lly3jkkUcCXq8v3IkREQlisViEjvNFyXliN3rnnXewdOlSDBkSnD0TixgRkSA3ZniJGOeN0vPEenV1dWHPnj1YsWKF/4sbIBYxIiJBEhISEB4e7nNMeHg4EhISZM2j9DyxXu+99x4yMjKQmpoqa72+8J0YEZEgGo0G2dnZPrsTs7OzhXwvpvQ8MQDYtm1b0Bo6erHFnojIh0BawAfjO7HBoIY8Me7EiIgEMxgMSE1N5Ykdg4BFjIgoCDQajaw2eiVQQ54Y/7OAiIhUi0WMiIhUi0WMiIhUi0WMiIhUi0WMiCgInE47Ojoq0Nb2ETo6KuB02m/+hwbAZrOhsLAQqampSEtLQ1ZWFnJycnD69Gmh83jT1dWFGTNmYNKkSZg0aRKys7PR1NTk+r20tBRTpkxBVlYW0tPTsXPnzqDcB78TIyLyIZDvmIzGUpyt/x2s1jbXNZ0uDuNTXkNMzAIh96XkKBan04moqCiUlZUhIyMDTU1NSE1NRXt7u2t8L0axEBEpiNFYiq9qVrkVMACwWi/hq5pVMBpLZc+hliiW3iOxzGYzIiMjodPpZK+9L34nRkQkiNNpx9n63wHw9IDLCUDC2frXER09D5IU+M5I6VEskiRh7969eOyxxzB8+HB0dHRg//79CAkJCXjN3nAnRkQkiMl0qt8OzJ0TVutFmEynZM+l5CiW7u5urF+/Hh9++CGam5vx2WefYdmyZW4HBovCIkZEJIjVahQ6zhulR7GcPn0ara2tuPfeewEA06ZNw6hRo9yKnygsYkREguh0MULHeaP0KJYxY8agpaUF3377LQDgu+++Q0NDA8aPHy9r3Z7wnRgRkSB6/TTodHGwWi/B83sxCTpdHPT6abLnUnIUS2xsLLZs2YLHH38cGo0GTqcTmzZtwujRo2Wvuy+22BMR+TDQFvDe7sQeN/712vMIbmL6fwlrsw82NUSx8HEiEZFAMTELMDH9v6DTxbpd1+niVFXA1IKPE4mIBIuJWYDo6Hn/7FY0QqeLgV4/TVZb/a2ghigWFjEioiCQJC1Gjpx5q2/jZ4+PE4mISLVYxIiISLVYxIiISLX4ToyIKAjsTicqTBYYf+xGTMgQzNSHQevh1AySh0WMiEiwj9tN+E39BVy02lzX4nVDsS5lNB6O1guZw2azoaioCMXFxdBqtQgJCUFCQgLWrl2LzMxMIXP40tXVhTlz5riOqYqPj8fmzZtdJ+CXlJTgN7/5DX788UeEhoZiy5YtmDRpkvD74MfOREQ+DPRj3I/bTXi6pqnfeR29e7A/pycKKWRKzhPr6OhASkoKjh07hgkTJuDo0aNYtWqVx1Pz+bEzEZFC2J1O/Kb+gtcgFgBYU38Bdpl7B6XniTU0NCAmJgYTJkwAADzwwANobm5GVVWVrHV7wseJRESCVJgsbo8Q+3ICaLXaUGGy4N6RI7yOuxml54mlpKSgvb0dFRUVmDlzJg4cOACLxYKmpiZMnjw54HV7wp0YEZEgxh+7hY7zRcl5YhEREdi3bx8KCgowZcoUHDlyBAaDAUOHDpW97r64EyMiEiQmxL+/Uv0d582NeWIjR4505Ynt2LEDhw4dAnBr8sRSUlKwadMmAMD999+PI0eOAACsVivi4uJcjxdF4k6MiEiQmfowxOuGwlsjvQRglG4oZurDvIzwj9LzxAC47fJef/11zJkzB3feeaesdXvCnRgRkSBaScK6lNF4uqYJEjwFsQCvp4wW8r2YkvPEAGDNmjX44osv0N3djbvvvhvbtm2TvWZP2GJPRORDIC3gnr4TG6UbitcFfic2GNSQJ8adGBGRYA9H65EdFcETOwYBixgRURBoJUlWG70SqCFPjI0dRESkWixiRESkWixiRESkWixiRESkWmzsICIKArvDiZONV2C8eh0xI4ZhetLt0GrYnSgad2JERIKV1FzEfW/+Ff/xfyrw6/8+jf/4PxW4782/oqTm5mcV+stms6GwsBCpqalIS0tDVlYWcnJycPr0aWFz+GvFihWQJAkWi8V1rb6+Hvfccw/Gjx+P6dOnu07NF41FjIhIoJKai/jl7ipc7HQ/g7Ct8zp+ubtKWCFbvnw5qqurUV5ejtraWlRXV2PlypWora3tN/bGsxFFO3jwoMdzGZ955hnk5+fj7NmzeOmll7By5cqgzM8iRkQkiN3hROHBOp95YoUH62B3qD9PDOiJaSksLMTGjRvd7s9oNKKqqgp5eXkAgEWLFqGxsRFNTU2y1u0J34kREQlysvFKvx3YjZwALnZex8nGK7g7OTLgeZSSJ7Zq1SqsXbsWERERbn/m/PnzGDVqFIYM6SkxkiRh7Nix+Mc//uEqkKJwJ0ZEJIjx6s1jTAYyzpdbnSf23nvvISQkBI888shN7w/oiYMJBhYxIiJBYkb4d4Ctv+O8uTFPDIArT+yVV15xXQt2nlhZWRn++te/IjEx0bW7SktLw1dffYUxY8agpaUF3d3drvnPnz+PsWPHBrxmb1jEiIgEmZ50O+IjhvnME4uP6Gm3l0MJeWKbNm1CS0sLmpqaXO+6amtrMXHiRMTExCArK8sVzbJv3z63YicS34kREQmi1Uj47UIDfrm7ymue2G8XGoR8L3ar88RuZsuWLXjqqadQVFSE8PBw7Ny5U/aaPWGeGBGRD4HkXZXUXEThwTq3Jo/4iGH47UIDstPjg3WrwjFPjIjoX1B2ejz+zRDHEzsGAYsYEVEQaDWSrDZ6JWCeGBERURCxiBERkWqxiBERkWqxiBERkWqxiBERBYPDDjQeA756v+d/HWJPkld6FMuvfvUrJCYmQpIkv85oDBS7E4mIRKv7CCh5GTC3/nQtfBSQ/SZgeFTIFMuXL4fFYkF5ebnrJPuDBw+itrYWmZmZbmPtdju0Wq2QefvyFsXy+OOP46WXXsJ9990XlHl7cSdGRCRS3UfA3qXuBQwAzBd7rtd9JHsKpUexAMD999+PO+64Q/Zab4Y7MSIiURz2nh2Y10QxCSgpAFIfBjSB74yUHsUymLgTIyISpfl4/x2YGydgvtAzTialR7EMFhYxIiJRLJfEjvNC6VEsg4lFjIhIlLBYseO8UHoUy2DiOzEiIlES7unpQjRfhOf3YlLP7wn3yJ5K6VEsq1atwocffoi2tjbMmzcPYWFh+O6772Svuy9GsRAR+TDgqJDe7kQAHhPFcncJa7MPNjVEsfBxIhGRSIZHewpVeJ/csPBRqipgasHHiUREohke7Wmjbz7e08QRFtvzCFFGW/2toIYoFhYxIqJg0GiBpFm3+i5+9vg4kYiIVItFjIiIVItFjIiIVIvvxIiIgsDusKPKWIX2a+2IDo3G5JjJ0KqssUMNWMSIiAQ73HwYb5x8A5eu/XS8VGxoLAqmF2Bewjwhc9hsNhQVFaG4uBharRYhISFISEjA2rVr+0WxBNuKFSuwfft2XL16FWFhYbh+/TqefPJJ1NXVITQ0FHFxcdi8ebPreCqR+DiRiEigw82HsfrIarcCBgDGa0asPrIah5sPC5ln+fLlqK6uRnl5OWpra1FdXY2VK1eitra239gbz0YUzVueWH5+Pr799lucPn0ajzzyCPLz84MyP4sYEZEgdocdb5x8A04PR071Xnvz5Juwy0x5Vnqe2LBhw/CLX/zCVdxmzpyJc+fOyVqzN3ycSEQkSJWxqt8O7EZOONF2rQ1VxipMi5sW8DxqyxP705/+hIULFw54nf7gToyISJD2a+1Cx/miljyxoqIi1NfX4/e//31A67wZFjEiIkGiQ6OFjvNGLXliGzZswP79+/HJJ58gNDQ0oLXeDIsYEZEgk2MmIzY0FhL6FwwAkCAhLjQOk2Mmy5pHDXliGzduRHFxMf7yl79Ar9fLWq8vfCdGRCSIVqNFwfQCrD6yGhIktwaP3sL28vSXhXwvpuQ8sZaWFrzwwgsYN24cZs+eDQDQ6XQ4ceKE7HX3xTwxIiIfAsm78vSdWFxoHF6e/rKw78QGgxryxLgTIyISbF7CPMweM5sndgwCFjEioiDQarSy2uiVQA15YmzsICIi1WIRIyIi1WIRIyIi1WIRIyIi1WIRIyIKAqfdjq4TJ9F56GN0nTgJp+CT5G02GwoLC5Gamoq0tDRkZWUhJycHp0+fFjqPP1asWAFJkmCxWFzX5s+fj4yMDGRmZmLWrFlBuy92JxIRCWb+9FNcKlqP7rY217UhcXGIffUVhM+fL2SO5cuXw2KxoLy83HWS/cGDB1FbW9svT8xut0OrDU57v7colr1797pO6vjggw+wYsUKVFVVCZ+fOzEiIoHMn36KC79+3q2AAUD3pUu48OvnYf70U9lzKD2KBYDbUVOdnZ3QaIJTbrgTIyISxGm341LResDTQUhOJyBJuFS0HiPmzoUkY2ekliiWpUuXoqysDABQUlIS2GJvgjsxIiJBrlX+vd8OzI3Tie62Nlyr/LvsudQQxbJr1y6cP38e69atw4svvhjwWn1hESMiEqS73b+cMH/HeaOWKJZey5YtQ1lZmeukfJFYxIiIBBkS7V9OmL/jvFF6FIvZbEZra6vr33HgwAFERkb6fPwZKL4TIyISJHTqFAyJi0P3pUue34tJEobExiJ06hTZcyk5iqWzsxOLFi3CDz/8AI1Gg+joaBw6dMjjblAuRrEQEfkw0KiQ3u5EAO6F7J9/gY/+4x+EtdkHmxqiWPg4kYhIoPD58zH6j3/AkNhYt+tDYmNVVcDUgo8TiYgEC58/HyPmzu3pVmxvx5DoaIROnSKrrf5WUEMUC4sYEVEQSFoths+Yfqtv42ePjxOJiEi1WMSIiEi1WMSIiEi1+E6MiCgIHA4nLtab0GW2Yni4DvEpemg04r+T+lfHnRgRkWAN1UbsevU4Pvhf1fjLtjp88L+qsevV42ioNgqbQ+l5Yr0KCwshSZJfhw0HgjsxIiKBGqqNKNnS/y/sLpMVJVtqkP1MOpKzYmTPo/Q8MQCoqqpCRUUFxo4dG5S5Ae7EiIiEcTicOLan3ueYL/bWw+GQd1CSGvLErFYrVq1ahU2bNgXluKle3IkREQlysd6ELpPV5xhLhxUX600YfddIn+N8UUOe2GuvvYa8vDwkJSUFvE5/cCdGRCRIl9l3ARvoOF+UnCdWXl6OU6dO4dlnn5W9zpthESMiEmR4uE7oOG+Unid29OhRfPPNN0hKSkJiYiJaWlqwYMECfPLJJ3KW7RGLGBGRIPEpegzX+y5QYSN72u3lUHqeWEFBAVpbW12/3XHHHSgtLcVDDz0ka92e8J0YEZEgGo2EWU+keOxO7HVfboqQ78WUnCc2mJgnRkTkQyB5Vw3VRhzbU+/W5BE2Uof7clOEtNcPFjXkiXEnRkQkWHJWDJImRfPEjkHAIkZEFAQajSSrjV4J1JAnxsYOIiJSLRYxIiJSLRYxIiJSLRYxIiJSLRYxIqIgcDjsOF97Bl//7SjO156Bw2G/+R8aAKVHsSQmJiI1NRWZmZnIzMzEnj17gjI3uxOJiASrP3Ecf92xFZYrP3X2hd0ehTlP5SNlxj1C5lBDFMv777+P9PT0oMzbizsxIiKB6k8cx0cbi9wKGABYrnyPjzYWof7EcflzqCCKZbBwJ0ZEJIjDYcdfd2z1OaZs51YkT5sBjSbwnZEaoliAntPyHQ4HZsyYgfXr1yM6OjrgNXvDnRgRkSAXvq7ttwPr6+rl73Hh61rZcyk5igUAPv/8c3z55ZeoqqpCZGQkli1bJmu93rCIEREJYjF1CB3njdKjWABg7NixAIChQ4fi+eefx7FjxwJb7E2wiBERCRKm9++YKX/HeaP0KJauri63+youLkZWVpasNXvDd2JERIKMnpCGsNujfD5SHBEZhdET0mTPpeQolkuXLmHRokWw2+1wOp0YN24cdu3aJXvNnjCKhYjIh4FGhfR2J3rz6OpXhbXZB5saolj4OJGISKCUGffg0dWvIuz2KLfrIyKjVFXA1IKPE4mIBEuZcQ+Sp83o6VY0dSBMPxKjJ6TJaqu/FdQQxcIiRkQUBBqNFmPSMm71bfzs8XEiERGpFosYERGpFosYERGpFt+JEREFgdPhhLWxE46rP0IzIgS6pAhIGs+nvVPguBMjIhLsh5rv0fbmSXz/f77Clf/+Ft//n6/Q9uZJ/FAjrtNP6XliVqsVzz33HFJSUpCWloa8vLygzM2dGBGRQD/UfI/Lu7/ud93e+SMu7/4akXkTcFt6lIc/OTBKzxMrKCiARqPB2bNnIUmSX4cNB4I7MSIiQZwOJ0wHG3yOMR08B6dD3kFJSs8T6+rqwvbt21FUVOQqcPHx8bLW7A2LGBGRINbGTtg7f/Q5xt5phbWxU9Y8/uaJrVmzBpWVlUhOTkZeXh527tyJM2fOID8/H7m5uX7N1ZsnduLECWzbtg2LFy92HTTsLU+soaEBkZGRWLduHaZOnYpZs2bhs88+C3zBPrCIEREJ4rjqu4ANdJwvSs4Ts9lsOHfuHAwGAyorK/H222/jySefRHt7u+x198UiRkQkiGZEiNBx3ig9TywhIQEajQZLliwBAEyaNAlJSUmorZUfBtoXixgRkSC6pAhoI3wXKG2EDrqkCJ9jbkbpeWJRUVGYO3cuSktLAQDNzc1obGzEXXfdJWvdnrA7kYhIEEkjQb8w2WN3Yi/9wnFCvhdTcp4YAGzevBkrVqzAyy+/DK1Wi61btwaluYN5YkREPgSSd/VDzfcwHWxwa/LQRuigXzhOSHv9YFFDnhh3YkREgt2WHoVhhkie2DEIWMSIiIJA0kgYlqy/1bchixryxNjYQUREqsUiRkREqsUiRkREqsUiRkREqsUiRkQUBA6HA42Njfjqq6/Q2NgIh8Mh9N+v5CgWk8mEzMxM1z/jx4/HkCFDcOXKFeFzszuRiEiwuro6lJSUwGw2u66Fh4cjOzsbBoNByBxKjmLR6/VuxXTDhg04evSozwOLA8WdGBGRQHV1ddi7d69bAQMAs9mMvXv3uiJQ5FB6FEtf27dvx8qVK2Wv2xPuxIiIBHE4HCgpKfE5pqSkBKmpqdBoAt9D+BvFUl1djZSUFBiNRhgMBpSVlWHixIl49913kZubi5qampvO1RvFsmHDBlRUVCAnJwcNDQ0YPny41yiWG5WXl+Py5cseT7sXgTsxIiJBmpub++3A+jKbzWhubpY9l5KjWG70zjvvYOnSpRgyJDh7JhYxIiJBehsbRI3zRulRLL26urqwZ88erFixIqB1+oNFjIhIkBsLh4hx3ig9iqXXe++9h4yMDKSmpspary98J0ZEJEhCQgLCw8N9PlIMDw9HQkKC7LmUHsUCANu2bQtaQ0cvRrEQEfkw0KiQ3u5Eb3Jzc4W12QebGqJY+DiRiEggg8GA3NxchIeHu10PDw9XVQFTCz5OJCISzGAwIDU1Fc3NzbBYLAgLC0NCQoKstvpbQQ1RLCxiRERBoNFokJSUdKtv42dPXf9ZQEREdAMWMSIiUi0WMSIiUi2+EyMiCgKn0w6T6RSsViN0uhjo9dMgScE5Sf5fGXdiRESCGY2l+Nvx+1FVvQS1df8fqqqX4G/H74fRWCpsDiXniQFAaWkppkyZgqysLKSnp2Pnzp1BmZs7MSIigYzGUnxVswqA+zkSVuslfFWzChPT/wsxMQtkz6PkPDGn04nFixejrKwMGRkZaGpqQmpqKh577DGMGDFC6PzciRERCeJ02nG2/nfoW8D++SsA4Gz963A67R5+959a8sR6z3U0m82IjIyETqeTtW5PuBMjIhKk5x1Ym48RTlitF2EyncLIkTMDnkfpeWKSJGHv3r147LHHMHz4cHR0dGD//v0ICQkJeM3ecCdGRCSI1WoUOs4XJeeJdXd3Y/369fjwww/R3NyMzz77DMuWLcOVK1dkr7svFjEiIkF0uhih47xRep7Y6dOn0drainvvvRcAMG3aNIwaNQpffvllwGv2hkWMiEgQvX4adLo4AP0LRg8JOl089PppsuZRep7YmDFj0NLSgm+//RYA8N1336GhoQHjx4+XtW5P+E6MiEgQSdJifMpr/+xOlODe4NFT2ManrBHyvZiS88RiY2OxZcsWPP7449BoNHA6ndi0aRNGjx4te919MU+MiMiHQPKujMZSnK3/nVuTh04Xj/Epa4S01w8WNeSJcSdGRCRYTMwCREfP44kdg4BFjIgoCCRJK6uNXgnUkCfGxg4iIlItFjEiIlItFjEiIlItFjEiIlItNnYQEQWB3elEhckC44/diAkZgpn6MGg9nJpB8nAnRkQk2MftJkwtr8Oi0w34ZV0zFp1uwNTyOnzcbhI2h9LzxEpKSjB16lRkZGRg5syZQTlyCuBOjIhIqI/bTXi6pqlfGEub1Yana5rw5/REPBytlz2PkvPEOjo6kJeXh2PHjmHChAk4evQolixZ4tep+QPFnRgRkSB2pxO/qb/gI00MWFN/AXaZByUpPU+soaEBMTExmDBhAgDggQceQHNzM6qqqmSt2xMWMSIiQSpMFly02rz+7gTQarWhwmTxOsYf/uaJrVmzBpWVlUhOTkZeXh527tyJM2fOID8/H7m5uX7N1ZsnduLECWzbtg2LFy92HTTsLU8sJSUF7e3tqKioAAAcOHAAFovFdVCwSCxiRESCGH/sFjrOFyXniUVERGDfvn0oKCjAlClTcOTIERgMBgwdOlT2uvviOzEiIkFiQvz7K9Xfcd7cmCc2cuRIV57Yjh07cOjQIQCDmyfWKy0tDYcOHcLEiRNx//3348iRIwAAq9WKuLg41+NFkbgTIyISZKY+DPG6oT7SxIBRuqGYqQ/zMsI/Ss8TA+C2y3v99dcxZ84c3HnnnbLW7Ql3YkREgmglCetSRuPpmiYvaWLA6ymjhXwvpuQ8MQBYs2YNvvjiC3R3d+Puu+/Gtm3bZK/ZE+aJERH5EEje1cftJvym/oJbk8co3VC8njJaSHv9YGGeGBHRv6CHo/XIjorgiR2DgEWMiCgItJKEe0eOuNW3IQvzxIiIiIKIRYyIiFSLRYyIiFSLRYyIiFSLRYyIKAjsDifKGy7jw9MXUN5wGXaH2K+ZlBDFIkkSMjIykJmZiczMTBw7dsz1W319Pe655x6MHz8e06dPdx04LBq7E4mIBCupuYjCg3W42PnTEU7xEcPw24UGZKfHC5lDKVEsx48fdzviqtczzzyD/Px8PPXUU3j//fexcuVKlJeXC5+fOzEiIoFKai7il7ur3AoYALR1Xscvd1ehpObmh+7ejFKiWLwxGo2oqqpCXl4eAGDRokVobGzkKfZEREpmdzhReLDOZ55Y4cE62Y8WlRLFAgAPPvggJk2ahNWrV7uunz9/HqNGjcKQIT0P+yRJwtixY/GPf/xDxqo9YxEjIhLkZOOVfjuwGzkBXOy8jpONV2TPdaujWACgubkZlZWVOH78ONrb2/Hiiy96vD+g5yT9YGARIyISxHj15jEmAxnnzY1RLABcUSyvvPKK61qwo1gAYOzYsQCA4cOH49lnn3U1dowZMwYtLS3o7u52zX/+/HnXeJFYxIiIBIkZ4d8Btv6O80YJUSwdHR24du0aAMDhcGDPnj3IysrqWV9MDLKysrB7924AwL59+5CYmOiWPSYKuxOJiASZnnQ74iOGoa3zusf3YhKAuIhhmJ7k/V2Wv251FMuZM2fwzDPPQJIkdHd3Y/LkyfjjH//o+nNbtmzBU089haKiIoSHh2Pnzp2y1+wJo1iIiHwYaFRIb3ci4DlP7H/nTRbWZh9saohi4eNEIiKBstPj8b/zJiMuwv0v5LiIYaoqYGrBx4lERIJlp8fj3wxxONl4Bcar1xEzoucRolajrjwxNUSxsIgREQWBViPh7uTIW30bP3t8nEhERKrFIkZERKrFIkZERKrFd2JERMHgsAPNxwHLJSAsFki4B9AE5yT5f2XciRERiVb3EfCHdGDnI8C+lT3/+4f0nuuCKD1P7Fe/+hUSExMhSRJqamqCdg/ciRERiVT3EbB3KdD3zA7zxZ7rubsAw6Oyp1F6ntjjjz+Ol156Cffdd19Q5u3FnRgRkSgOO1DyMvoVMOCnayUFPeNkUHqeGADcf//9uOOOO2St0x/ciRERidJ8HDC3+hjgBMwXesYlzQp4Gn/zxKqrq5GSkgKj0QiDwYCysjJMnDgR7777LnJzc/16zNebJ7ZhwwZUVFQgJycHDQ0NGD58OICePDGbzYa5c+fi9ddfd10fLNyJERGJYrkkdpwPSs8TGywsYkREooTFih3nhdLzxAYTixgRkSgJ9wDho/DTmfV9SUD46J5xMig9T2ww8Z0YEZEoGi2Q/eY/uxMleAxjyX5DyPdiSs8TW7VqFT788EO0tbVh3rx5CAsLw3fffSd73X0xT4yIyIeA8q7qPurpUryxySN8dE8BE9BeP1jUkCfGnRgRkWiGR4HUh3lixyBgESMiCgaNVlYbvRKoIU+MjR1ERKRaLGJERKRaLGJERKRaLGJERKRaLGJEREFgd9hxqu0U/t+5/4dTbadgl3nob19KjmK5fv06cnJyMH78eGRmZiI7OxtNTU1BuQd2JxIRCXa4+TDeOPkGLl376YzE2NBYFEwvwLyEeULmUHoUS35+Ph566CFIkoS3334b+fn5+PTTT4XPz50YEZFAh5sPY/WR1W4FDACM14xYfWQ1Djcflj2H0qNYhg0bhl/84heuMxZnzpyJc+fOyV63J9yJEREJYnfY8cbJN+D0kCfmhBMSJLx58k3MHjMbWhkfPqstiuVPf/oTFi5cGPB6feFOjIhIkCpjVb8d2I2ccKLtWhuqjFWy51JLFEtRURHq6+vx+9//Xt6CvWARIyISpP1au9Bx3qglimXDhg3Yv38/PvnkE4SGhg5wlf5hESMiEiQ6NFroOG/UEMWyceNGFBcX4y9/+Qv0er2s9frCd2JERIJMjpmM2NBYGK8ZPb4XkyAhNjQWk2Mmy55LyVEsLS0teOGFFzBu3DjMnj0bAKDT6XDixAnZ6+6LUSxERD4MNCqktzsRgFshk/6ZJ7bxwY3C2uyDTQ1RLHycSEQk0LyEedj44EbEhMa4XY8NjVVVAVMLPk4kIhJsXsI8zB4zG1XGKrRfa0d0aDQmx0yW1VZ/K6ghioVFjIgoCLQaLabFTbvVt/Gzx8eJRESkWixiRESkWixiRESkWnwnRkQUBE67Hdcq/47u9nYMiY5G6NQpkIJ0kvy/MhYxIiLBzJ9+iktF69Hd1ua6NiQuDrGvvoLw+fOFzGGz2VBUVITi4mJotVqEhIQgISEBa9eu7RfFEiySJGHixInQaHoe6r311luYNWsWAGD+/Ploa2uDRqPBiBEj8NZbbwXlvljEiIgEMn/6KS78+nmgzzkS3Zcu9Vz/4x+EFDKl54nt3bvXddzUBx98gBUrVqCqSv7Bx33xnRgRkSBOux2Xitb3K2A9P/Zcu1S0Hk67vJRnpeeJAXA7L7Gzs9O1WxONOzEiIkGuVf7d7RFiP04nutvacK3y7xg+Y3rA86glT2zp0qUoKysDAJSUlAS8Xl+4EyMiEqS73b+IFX/H+aKGPLFdu3bh/PnzWLduncesMRFYxIiIBBkS7V/Eir/jvFFLnlivZcuWoayszBX3IhKLGBGRIKFTp2BIXBzgoWAAACQJQ+LiEDp1iqx5lJ4nZjab0dra6vp3HDhwAJGRkT4ffwaK78SIiASRtFrEvvpKTxeiJLk3ePyzsMW++oqQ78WUnCfW2dmJRYsW4YcffoBGo0F0dDQOHTrkcTcoF/PEiIh8CCTvajC+ExsMasgT406MiEiw8PnzMWLuXJ7YMQhYxIiIgkDSamW10SuBGvLE2NhBRESqxSJGRESqxSJGRESqxSJGRESqxSJGRBQEDocTF77twNlTbbjwbQccDrFfM9lsNhQWFiI1NRVpaWnIyspCTk4OTp8+LXQeXyRJQkZGBjIzM5GZmenxxI7CwkJIkuTXOY2BYHciEZFgDdVGHNtTjy6T1XVtuF6HWU+kIDkrRsgcSo9iAYCqqipUVFS4jqcKBu7EiIgEaqg2omRLjVsBA4AukxUlW2rQUG2UPYcaolisVitWrVqFTZs2BeWkjl7ciRERCeJwOHFsT73PMV/srUfSpGhoNIH/xa6GKJbXXnsNeXl5SEpKCnid/uBOjIhIkIv1pn47sL4sHVZcrDfJnkvJUSzl5eU4deoUnn32WdnrvBkWMSIiQbrMvgvYQMd5o/QolqNHj+Kbb75BUlISEhMT0dLSggULFuCTTz4JcMXesYgREQkyPFwndJw3So9iKSgoQGtrK5qamtDU1IQ77rgDpaWleOihh2St2xO+EyMiEiQ+RY/hep3PR4phI3WIT9HLnkvJUSyDiVEsREQ+DDQqpLc70ZvsZ9KFtdkHmxqiWPg4kYhIoOSsGGQ/k47hevdHhmEjdaoqYGrBx4lERIIlZ8UgaVJ0T7ei2Yrh4T2PEOW01d8KaohiYREjIgoCjUbC6LtG3nwgycLHiUREpFosYkREpFosYkREpFp8J0ZEFAQOhx0Xvq6FxdSBMP1IjJ6QBo0mOCfJ/ytjESMiEqz+xHH8dcdWWK781NkXdnsU5jyVj5QZ9wiZw2azoaioCMXFxdBqtQgJCUFCQgLWrl3bL4olWCRJwsSJE6HR9DzUe+uttzBr1iwAPZ2Nw4YNc3379corr+CJJ54Qfg8sYkREAtWfOI6PNhb1u2658j0+2liER1e/KqSQqSFP7P3330d6enpQ5u3Fd2JERII4HHb8dcdWn2PKdm6Fw2H3OeZm1JAnNli4EyMiEuTC17VujxA9uXr5e1z4uhZj0jICnkcNeWJAT+SLw+HAjBkzsH79ekRHRwe8Zm+4EyMiEsRi6hA6zhcl54kBwOeff44vv/wSVVVViIyMxLJly2Sv2RMWMSIiQcL0/p3Q4e84b5SeJ3bjb0OHDsXzzz/v9ptILGJERIKMnpCGsNujfI4ZERmF0RPSZM2j9Dyxrq4ut/sqLi52/SYa34kREQmi0Wgx56l8j92JvWYvyxfyvZiS88QuXbqERYsWwW63w+l0Yty4cdi1a5fsNXvCPDEiIh8Cybvy9J3YiMgozF4m7juxwaCGPDHuxIiIBEuZcQ+Sp83giR2DgEWMiCgINBqtrDZ6JVBDnhgbO4iISLVYxIiISLVYxIiISLVYxIiISLVYxIiIgsDpcOJ6gwnXThtxvcEEp0Ps10w2mw2FhYVITU1FWloasrKykJOTg9OnTwudxxdJkpCRkYHMzExkZma6ncphtVrx3HPPISUlBWlpacjLywvKPbA7kYhIsB9qvofpYAPsnT+6rmkjQqBfmIzb0n2f6OEvpUexFBQUQKPR4OzZs5Akya9zGgPBnRgRkUA/1HyPy7u/ditgAGDv/BGXd3+NH2rkt6wrPYqlq6sL27dvR1FRkeucxfj4eNnr9oRFjIhIEKfDCdPBBp9jTAfPyX606G8Uy5o1a1BZWYnk5GTk5eVh586dOHPmDPLz85Gbm+vXXL1RLCdOnMC2bduwePFitzMaH3zwQUyaNAmrV692XW9oaEBkZCTWrVuHqVOnYtasWfjss89krdkbFjEiIkGsjZ39dmB92TutsDZ2yp5LyVEsNpsN586dg8FgQGVlJd5++208+eSTaG9vl73uvljEiIgEcVz1XcAGOs4bpUexJCQkQKPRYMmSJQCASZMmISkpCbW1tYEs1ycWMSIiQTQjQoSO80bpUSxRUVGYO3cuSktLAfTs2BobG3HXXXfJWrcn7E4kIhJElxQBbUSIz0eK2ggddEkRsudSchQLAGzevBkrVqzAyy+/DK1Wi61btwaluYNRLEREPgw0KqS3O9GbyLwJwtrsg00NUSx8nEhEJNBt6VGIzJsAbYT7I0NthE5VBUwt+DiRiEiw29KjMMwQCWtjJxxXf4RmRAh0SRGQNP2bK5RMDVEsLGJEREEgaSQMS9bf6tv42ePjRCIiUi0WMSIiUi0WMSIiUi2+EyMiCgKHw4Hm5mZYLBaEhYW5TrEgsVjEiIgEq6urQ0lJCcxms+taeHg4srOzYTAYhMxhs9lQVFSE4uJiaLVahISEICEhAWvXru0XxRIskiRh4sSJruL81ltvYdasWTCZTHjwwQdd465du4Zz587BaDT6PLQ4ECxiREQC1dXVuU7DuJHZbMbevXuRm5srpJApOU9Mr9e7hXNu2LABR48eFV7AAL4TIyISxuFwoKSkxOeYkpISOBwOWfMoPU+sr+3bt2PlypWy1uwNd2JERII0Nze7PUL0xGw2o7m5GUlJSQHP42+eWHV1NVJSUmA0GmEwGFBWVoaJEyfi3XffRW5uLmpqam46V2+e2IYNG1BRUYGcnBw0NDRg+PDhAHryxGw2G+bOnYvXX3/ddb1XeXk5Ll++jEceeSTg9frCnRgRkSAWi0XoOF+UnCd2o3feeQdLly7FkCHB2TOxiBERCdL33ZDccd4oPU+sV1dXF/bs2YMVK1YMcIX+YxEjIhIkISEB4eHhPseEh4cjISFB1jxKzxPr9d577yEjIwOpqamy1usL34kREQmi0WiQnZ3tsTuxV3Z2tpDvxZSeJwYA27ZtC1pDRy/miRER+RBI3tVgfCc2GNSQJ8adGBGRYAaDAampqTyxYxCwiBERBYFGo5HVRq8EasgT438WEBGRarGIERGRarGIERGRarGIERGRarGxg4goCJxOO0ymU7BajdDpYqDXT4MkBeck+X9l3IkREQlmNJbib8fvR1X1EtTW/X+oql6Cvx2/H0ZjqbA5bDYbCgsLkZqairS0NGRlZSEnJ8ctAiXYJElCRkYGMjMzkZmZ6XbsVGlpKaZMmYKsrCykp6dj586dwbkHfuxMROTdQD/GNRpL8VXNKgB9/2rtOW9wYvp/ISZmgez7ysvLg8Viwfbt293yxMxmM5YsWeI2Nlh5YpIk4erVq/3OgnQ6nYiKikJZWRkyMjLQ1NSE1NRUtLe3Y8SIEW5j5X7szJ0YEZEgTqcdZ+t/h/4FDK5rZ+tfh9Np9/C7/9SSJ9Z7rqPZbEZkZCR0Op2sdXvCd2JERIL0vANr8zHCCav1IkymUxg5cmbA8yg9T0ySJOzduxePPfYYhg8fjo6ODuzfvx8hISEBr9kb7sSIiASxWo1Cx/mi5Dyx7u5urF+/Hh9++CGam5vx2WefYdmyZbhy5YrsdffFIkZEJIhOFyN0nDdKzxM7ffo0Wltbce+99wIApk2bhlGjRrmKn0gsYkREguj106DTxaG3iaM/CTpdPPT6abLmUXqe2JgxY9DS0oJvv/0WAPDdd9+hoaEB48ePl7VuT/hOjIhIEEnSYnzKa//sTpTg3uDRU9jGp6wR8r2YkvPEYmNjsWXLFjz++OPQaDRwOp3YtGkTRo8eLXvdfbHFnojIh0BawI3GUpyt/51bk4dOF4/xKWuEtNcPFuaJERH9C4qJWYDo6Hk8sWMQsIgREQWBJGlltdErAfPEiIiIgohFjIiIVItFjIiIVItFjIiIVItFjIgoCOxOJ/7WcRUHLnXgbx1XYRf8NZPSo1hKSkowdepUZGRkYObMmUE5rQNgdyIRkXAft5vwm/oLuGi1ua7F64ZiXcpoPBytFzLH8uXLYbFYUF5e7hbFUltbi8zMTLexwYpiAYDjx4/3i2Lp6OhAXl4ejh07hgkTJuDo0aNYsmSJXwcODxR3YkREAn3cbsLTNU1uBQwA2qw2PF3ThI/bTbLnUHoUS0NDA2JiYjBhwgQAwAMPPIDm5mZUVVXJXntfLGJERILYnU78pv6CjzQxYE39BdmPFv2NYlmzZg0qKyuRnJyMvLw87Ny5E2fOnEF+fj5yc3P9mqs3iuXEiRPYtm0bFi9e7HZG44MPPohJkyZh9erVruspKSlob29HRUUFAODAgQOwWCxoamoKfNFesIgREQlSYbL024HdyAmg1WpDhckiey4lR7FERERg3759KCgowJQpU3DkyBEYDAYMHTpU9rr74jsxIiJBjD92Cx3nzY1RLCNHjnRFsezYsQOHDh0CcGuiWPLz811j7r//fhw5cgQAYLVaERcX53q8KBJ3YkREgsSE+Lcv8HecN0qPYgHgtst7/fXXMWfOHNx5552y1u0Jd2JERILM1IchXjcUbVabx/diEnq6FGfqwzz8OjBKjmIBgDVr1uCLL75Ad3c37r77bmzbtk32mj1hFAsRkQ8DjQrp7U4EPKWJAX9OTxTWZh9saohi4eNEIiKBHo7W48/piYjTuTcxxOuGqqqAqQUfJxIRCfZwtB7ZURGoMFlg/LEbMSFDMFMfBq2H5golU0MUC4sYEVEQaCUJ944ccatv42ePjxOJiEi1WMSIiEi1WMSIiEi1+E6MiCgI7A4nTjZegfHqdcSMGIbpSbdDq1FXY4casIgREQlWUnMRhQfrcLHzpyOc4iOG4bcLDchOjxcyh81mQ1FREYqLi6HVahESEoKEhASsXbu2XxRLsHR0dOC5557DyZMnMWTIEPz7v/873njjDQA9J+0vW7YM33//PfR6PXbs2AGDwSD8Hvg4kYhIoJKai/jl7iq3AgYAbZ3X8cvdVSipufmhu/5Yvnw5qqurUV5ejtraWlRXV2PlypWora3tN/bGsxFFWrFihescx6+//hq//vWvXb8988wzyM/Px9mzZ/HSSy9h5cqVQbkHFjEiIkHsDicKD9b5jGIpPFgHu0PeQUlKyBP77rvvUFVVhdWrV7v+bHx8zy7TaDSiqqoKeXl5AIBFixahsbGRUSxEREp2svFKvx3YjZwALnZex8nGK7LmUUKeWF1dHcaMGYP/+T//JyZPnoz58+ejuroaAHD+/HmMGjUKQ4b0vLGSJAljx47FP/7xD1nr9oRFjIhIEOPVm8eYDGScL7c6T8xms6G8vBz/8R//gaqqKrzwwgtYuHAhuru7+90f0BMHEwwsYkREgsSM8O8AW3/HeXNjnhgAV57YK6+84roW7DyxhIQEjB49GrNnzwYALFiwAD/++CNaWlowZswYtLS0uAqa0+nE+fPnXfljIrGIEREJMj3pdsRHDIO3RnoJPV2K05O8Pwb0hxLyxKZMmYLw8HCcOXMGAFBZWQmgJ9olJiYGWVlZ2L17NwBg3759SExMdL1rE4kt9kREgmg1En670IBf7q6CBM9RLL9daBDyvditzhPrvYenn34a169fx7Bhw7Bv3z4MHdpzev+WLVvw1FNPoaioCOHh4di5c6fsNXvCPDEiIh8CybsajO/EBoMa8sS4EyMiEiw7PR7/ZojjiR2DgEWMiCgItBoJdydH3urbkEUNeWJs7CAiItViESMiItViESMiItViESMiItViESMiCgaHHWg8Bnz1fs//OsSeJG+z2VBYWIjU1FSkpaUhKysLOTk5OH36tNB5fOno6MCSJUuQkpKCCRMmoKCgwPXbr371KyQmJkKSJNTU1ATtHtidSEQkWt1HQMnLgLn1p2vho4DsNwHDo0KmWL58OSwWC8rLy10n2R88eBC1tbX98sTsdju0Wq2QeW+0YsUK3Hvvva4TPW48i/Hxxx/HSy+9hPvuu0/4vDfiToyISKS6j4C9S90LGACYL/Zcr/tI9hRKj2IBgPvvvx933HGH7LXeDHdiRESiOOw9OzCviWISUFIApD4MaALfGfkbxVJdXY2UlBQYjUYYDAaUlZVh4sSJePfdd5Gbm+vXY77eKJYNGzagoqICOTk5aGhocItiqaysRFRUFN58801kZWUFvK5AcCdGRCRK8/H+OzA3TsB8oWecTEqPYhksLGJERKJYLokd54XSo1gGE4sYEZEoYbFix3mh9CiWwcR3YkREoiTc09OFaL4Iz+/FpJ7fE+6RPZXSo1hWrVqFDz/8EG1tbZg3bx7CwsLw3XffyV53X4xiISLyYcBRIb3diQA8Jorl7hLWZh9saohi4eNEIiKRDI/2FKrwPrlh4aNUVcDUgo8TiYhEMzza00bffLyniSMstucRooy2+ltBDVEsLGJERMGg0QJJs271Xfzs8XEiERGpFosYERGpFosYERGpFt+JEREFgd1hR5WxCu3X2hEdGo3JMZOhVVljhxqwiBERCXa4+TDeOPkGLl376Xip2NBYFEwvwLyEeULmsNlsKCoqQnFxMbRaLUJCQpCQkIC1a9f2i2IJlo6ODjz33HM4efIkhgwZgn//93/HG2+8gevXr+PJJ59EXV0dQkNDERcXh82bN7tOxxeJjxOJiAQ63HwYq4+sditgAGC8ZsTqI6txuPmwkHmWL1+O6upqlJeXo7a2FtXV1Vi5ciVqa2v7jb3xbESRVqxY4TrH8euvv8avf/1r12/5+fn49ttvcfr0aTzyyCPIz88Pyj2wiBERCWJ32PHGyTfg9HDkVO+1N0++CbvMlGel54kNGzYMv/jFL1yHDs+cORPnzp2TtWZv+DiRiEiQKmNVvx3YjZxwou1aG6qMVZgWNy3gedSWJ/anP/0JCxcuDHi9vnAnRkQkSPu1dqHjfFFLnlhRURHq6+vx+9//XvaaPWERIyISJDo0Wug4b9SSJ7Zhwwbs378fn3zyCUJDQwNb7E2wiBERCTI5ZjJiQ2MhoX/BAAAJEuJC4zA5ZrKsedSQJ7Zx40YUFxfjL3/5C/R6vaz1+sJ3YkREgmg1WhRML8DqI6shQXJr8OgtbC9Pf1nI92JKzhNraWnBCy+8gHHjxrl2ajqdDidOnJC97r6YJ0ZE5EMgeVeevhOLC43Dy9NfFvad2GBQQ54Yd2JERILNS5iH2WNm88SOQcAiRkQUBFqNVlYbvRKoIU+MjR1ERKRaLGJERKRaLGJERKRaLGJERKRaLGJEREHgtNvRdeIkOg99jK4TJ+EUfJK8zWZDYWEhUlNTkZaWhqysLOTk5OD06dNC5/Glo6MDS5YsQUpKCiZMmICCggLXb/Pnz0dGRgYyMzMxa9asoN0XuxOJiAQzf/opLhWtR3dbm+vakLg4xL76CsLnzxcyx/Lly2GxWFBeXu46yf7gwYOora3tlydmt9uh1Ypv71+xYgXuvfde14keN57FuHfvXtdJHR988AFWrFiBqqoq4ffAnRgRkUDmTz/FhV8/71bAAKD70iVc+PXzMH/6qew5lB7FAsDtqKnOzk5oNMEpN9yJEREJ4rTbcaloPeDpICSnE5AkXCpajxFz50KSsTNSSxTL0qVLUVZWBgAoKSkJeL2+cCdGRCTItcq/99uBuXE60d3WhmuVf5c9lxqiWHbt2oXz589j3bp1ePHFF2Wv2RMWMSIiQbrb/csJ83ecN2qJYum1bNkylJWVuU7KF4lFjIhIkCHR/uWE+TvOG6VHsZjNZrS2trr+HQcOHEBkZKTPx5+B4jsxIiJBQqdOwZC4OHRfuuT5vZgkYUhsLEKnTpE9l5KjWNra2rBo0SL88MMP0Gg0iI6OxqFDhzzuBuViFAsRkQ8DjQrp7U4E4F7I/vkX+Og//kFYm32wqSGKhY8TiYgECp8/H6P/+AcMiY11uz4kNlZVBUwt+DiRiEiw8PnzMWLu3J5uxfZ2DImORujUKbLa6m8FNUSxsIgREQWBpNVi+Izpt/o2fvb4OJGIiFSLRYyIiFSLRYyIiFSL78SIiILA4XDiYr0JXWYrhofrEJ+ih0Yj/jupf3XciRERCdZQbcSuV4/jg/9Vjb9sq8MH/6sau149joZqo7A5lJ4n1quwsBCSJPl12HAguBMjIhKoodqIki39/8LuMllRsqUG2c+kIzkrRvY8Ss8TA4CqqipUVFRg7NixwufuxZ0YEZEgDocTx/bU+xzzxd56OBzyDkpSQ56Y1WrFqlWrsGnTpqAcN9WLOzEiIkEu1pvQZbL6HGPpsOJivQmj7xrpc5wvasgTe+2115CXl4ekpKSA1+kP7sSIiATpMvsuYAMd54uS88TKy8tx6tQpPPvss7LXeTMsYkREggwP1wkd543S88SOHj2Kb775BklJSUhMTERLSwsWLFiATz75RNa6PWERIyISJD5Fj+F63wUqbGRPu70cSs8TKygoQGtrK5qamtDU1IQ77rgDpaWleOihh2St2xO+EyMiEkSjkTDriRSP3Ym97stNEfK9mJLzxAYT88SIiHwIJO+qodqIY3vq3Zo8wkbqcF9uipD2+sGihjwx7sSIiARLzopB0qRontgxCFjEiIiCQKORZLXRK4Ea8sTY2EFERKrFIkZERKrFIkZERKrFIkZERKrFIkZEFAQOhx3na8/g678dxfnaM3A47Df/QwOg9CiWxMREpKamIjMzE5mZmdizZ09Q7oHdiUREgtWfOI6/7tgKy5WfOvvCbo/CnKfykTLjHiFzqCGK5f3330d6errweW/EnRgRkUD1J47jo41FbgUMACxXvsdHG4tQf+K4/DlUEMUyWFjEiIgEcTjs+OuOrT7HlO3cKvvRor9RLGvWrEFlZSWSk5ORl5eHnTt34syZM8jPz0dubq5fc/VGsZw4cQLbtm3D4sWL0dXV5RbFMnnyZMyfPx/V1dVuf3bJkiWYOHEinn76abS3t8taszcsYkREglz4urbfDqyvq5e/x4Wva2XPpeQoFgD4/PPP8eWXX6KqqgqRkZFYtmyZ7DV7wiJGRCSIxdQhdJw3So9iAYCxY8cCAIYOHYrnn38ex44dC3C1vrGIEREJEqb375gpf8d5o/Qolq6uLrf7Ki4udiU+i8buRCIiQUZPSEPY7VE+HymOiIzC6AlpsudSchTL+fPnsWjRItjtdjidTowbNw67du2SvWZPGMVCROTDQKNCersTvXl09avC2uyDTQ1RLHycSEQkUMqMe/Do6lcRdnuU2/URkVGqKmBqwceJRESCpcy4B8nTZvR0K5o6EKYfidET0qDRiP/gOJjUEMXCIkZEFAQajRZj0jJu9W387PFxIhERqRaLGBERqRaLGBERqRbfiRERBYHT4YS1sROOqz9CMyIEuqQISJr+p2aQPCxiRESC/VDzPUwHG2Dv/NF1TRsRAv3CZNyWHuXjT/rPZrOhqKgIxcXF0Gq1CAkJQUJCAtauXdsviiVYOjo68Nxzz+HkyZMYMmQI/v3f/x1vvPEGAMBqteKFF15AaWkpQkJCkJWVhd27dwu/BxYxIiKBfqj5Hpd3f93vur3zR1ze/TUi8yYIKWRKzxMrKCiARqPB2bNnIUmSX4cNB4LvxIiIBHE6nDAdbPA5xnTwHJwOeQclKT1PrKurC9u3b0dRUZHr4OFgZY2xiBERCWJt7HR7hOiJvdMKa2OnrHmUnifW0NCAyMhIrFu3DlOnTsWsWbPw2WefyVqzNyxiRESCOK76LmADHeeLkvPEbDYbzp07B4PBgMrKSrz99tt48skngxKMySJGRCSIZkSI0HHeKD1PLCEhARqNBkuWLAEATJo0CUlJSaitlR8G2heLGBGRILqkCGgjfBcobYQOuqQIWfMoPU8sKioKc+fORWlpKQCgubkZjY2NuOuuu2St2xN2JxIRCSJpJOgXJnvsTuylXzhOyPdiSs4TA4DNmzdjxYoVePnll6HVarF169agNHcwT4yIyIdA8q48fyemg37hOGHfiQ0GNeSJcSdGRCTYbelRGGaI5Ikdg4BFjIgoCCSNhGHJ+lt9G7KoIU+MjR1ERKRaLGJERKRaLGJERKRaLGJERKRabOwgIgoCh8OB5uZmWCwWhIWFuU6xILFYxIiIBKurq0NJSQnMZrPrWnh4OLKzs2EwGITMoeQ8MZPJhAcffNA17tq1azh37hyMRqPPQ4sDwSJGRCRQXV2d6zSMG5nNZuzduxe5ublCCpmS88T0ej1Onz7tGrdhwwYcPXpUeAED+E6MiEgYh8OBkpISn2NKSkrgcDhkzaP0PLG+tm/fjpUrV8paszfciRERCdLc3Oz2CNETs9mM5uZmJCUlBTyPv3li1dXVSElJgdFohMFgQFlZGSZOnIh3330Xubm5qKmpuelcvXliGzZsQEVFBXJyctDQ0OCWJ1ZZWYmoqCi8+eabyMrKcvvz5eXluHz5Mh555JGA1+sLd2JERIJYLBah43xRcp7Yjd555x0sXboUQ4YEZ8/EIkZEJMiNGV4ixnmj9DyxXl1dXdizZw9WrFgR2EL9wCJGRCRIQkICwsPDfY4JDw9HQkKCrHmUnifW67333kNGRgZSU1NlrdcXvhMjIhJEo9EgOzvbY3dir+zsbCHfiyk9TwwAtm3bFrSGjl7MEyMi8iGQvKvB+E5sMDBPjIjoX5DBYEBqaipP7BgELGJEREGg0WhktdErAfPEiIiIgohFjIiIVItFjIiIVItFjIiIVItFjIgoCJxOOzo6KtDW9hE6OirgdNpv/ocGwGazobCwEKmpqUhLS0NWVhZycnLcTo8Pto6ODixZsgQpKSmYMGECCgoKXL+VlpZiypQpyMrKQnp6Onbu3BmUe2B3IhGRYEZjKc7W/w5Wa5vrmk4Xh/EpryEmZoGQOZQcxeJ0OrF48WKUlZUhIyMDTU1NSE1NxWOPPYYRI0YIvQfuxIiIBDIaS/FVzSq3AgYAVuslfFWzCkZjqew51BLF0nskltlsRmRkJHQ6ney198WdGBGRIE6nHWfrfwfA00FITgASzta/jujoeZCkwHdGSo9ikSQJe/fuxWOPPYbhw4ejo6MD+/fvR0hISMBr9oY7MSIiQUymU/12YO6csFovwmQ6JXsuJUexdHd3Y/369fjwww/R3NyMzz77DMuWLcOVK1dkr7svFjEiIkGsVqPQcd4oPYrl9OnTaG1txb333gsAmDZtGkaNGoUvv/wy8EV7wSJGRCSIThcjdJw3So9iGTNmDFpaWvDtt98C6Hl/1tDQgPHjx8tatyd8J0ZEJIhePw06XRys1kvw/F5Mgk4XB71+muy5lBzFEhsbiy1btuDxxx+HRqOB0+nEpk2b3LLGRGEUCxGRDwONCuntTuxx41+vPY/zJqb/l7A2+2BTQxQLHycSEQkUE7MAE9P/CzpdrNt1nS5OVQVMLfg4kYhIsJiYBYiOnvfPbkUjdLoY6PXTZLXV3wpqiGJhESMiCgJJ0mLkyJm3+jZ+9vg4kYiIVItFjIiIVItFjIiIVIvvxIiIgsDudKLCZIHxx27EhAzBTH0YtB5OzSB5uBMjIhLs43YTppbXYdHpBvyyrhmLTjdgankdPm43CZtD6XliJSUlmDp1KjIyMjBz5sygHDkFcCdGRCTUx+0mPF3T1O+8jjarDU/XNOHP6Yl4OFovex4l54l1dHQgLy8Px44dw4QJE3D06FEsWbLEr1PzB4o7MSIiQexOJ35Tf8FrEAsArKm/ALvMg5KUnifW0NCAmJgYTJgwAQDwwAMPoLm5GVVVVbLW7QmLGBGRIBUmCy5abV5/dwJotdpQYbLImsffPLE1a9agsrISycnJyMvLw86dO3HmzBnk5+cjNzfXr7l688ROnDiBbdu2YfHixejq6nLLE5s8eTLmz5+P6upqAD0HFLe3t6OiogIAcODAAVgsFjQ1NclatycsYkREghh/7BY6zhcl54lFRERg3759KCgowJQpU3DkyBEYDAYMHTpU9rr74jsxIiJBYkL8+yvV33He3JgnNnLkSFee2I4dO3Do0CEAtzZPrPc0/CNHjgAArFYr4uLiXI8XReJOjIhIkJn6MMTrhsJbI70EYJRuKGbqw7yM8I/S88QAuO3yXn/9dcyZMwd33nmnrHV7wp0YEZEgWknCupTReLqmCRI8BbEAr6eMFvK9mJLzxABgzZo1+OKLL9Dd3Y27774b27Ztk71mT5gnRkTkQyB5Vx+3m/Cb+gtuTR6jdEPxespoIe31g0UNeWLciRERCfZwtB7ZURE8sWMQsIgREQWBVpJw78gRt/o2ZFFDnhgbO4iISLVYxIiISLVYxIiISLVYxIiISLVYxIiIgsDucKK84TI+PH0B5Q2XYXeI/ZrpVkex1NXVITMz0/VPYmKi21mO9fX1uOeeezB+/HhMnz7ddeCwaOxOJCISrKTmIgoP1uFi509HOMVHDMNvFxqQnR4vZI5bHcViMBjcCuZzzz3ndrTVM888g/z8fDz11FN4//33sXLlSpSXlwu9B4A7MSIioUpqLuKXu6vcChgAtHVexy93V6Gk5uaH7t6MEqJYbmS1WvF//+//xcqVKwEARqMRVVVVyMvLAwAsWrQIjY2NQTnFnjsxIiJB7A4nCg/Wec0TkwAUHqzDvxnioNUE/uGzv1Es1dXVSElJgdFohMFgQFlZGSZOnIh3330Xubm5foVU9kaxbNiwARUVFcjJyUFDQ4Pr6CkA2L9/P5KSklw7wPPnz2PUqFEYMqSnxEiShLFjx+If//iHq0CKwp0YEZEgJxuv9NuB3cgJ4GLndZxsvCJ7rlsdxXKjd955x7UL83R/QM9J+sHAIkZEJIjx6s1jTAYyzpsbo1gAuKJYXnnlFde1YEex9Gpubsbx48exePFi17UxY8agpaUF3d3drvnPnz+PsWPHDnClN8ciRkQkSMwI/w6w9XecN0qIYum1fft2/I//8T+g1+t/Wl9MDLKysrB7924AwL59+5CYmCj8USLAd2JERMJMT7od8RHD0NZ53eN7MQlAXMQwTE/y/i7LX0qIYnE6ndixYwe2b9/e7/62bNmCp556CkVFRQgPD8fOnTtlr9kTRrEQEfkw0KiQ3u5EwHOe2P/OmyyszT7Y1BDFwseJREQCZafH43/nTUZchPtfyHERw1RVwNSCjxOJiATLTo/HvxnicLLxCoxXryNmRM8jRDlt9beCGqJYWMSIiIJAq5Fwd3Lkrb6Nnz0+TiQiItViESMiItViESMiItXiOzEiomBw2IHm44DlEhAWCyTcA2jEniRPLGJEROLVfQSUvAyYW3+6Fj4KyH4TMDwqZAqbzYaioiIUFxdDq9UiJCQECQkJWLt2bb8olmCoq6tzO2rKZDLBbDbjypWecyF/9atf4aOPPkJzczO++uorpKenB+U+WMSIiESq+wjYuxToe2aH+WLP9dxdQgqZ0vPEHn/8cbz00ku47777hM7bF9+JERGJ4rD37MC8hrEAKCnoGSeD0vPEAOD+++/HHXfcIWud/uBOjIhIlObj7o8Q+3EC5gs945JmBTyN0vPEBhN3YkREolguiR3ng9LzxAYLixgRkShhsWLHeaH0PLHBxCJGRCRKwj09XYjwdkaiBISP7hkng9LzxAYT34kREYmi0fa00e9dip5C5iGMJfsNId+LKT1PbNWqVfjwww/R1taGefPmISwsDN99953sdffFPDEiIh8Cyrvy+J3Y6J4CJug7scGghjwx7sSIiEQzPAqkPswTOwYBixgRUTBotLLa6JVADXlibOwgIiLVYhEjIiLVYhEjIiLVYhEjIiLVYmMHEVEQ2B12VBmr0H6tHdGh0ZgcMxlagd2JSo5iuX79Op588knU1dUhNDQUcXFx2Lx5s+tgYZFYxIiIBDvcfBhvnHwDl679dEZibGgsCqYXYF7CPCFzKD2KJT8/Hw899BAkScLbb7+N/Px8fPrpp0LvAeDjRCIioQ43H8bqI6vdChgAGK8ZsfrIahxuPix7DqVHsQwbNgy/+MUvXEVt5syZOHfunOx1e8KdGBGRIHaHHW+cfANOD3liTjghQcKbJ9/E7DGzZT1aVFsUy5/+9CcsXLhwwOv0B3diRESCVBmr+u3AbuSEE23X2lBlrJI9l1qiWIqKilBfX4/f//73A1+kH1jEiIgEab/WLnScN2qJYtmwYQP279+PTz75BKGhof4vcABYxIiIBIkOjRY6zhs1RLFs3LgRxcXF+Mtf/hLUmBa+EyMiEmRyzGTEhsbCeM3o8b2YBAmxobGYHDNZ9lxKjmJpaWnBCy+8gHHjxmH27NkAAJ1OhxMnTshed1+MYiEi8mGgUSG93YkA3AqZ9M88sY0PbhTWZh9saohi4eNEIiKB5iXMw8YHNyImNMbtemxorKoKmFrwcSIRkWDzEuZh9pjZQT2xYzCoIYqFRYyIKAi0Gi2mxU271bfxs8fHiUREpFosYkREpFosYkREpFp8J0ZEFAROux3XKv+O7vZ2DImORujUKZAEnyRPLGJERMKZP/0Ul4rWo7utzXVtSFwcYl99BeHz5wuZQ8l5YgAwf/58tLW1QaPRYMSIEXjrrbeCcl8sYkREApk//RQXfv080Occie5Ll3qu//EPQgqZ0vPE9u7d6zpu6oMPPsCKFStQVSX/4OO++E6MiEgQp92OS0Xr+xWwnh97rl0qWg/nDQfuBkLpeWIA3M5L7OzshEYTnHLDnRgRkSDXKv/u9gixH6cT3W1tuFb5dwyfMT3gedSSJ7Z06VKUlZUBAEpKSgJb7E1wJ0ZEJEh3u38RK/6O80UNeWK7du3C+fPnsW7dOrz44ouBLfQmWMSIiAQZEu1fxIq/47xRS55Yr2XLlqGsrMwV9yISixgRkSChU6dgSFwc4KFgAAAkCUPi4hA6dYqseZSeJ2Y2m9Ha2ur6vw8cOIDIyEifjz8DxXdiRESCSFotYl99pacLUZLcGzz+WdhiX31FyPdiSs4T6+zsxKJFi/DDDz9Ao9EgOjoahw4d8rgblIt5YkREPgSSdzUY34kNBjXkiXEnRkQkWPj8+Rgxdy5P7BgELGJEREEgabWy2uiVQA15YmzsICIi1WIRIyIi1WIRIyIi1WIRIyIi1WIRIyIKAofDiQvfduDsqTZc+LYDDofYr5lsNhsKCwuRmpqKtLQ0ZGVlIScnx+1k+WCqq6tDZmam65/ExESPHzMXFhZCkiS/zmkMBLsTiYgEa6g24tieenSZrK5rw/U6zHoiBclZMULmUHoUCwBUVVWhoqICY8eOFTr3jbgTIyISqKHaiJItNW4FDAC6TFaUbKlBQ7VR9hxqiGKxWq1YtWoVNm3aFJSTOnpxJ0ZEJIjD4cSxPfU+x3yxtx5Jk6Kh0QT+F7saolhee+015OXlISkpKeB1+oM7MSIiQS7Wm/rtwPqydFhxsd4key4lR7GUl5fj1KlTePbZZ+Ut0g8sYkREgnSZfRewgY7zRulRLEePHsU333yDpKQkJCYmoqWlBQsWLMAnn3wy8MXeBIsYEZEgw8N1Qsd5o/QoloKCArS2tqKpqQlNTU244447UFpaioceekjWuj3hOzEiIkHiU/QYrtf5fKQYNlKH+BS97LmUHMUymBjFQkTkw0CjQnq7E73JfiZdWJt9sKkhioWPE4mIBErOikH2M+kYrnd/ZBg2UqeqAqYWfJxIRCRYclYMkiZF93Qrmq0YHt7zCFFOW/2toIYoFhYxIqIg0GgkjL5r5M0Hkix8nEhERKrFIkZERKrFIkZERKrFd2JEREHgcNhx4etaWEwdCNOPxOgJadBoxJ4kTyxiRETC1Z84jr/u2ArLlZ86+8Juj8Kcp/KRMuMeIXPYbDYUFRWhuLgYWq0WISEhSEhIwNq1a/tFsQRDXV2d21FTJpMJZrMZV65cAdDT2Ths2DDXt1+vvPIKnnjiCeH3wSJGRCRQ/Ynj+GhjUb/rlivf46ONRXh09atCCpka8sTef/99pKenC523L74TIyISxOGw4687tvocU7ZzKxwOu88xN6OGPLHBwp0YEZEgF76udXuE6MnVy9/jwte1GJOW4XOcL2rIEwN6Il8cDgdmzJiB9evXIzo6OuA1e8OdGBGRIBZTh9Bxvig5TwwAPv/8c3z55ZeoqqpCZGQkli1bFvhifWARIyISJEzv3wkd/o7zRul5YgAwduxYAMDQoUPx/PPP49ixYwNYof9YxIiIBBk9IQ1ht0f5HDMiMgqjJ6TJmkfpeWJdXV1u91VcXIysrCw5S/aK78SIiATRaLSY81S+x+7EXrOX5Qv5XkzJeWKXLl3CokWLYLfb4XQ6MW7cOOzatUv2mj1hnhgRkQ+B5F15+k5sRGQUZi8T953YYFBDnhh3YkREgqXMuAfJ02bwxI5BwCJGRBQEGo1WVhu9EqghT4yNHUREpFosYkREpFosYkREpFosYkREpFps7CAiCgKnwwlrYyccV3+EZkQIdEkRkDT9T80geVjEiIgE+6Hme5gONsDe+aPrmjYiBPqFybgt3feJHv5Sep6Y1WrFCy+8gNLSUoSEhCArKwu7d+8Wfh8sYkREAv1Q8z0u7/6633V754+4vPtrROZNEFLIlJ4nVlBQAI1Gg7Nnz0KSJL8OGw4E34kREQnidDhhOtjgc4zp4Dk4HfIOSlJ6nlhXVxe2b9+OoqIiV2GLj4+XtWZvWMSIiASxNna6PUL0xN5phbWxU9Y8/uaJrVmzBpWVlUhOTkZeXh527tyJM2fOID8/H7m5uX7N1ZsnduLECWzbtg2LFy/ud9Bw3zyxhoYGREZGYt26dZg6dSpmzZqFzz77LOD1+sIiRkQkiOOq7wI20HG+KDlPzGaz4dy5czAYDKisrMTbb7+NJ598Eu3t7fIW7QGLGBGRIJoRIULHeaP0PLGEhARoNBosWbIEADBp0iQkJSWhtrZ2gCu9ORYxIiJBdEkR0Eb4LlDaCB10SRGy5lF6nlhUVBTmzp2L0tJSAD2FrrGxEXfddZesdXvC7kQiIkEkjQT9wmSP3Ym99AvHCfleTMl5YgCwefNmrFixAi+//DK0Wi22bt0alOYO5okREfkQSN6V5+/EdNAvHCfsO7HBwDwxIqJ/QbelR2GYIZIndgwCFjEioiCQNBKGJetv9W3IwjwxIiKiIGIRIyIi1WIRIyIi1WIRIyIi1WJjBxFREDgcDjQ3N8NisSAsLMx1ioUoSo5iMZlMePDBB12/Xbt2DefOnYPRaPR53mMgWMSIiASrq6tDSUkJzGaz61p4eDiys7NhMBiEzKHkKBa9Xu/224YNG3D06FHhBQzg40QiIqHq6uqwd+9etwIGAGazGXv37nVFoMih9CiWvrZv3+71N7m4EyMiEsThcKCkpMTnmJKSEqSmpsp6tOhvFEt1dTVSUlJgNBphMBhQVlaGiRMn4t1330Vubi5qampuOldvFMuGDRtQUVGBnJwcNDQ0uI6eAvpHsdyovLwcly9fxiOPPBLQWm+GOzEiIkGam5v77cD6MpvNaG5ulj2XkqNY+v62dOlSDBkSnD0TixgRkSAWi0XoOG+UHsXSq6urC3v27MGKFSv8X9wAsYgREQlyY+EQMc4bpUex9HrvvfeQkZGB1NRUOcv1ie/EiIgESUhIQHh4uM9HiuHh4UhISJA9l9KjWABg27ZtQWvo6MUoFiIiHwYaFdLbnehNbm6usDb7YFNDFAsfJxIRCWQwGJCbm4vw8HC36+Hh4aoqYGrBx4lERIIZDAakpqYG9cSOwaCGKBYWMSKiINBoNEhKSrrVt/Gzp67/LCAiIroBixgREakWixgREakW34kREQWB02mHyXQKVqsROl0M9PppkCSxJ8kTixgRkXBGYynO1v8OVmub65pOF4fxKa8hJmaBkDmUnCcGAKWlpXj11VfhcDhgs9nw4osvYtmyZcLvg0WMiEggo7EUX9WsAuB+joTVeglf1azCxPT/ElLIlJwn5nQ6sXjxYpSVlSEjIwNNTU1ITU3FY489hhEjRgi9D74TIyISxOm042z979C3gP3zVwDA2frX4XTaPfzuP7XkifWe62g2mxEZGQmdTidr3Z5wJ0ZEJEjPO7A2HyOcsFovwmQ6hZEjZwY8j9LzxCRJwt69e/HYY49h+PDh6OjowP79+xESEhLwmr3hToyISBCr1Sh0nC9KzhPr7u7G+vXr8eGHH6K5uRmfffYZli1b5npfJhKLGBGRIDpdjNBx3ig9T+z06dNobW3FvffeCwCYNm0aRo0a1a/4icAiRkQkiF4/DTpdHID+BaOHBJ0uHnr9NFnzKD1PbMyYMWhpacG3334LAPjuu+/Q0NCA8ePHy1q3J3wnRkQkiCRpMT7ltX92J0pwb/DoKWzjU9YI+V5MyXlisbGx2LJlCx5//HFoNBo4nU5s2rQJo0ePlr3uvpgnRkTkQyB5V56/E4vH+JQ1wr4TGwxqyBPjToyISLCYmAWIjp7HEzsGAYsYEVEQSJJWVhu9EqghT4yNHUREpFosYkREpFosYkREpFosYkREpFosYkREQWB3OvG3jqs4cKkDf+u4Crvgr5lsNhsKCwuRmpqKtLQ0ZGVlIScnx+1k+WCqq6tDZmam65/ExES3sxxLSkowdepUZGRkYObMmUE5rQNgdyIRkXAft5vwm/oLuGi1ua7F64ZiXcpoPBytFzKHkqNYOjo6kJeXh2PHjmHChAk4evQolixZ4teBwwPFnRgRkUAft5vwdE2TWwEDgDarDU/XNOHjdpPsOZQexdLQ0ICYmBhMmDABAPDAAw+gubkZVVVVstfeF4sYEZEgdqcTv6m/4CNNDFhTf0H2o0V/o1jWrFmDyspKJCcnIy8vDzt37sSZM2eQn5+P3Nxcv+bqjWI5ceIEtm3bhsWLF/c7o7FvFEtKSgra29tRUVEBADhw4AAsFguampoCWq8vLGJERIJUmCz9dmA3cgJotdpQYbLInkvJUSwRERHYt28fCgoKMGXKFBw5cgQGgwFDhw6Vt2gP+E6MiEgQ44/dQsd5c2MUy8iRI11RLDt27MChQ4cADH4Uy3vvvec25v7778eRI0cA9DxujIuLcz1eFIk7MSIiQWJC/NsX+DvOG6VHsQBw2+W9/vrrmDNnDu68886A1+wNd2JERILM1IchXjcUbVabx/diEnq6FGfqwzz8OjBKjmIBgDVr1uCLL75Ad3c37r77bmzbtk32mj1hFAsRkQ8DjQrp7U4EPKWJAX9OTxTWZh9saohi4eNEIiKBHo7W48/piYjTuTcxxOuGqqqAqQUfJxIRCfZwtB7ZURGoMFlg/LEbMSFDMFMfBq2H5golU0MUC4sYEVEQaCUJ944ccatv42ePjxOJiPzA9oHgkPv/V+7EiIh8GDp0KCRJQnt7O6Kjoz1+b0WBcTqdaG9vhyRJAX8Ize5EIqKbsFgsaGlp4W4sCCRJwh133OH2cfaA/jyLGBHRzdntdths3o+UosAMHTpU1gn7LGJERKRabOwgIiLVYhEjIiLVYhEjIiLVYhEjIiLVYhEjIiLVYhEjIiLVYhEjIiLV+v8BXmEUwcDse4sAAAAASUVORK5CYII=", + "text/plain": [ + "
                            " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "\n", + "for i in range(n_features):\n", + " sorted_idx = X_test[:, i].argsort(axis=0).squeeze()\n", + " temp_X = X_test[sorted_idx, i]\n", + " temp_Y = Y_test[sorted_idx,]\n", + " temp_be = grp_id_test[sorted_idx, :].squeeze()\n", + " temp_yhat = yhat[sorted_idx,]\n", + " temp_s2 = ys2[sorted_idx,]\n", + "\n", + " plt.figure()\n", + " for j in range(n_grps):\n", + " scat1 = plt.scatter(temp_X[temp_be == j,], temp_Y[temp_be == j,],\n", + " label='Group' + str(j))\n", + " # Showing the quantiles\n", + " resolution = 200\n", + " synth_X = np.linspace(-4, 4, resolution)\n", + " q = nm.get_mcmc_quantiles(\n", + " synth_X, batch_effects=j*np.ones(resolution))\n", + " col = scat1.get_facecolors()[0]\n", + " plt.plot(synth_X, q.T, linewidth=1, color=col, zorder=0)\n", + "\n", + " plt.title('Model %s, Feature %d' % (model_type, i))\n", + " plt.legend()\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dev_215", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/test_blr.py b/tests/test_blr.py index 59be02c2..afaaad58 100644 --- a/tests/test_blr.py +++ b/tests/test_blr.py @@ -10,8 +10,9 @@ from pcntoolkit.model.bayesreg import BLR from pcntoolkit.model.gp import GPR from pcntoolkit.util.utils import WarpBoxCox, WarpAffine, WarpCompose, WarpSinArcsinh +import os - +workingdir = '/Users/stijndeboer/temp/BLR/' def create_noise(type_noise, N, parameters=None): """Function to create different noise distributions""" if type_noise == 'exp': @@ -112,6 +113,7 @@ def create_noise(type_noise, N, parameters=None): 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.savefig(os.path.join(workingdir, 'linear_regression.png')) plt.show() print(B.nlZ) @@ -151,11 +153,15 @@ def create_noise(type_noise, N, parameters=None): 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.savefig(os.path.join(workingdir, 'warp1.png')) + plt.show() xx = np.linspace(-5, 5, 100) plt.plot(xx, W.invf(xx, warp_param)) plt.title('estimated warping function') +plt.savefig(os.path.join(workingdir, 'warp2.png')) + plt.show() # estimate a model with heteroskedastic noise @@ -181,6 +187,8 @@ def create_noise(type_noise, N, parameters=None): print(hyp) plt.fill_between(Xs, yhat-1.96*np.sqrt(s2), yhat+1.96*np.sqrt(s2), alpha=0.2) +plt.savefig(os.path.join(workingdir, 'linear_regression2.png')) + plt.show() print("Estimate a model with site-specific noise ...") @@ -232,6 +240,8 @@ def create_noise(type_noise, N, parameters=None): 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]) @@ -239,4 +249,5 @@ def create_noise(type_noise, N, parameters=None): 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.savefig(os.path.join(workingdir, 'linear_regression3.png')) plt.show() diff --git a/tests/test_normative.py b/tests/test_normative.py index 666bce49..4a4b8e29 100644 --- a/tests/test_normative.py +++ b/tests/test_normative.py @@ -4,12 +4,11 @@ @author: andmar """ -# import pcntoolkit -from normative import estimate +from pcntoolkit.normative import estimate import os import sys # from pcntoolkit.normative import estimate -sys.path.append('/home/preclineu/andmar/sfw/PCNtoolkit/pcntoolkit') +# sys.path.append('/home/preclineu/andmar/sfw/PCNtoolkit/pcntoolkit') # wdir = '/home/mrstats/andmar/py.sandbox/normative_nimg' # wdir = '/Users/andre/data/normative_nimg' @@ -26,7 +25,7 @@ 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, +estimate(covfile=covfile, respfile=respfile, maskfile=maskfile, testresp=testresp, testcov=testcov, alg="blr") # , configparam=4) # cvfolds = 2 diff --git a/tests/test_normative_parallel.py b/tests/test_normative_parallel.py index 89644c59..1a861429 100644 --- a/tests/test_normative_parallel.py +++ b/tests/test_normative_parallel.py @@ -12,7 +12,7 @@ # configs # specify your python path. Make sure you are using the Python in the right environement. -python_path = '/path/to/my/python' +python_path = '/path/to/my/python' # specify the working directory to sacve the results. processing_dir = '/path/to/my/test/directory/' @@ -21,29 +21,31 @@ cov_num = 1 # simulating data -pd.DataFrame(np.random.random([sample_num, resp_num])).to_pickle(os.path.join(processing_dir,'train_resp.pkl')) -pd.DataFrame(np.random.random([sample_num, cov_num])).to_pickle(os.path.join(processing_dir,'train_cov.pkl')) -pd.DataFrame(np.random.random([sample_num, resp_num])).to_pickle(os.path.join(processing_dir,'test_resp.pkl')) -pd.DataFrame(np.random.random([sample_num, cov_num])).to_pickle(os.path.join(processing_dir,'test_cov.pkl')) +pd.DataFrame(np.random.random([sample_num, resp_num])).to_pickle( + os.path.join(processing_dir, 'train_resp.pkl')) +pd.DataFrame(np.random.random([sample_num, cov_num])).to_pickle( + os.path.join(processing_dir, 'train_cov.pkl')) +pd.DataFrame(np.random.random([sample_num, resp_num])).to_pickle( + os.path.join(processing_dir, 'test_resp.pkl')) +pd.DataFrame(np.random.random([sample_num, cov_num])).to_pickle( + os.path.join(processing_dir, 'test_cov.pkl')) -respfile = os.path.join(processing_dir,'train_resp.pkl') -covfile = os.path.join(processing_dir,'train_cov.pkl') +respfile = os.path.join(processing_dir, 'train_resp.pkl') +covfile = os.path.join(processing_dir, 'train_cov.pkl') -testresp = os.path.join(processing_dir,'test_resp.pkl') -testcov = os.path.join(processing_dir,'test_cov.pkl') +testresp = os.path.join(processing_dir, 'test_resp.pkl') +testcov = os.path.join(processing_dir, 'test_cov.pkl') job_name = 'nmp_test' batch_size = 1 memory = '4gb' duration = '01:00:00' cluster = 'slurm' -binary='True' +binary = 'True' execute_nm(processing_dir, python_path, job_name, covfile, respfile, - testcovfile_path=testcov, testrespfile_path=testresp, batch_size=batch_size, + testcovfile_path=testcov, testrespfile_path=testresp, batch_size=batch_size, memory=memory, duration=duration, cluster_spec=cluster, log_path=processing_dir, interactive='auto', binary=binary, savemodel='True', saveoutput='True') - - diff --git a/pytest/test_sanity.py b/tests/test_sanity.py similarity index 100% rename from pytest/test_sanity.py rename to tests/test_sanity.py diff --git a/tests/test_test.py b/tests/test_test.py new file mode 100644 index 00000000..e69de29b