From 32b848f95f2a2c82d5ee32f8bb7550d282dc2fd4 Mon Sep 17 00:00:00 2001 From: Charlotte Fraza <61728977+CharFraza@users.noreply.github.com> Date: Mon, 12 Dec 2022 16:18:42 +0100 Subject: [PATCH 01/36] Ensure kwargs str --- pcntoolkit/normative_parallel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcntoolkit/normative_parallel.py b/pcntoolkit/normative_parallel.py index 8c549b67..9854900e 100755 --- a/pcntoolkit/normative_parallel.py +++ b/pcntoolkit/normative_parallel.py @@ -899,7 +899,7 @@ def bashwrap_nm(processing_dir, # add in optional arguments. for k in kwargs: - job_call = [job_call[0] + ' ' + k + '=' + kwargs[k]] + job_call = [job_call[0] + ' ' + k + '=' + str(kwargs[k])] # writes bash file into processing dir with open(processing_dir+job_name, 'w') as bash_file: From c87126eb8d4126f772655ff35c7fd7d6a9b537f3 Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Mon, 2 Jan 2023 12:36:51 +0100 Subject: [PATCH 02/36] Updates to require python < 3.10 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 1b05d04b..bc72bf2c 100644 --- a/setup.py +++ b/setup.py @@ -24,4 +24,5 @@ 'theano==1.0.5', 'arviz==0.11.0' ], + python_requires='<3.10', zip_safe=False) From e14aaef44c407daa204ea9506bf06da2af366aee Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Mon, 23 Jan 2023 16:06:11 +0100 Subject: [PATCH 03/36] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bc72bf2c..388fa7ae 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ 'argparse', 'nibabel>=2.5.1', 'six', - 'sklearn', + 'scikit-learn', 'bspline', 'matplotlib', 'numpy>=1.19.5,<1.23', From ff6b933269b52ec6cf1668578ac8aa87e63f38ac Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Mon, 23 Jan 2023 16:17:27 +0100 Subject: [PATCH 04/36] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 388fa7ae..a64d08ef 100644 --- a/setup.py +++ b/setup.py @@ -24,5 +24,5 @@ 'theano==1.0.5', 'arviz==0.11.0' ], - python_requires='<3.10', + #python_requires='<3.10', zip_safe=False) From 0c188fce081e0536895420f1e725ecef2c2c033b Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Tue, 7 Feb 2023 10:42:18 +0100 Subject: [PATCH 05/36] changed default interpreter (for Docker) --- pcntoolkit/normative.py | 2 +- pcntoolkit/normative_parallel.py | 2 +- pcntoolkit/trendsurf.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pcntoolkit/normative.py b/pcntoolkit/normative.py index bfd50553..5153a735 100755 --- a/pcntoolkit/normative.py +++ b/pcntoolkit/normative.py @@ -1,4 +1,4 @@ -#!/Users/andre/sfw/anaconda3/bin/python +#!/opt/conda/bin/python # ------------------------------------------------------------------------------ # Usage: diff --git a/pcntoolkit/normative_parallel.py b/pcntoolkit/normative_parallel.py index 9854900e..3076ce28 100755 --- a/pcntoolkit/normative_parallel.py +++ b/pcntoolkit/normative_parallel.py @@ -1,4 +1,4 @@ -#!/.../anaconda/bin/python/ +#!/opt/conda/bin/python # ----------------------------------------------------------------------------- # Run parallel normative modelling. diff --git a/pcntoolkit/trendsurf.py b/pcntoolkit/trendsurf.py index e0f0d6e5..d7e03732 100644 --- a/pcntoolkit/trendsurf.py +++ b/pcntoolkit/trendsurf.py @@ -1,4 +1,4 @@ -#!/Users/andre/sfw/anaconda3/bin/python +#!/opt/conda/bin/python # ------------------------------------------------------------------------------ # Usage: From e99ef31611bfeeaaa32195ff3fd96b3187b12533 Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Tue, 7 Feb 2023 16:56:28 +0100 Subject: [PATCH 06/36] added Dockerfile --- docker/Dockerfile | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docker/Dockerfile diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..d79b8d22 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,28 @@ +# Installs software and cleans up unnecessary memory: the smaller, the less downtime +FROM continuumio/miniconda3:latest +RUN apt-get update +RUN apt-get install -y libhdf5-dev \ + pkg-config \ + gcc \ + g++ \ + zip + +# Make sure we have the right version of numpy +RUN conda install numpy=1.21 + +# Install directly from GitHub (master branch) +#RUN pip install scikit-learn +#RUN pip install pcntoolkit==0.26 + +# This is an alternative method that pulls from the dev branch +RUN wget https://github.com/amarquand/PCNtoolkit/archive/dev.zip +RUN unzip dev.zip +RUN pip install scikit-learn +RUN cd PCNtoolkit-dev; pip install . ; cd .. + +# Add command line links and clean up +RUN ln -s /opt/conda/lib/python3.10/site-packages/pcntoolkit /opt/ptk +RUN chmod 755 /opt/ptk/normative.py +RUN chmod 755 /opt/ptk/normative_parallel.py +RUN chmod 755 /opt/ptk/trendsurf.py +RUN rm -rf PCNtoolkit-dev dev.zip From e4996c7c23e6ef6b4f695b674326eb5ae34352a2 Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Tue, 7 Feb 2023 17:22:14 +0100 Subject: [PATCH 07/36] updated Dockerfile --- docker/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index d79b8d22..3738eff1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -25,4 +25,5 @@ RUN ln -s /opt/conda/lib/python3.10/site-packages/pcntoolkit /opt/ptk RUN chmod 755 /opt/ptk/normative.py RUN chmod 755 /opt/ptk/normative_parallel.py RUN chmod 755 /opt/ptk/trendsurf.py -RUN rm -rf PCNtoolkit-dev dev.zip +RUN echo "export PATH=${PATH}:/opt/ptk" >> ~/.bashrc +RUN rm -rf PCNtoolkit-dev dev.zip \ No newline at end of file From 23446b6d3c70c222b8cb3884906cf1d75ff1b0b0 Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Wed, 8 Feb 2023 17:55:57 +0100 Subject: [PATCH 08/36] bug in config name is resolved. --- pcntoolkit/normative_model/norm_hbr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcntoolkit/normative_model/norm_hbr.py b/pcntoolkit/normative_model/norm_hbr.py index 8ad0b11a..6f9988d5 100644 --- a/pcntoolkit/normative_model/norm_hbr.py +++ b/pcntoolkit/normative_model/norm_hbr.py @@ -101,7 +101,7 @@ def __init__(self, **kwargs): self.configs['random_sigma'] = kwargs.pop('random_noise','False') == 'True' if 'random_slope' in kwargs.keys(): print("The keyword \'random_slope\' is deprecated. It is now automatically replaced with \'random_intercept_mu\'") - self.configs['random_intercept_mu'] = kwargs.pop('random_slope','False') == 'True' + self.configs['random_slope_mu'] = kwargs.pop('random_slope','False') == 'True' ##### End Deprecations From be8b65a741fbb92759cf07336b07bcb7698940db Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Tue, 14 Feb 2023 18:52:36 +0100 Subject: [PATCH 09/36] Initial commit --- build/lib/pcntoolkit/__init__.py | 4 + build/lib/pcntoolkit/configs.py | 9 + build/lib/pcntoolkit/dataio/__init__.py | 1 + build/lib/pcntoolkit/dataio/fileio.py | 427 +++++ build/lib/pcntoolkit/model/NP.py | 82 + build/lib/pcntoolkit/model/NPR.py | 80 + build/lib/pcntoolkit/model/SHASH.py | 271 +++ build/lib/pcntoolkit/model/__init__.py | 6 + build/lib/pcntoolkit/model/architecture.py | 201 +++ build/lib/pcntoolkit/model/bayesreg.py | 568 +++++++ build/lib/pcntoolkit/model/gp.py | 488 ++++++ build/lib/pcntoolkit/model/hbr.py | 923 ++++++++++ build/lib/pcntoolkit/model/rfa.py | 243 +++ build/lib/pcntoolkit/normative.py | 1434 ++++++++++++++++ build/lib/pcntoolkit/normative_NP.py | 270 +++ .../pcntoolkit/normative_model/__init__.py | 6 + .../pcntoolkit/normative_model/norm_base.py | 60 + .../pcntoolkit/normative_model/norm_blr.py | 252 +++ .../pcntoolkit/normative_model/norm_gpr.py | 72 + .../pcntoolkit/normative_model/norm_hbr.py | 231 +++ .../lib/pcntoolkit/normative_model/norm_np.py | 229 +++ .../pcntoolkit/normative_model/norm_rfa.py | 72 + .../pcntoolkit/normative_model/norm_utils.py | 28 + build/lib/pcntoolkit/normative_parallel.py | 1275 ++++++++++++++ build/lib/pcntoolkit/trendsurf.py | 253 +++ build/lib/pcntoolkit/util/__init__.py | 1 + build/lib/pcntoolkit/util/hbr_utils.py | 236 +++ build/lib/pcntoolkit/util/utils.py | 1507 +++++++++++++++++ dist/pcntoolkit-0.26-py3.8.egg | Bin 0 -> 201350 bytes dist/pcntoolkit-0.26-py3.9.egg | Bin 0 -> 201529 bytes pcntoolkit.egg-info/PKG-INFO | 9 + pcntoolkit.egg-info/SOURCES.txt | 37 + pcntoolkit.egg-info/dependency_links.txt | 1 + pcntoolkit.egg-info/not-zip-safe | 1 + pcntoolkit.egg-info/requires.txt | 14 + pcntoolkit.egg-info/top_level.txt | 1 + requirements.txt | 67 + 37 files changed, 9359 insertions(+) create mode 100644 build/lib/pcntoolkit/__init__.py create mode 100644 build/lib/pcntoolkit/configs.py create mode 100644 build/lib/pcntoolkit/dataio/__init__.py create mode 100644 build/lib/pcntoolkit/dataio/fileio.py create mode 100644 build/lib/pcntoolkit/model/NP.py create mode 100644 build/lib/pcntoolkit/model/NPR.py create mode 100644 build/lib/pcntoolkit/model/SHASH.py create mode 100644 build/lib/pcntoolkit/model/__init__.py create mode 100644 build/lib/pcntoolkit/model/architecture.py create mode 100644 build/lib/pcntoolkit/model/bayesreg.py create mode 100644 build/lib/pcntoolkit/model/gp.py create mode 100644 build/lib/pcntoolkit/model/hbr.py create mode 100644 build/lib/pcntoolkit/model/rfa.py create mode 100644 build/lib/pcntoolkit/normative.py create mode 100644 build/lib/pcntoolkit/normative_NP.py create mode 100644 build/lib/pcntoolkit/normative_model/__init__.py create mode 100644 build/lib/pcntoolkit/normative_model/norm_base.py create mode 100644 build/lib/pcntoolkit/normative_model/norm_blr.py create mode 100644 build/lib/pcntoolkit/normative_model/norm_gpr.py create mode 100644 build/lib/pcntoolkit/normative_model/norm_hbr.py create mode 100644 build/lib/pcntoolkit/normative_model/norm_np.py create mode 100644 build/lib/pcntoolkit/normative_model/norm_rfa.py create mode 100644 build/lib/pcntoolkit/normative_model/norm_utils.py create mode 100644 build/lib/pcntoolkit/normative_parallel.py create mode 100644 build/lib/pcntoolkit/trendsurf.py create mode 100644 build/lib/pcntoolkit/util/__init__.py create mode 100644 build/lib/pcntoolkit/util/hbr_utils.py create mode 100644 build/lib/pcntoolkit/util/utils.py create mode 100644 dist/pcntoolkit-0.26-py3.8.egg create mode 100644 dist/pcntoolkit-0.26-py3.9.egg create mode 100644 pcntoolkit.egg-info/PKG-INFO create mode 100644 pcntoolkit.egg-info/SOURCES.txt create mode 100644 pcntoolkit.egg-info/dependency_links.txt create mode 100644 pcntoolkit.egg-info/not-zip-safe create mode 100644 pcntoolkit.egg-info/requires.txt create mode 100644 pcntoolkit.egg-info/top_level.txt create mode 100644 requirements.txt diff --git a/build/lib/pcntoolkit/__init__.py b/build/lib/pcntoolkit/__init__.py new file mode 100644 index 00000000..087fe624 --- /dev/null +++ b/build/lib/pcntoolkit/__init__.py @@ -0,0 +1,4 @@ +from . import trendsurf +from . import normative +from . import normative_parallel +from . import normative_NP diff --git a/build/lib/pcntoolkit/configs.py b/build/lib/pcntoolkit/configs.py new file mode 100644 index 00000000..98b56f17 --- /dev/null +++ b/build/lib/pcntoolkit/configs.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Mon Dec 7 12:51:07 2020 + +@author: seykia +""" + +PICKLE_PROTOCOL = 4 diff --git a/build/lib/pcntoolkit/dataio/__init__.py b/build/lib/pcntoolkit/dataio/__init__.py new file mode 100644 index 00000000..1208872a --- /dev/null +++ b/build/lib/pcntoolkit/dataio/__init__.py @@ -0,0 +1 @@ +from . import fileio diff --git a/build/lib/pcntoolkit/dataio/fileio.py b/build/lib/pcntoolkit/dataio/fileio.py new file mode 100644 index 00000000..37ce1ef7 --- /dev/null +++ b/build/lib/pcntoolkit/dataio/fileio.py @@ -0,0 +1,427 @@ +from __future__ import print_function + +import os +import sys +import numpy as np +import nibabel as nib +import tempfile +import pandas as pd +import re + +try: # run as a package if installed + from pcntoolkit import configs +except ImportError: + pass + + path = os.path.abspath(os.path.dirname(__file__)) + path = os.path.dirname(path) # parent directory + if path not in sys.path: + sys.path.append(path) + del path + import configs + +CIFTI_MAPPINGS = ('dconn', 'dtseries', 'pconn', 'ptseries', 'dscalar', + 'dlabel', 'pscalar', 'pdconn', 'dpconn', + 'pconnseries', 'pconnscalar') + +CIFTI_VOL_ATLAS = 'Atlas_ROIs.2.nii.gz' + +PICKLE_PROTOCOL = configs.PICKLE_PROTOCOL + +# ------------------------ +# general utility routines +# ------------------------ + +def predictive_interval(s2_forward, + cov_forward, + multiplicator): + # calculates a predictive interval + + PI=np.zeros(len(cov_forward)) + for i,xdot in enumerate(cov_forward): + s=np.sqrt(s2_forward[i]) + PI[i]=multiplicator*s + return PI + +def create_mask(data_array, mask, verbose=False): + # create a (volumetric) mask either from an input nifti or the nifti itself + + if mask is not None: + if verbose: + print('Loading ROI mask ...') + maskvol = load_nifti(mask, vol=True) + maskvol = maskvol != 0 + else: + if len(data_array.shape) < 4: + dim = data_array.shape[0:3] + (1,) + else: + dim = data_array.shape[0:3] + (data_array.shape[3],) + + if verbose: + print('Generating mask automatically ...') + if dim[3] == 1: + maskvol = data_array[:, :, :] != 0 + else: + maskvol = data_array[:, :, :, 0] != 0 + + return maskvol + + +def vol2vec(dat, mask, verbose=False): + # vectorise a 3d image + + if len(dat.shape) < 4: + dim = dat.shape[0:3] + (1,) + else: + dim = dat.shape[0:3] + (dat.shape[3],) + + #mask = create_mask(dat, mask=mask, verbose=verbose) + if mask is None: + mask = create_mask(dat, mask=mask, verbose=verbose) + + # mask the image + maskid = np.where(mask.ravel())[0] + dat = np.reshape(dat, (np.prod(dim[0:3]), dim[3])) + dat = dat[maskid, :] + + # convert to 1-d array if the file only contains one volume + if dim[3] == 1: + dat = dat.ravel() + + return dat + + +def file_type(filename): + # routine to determine filetype + + if filename.endswith(('.dtseries.nii', '.dscalar.nii', '.dlabel.nii')): + ftype = 'cifti' + elif filename.endswith(('.nii.gz', '.nii', '.img', '.hdr')): + ftype = 'nifti' + elif filename.endswith(('.txt', '.csv', '.tsv', '.asc')): + ftype = 'text' + elif filename.endswith(('.pkl')): + ftype = 'binary' + else: + raise ValueError("I don't know what to do with " + filename) + + return ftype + + +def file_extension(filename): + # routine to get the full file extension (e.g. .nii.gz, not just .gz) + + parts = filename.split(os.extsep) + + if parts[-1] == 'gz': + if parts[-2] == 'nii' or parts[-2] == 'img' or parts[-2] == 'hdr': + ext = parts[-2] + '.' + parts[-1] + else: + ext = parts[-1] + elif parts[-1] == 'nii': + if parts[-2] in CIFTI_MAPPINGS: + ext = parts[-2] + '.' + parts[-1] + else: + ext = parts[-1] + else: + ext = parts[-1] + + ext = '.' + ext + return ext + + +def file_stem(filename): + + idx = filename.find(file_extension(filename)) + stm = filename[0:idx] + + return stm + +# -------------- +# nifti routines +# -------------- + + +def load_nifti(datafile, mask=None, vol=False, verbose=False): + + if verbose: + print('Loading nifti: ' + datafile + ' ...') + img = nib.load(datafile) + dat = img.get_data() + + if mask is not None: + mask=load_nifti(mask, vol=True) + + if not vol: + dat = vol2vec(dat, mask) + + return dat + + +def save_nifti(data, filename, examplenii, mask, dtype=None): + + ''' + Write output to nifti + + Basic usage:: + + save_nifti(data, filename mask, dtype) + + where the variables are defined below. + + :param data: numpy array containing the data to write out + :param filename: where to store it + :param examplenii: nifti to copy the geometry and data type from + :mask: nifti image containing a mask for the image + :param dtype: data type for the output image (if different from the image) + ''' + + + # load mask + if isinstance(mask, str): + mask = load_nifti(mask, vol=True) + mask = mask != 0 + + # load example image + ex_img = nib.load(examplenii) + ex_img.shape + dim = ex_img.shape[0:3] + if len(data.shape) < 2: + nvol = 1 + data = data[:, np.newaxis] + else: + nvol = int(data.shape[1]) + + # write data + array_data = np.zeros((np.prod(dim), nvol)) + array_data[mask.flatten(), :] = data + array_data = np.reshape(array_data, dim+(nvol,)) + hdr = ex_img.header + if dtype is not None: + hdr.set_data_dtype(dtype) + array_data = array_data.astype(dtype) + array_img = nib.Nifti1Image(array_data, ex_img.affine, hdr) + + nib.save(array_img, filename) + +# -------------- +# cifti routines +# -------------- + + +def load_cifti(filename, vol=False, mask=None, rmtmp=True): + + # parse the name + dnam, fnam = os.path.split(filename) + fpref = file_stem(fnam) + outstem = os.path.join(tempfile.gettempdir(), + str(os.getpid()) + "-" + fpref) + + # extract surface data from the cifti file + print("Extracting cifti surface data to ", outstem, '-*.func.gii', sep="") + giinamel = outstem + '-left.func.gii' + giinamer = outstem + '-right.func.gii' + os.system('wb_command -cifti-separate ' + filename + + ' COLUMN -metric CORTEX_LEFT ' + giinamel) + os.system('wb_command -cifti-separate ' + filename + + ' COLUMN -metric CORTEX_RIGHT ' + giinamer) + + # load the surface data + giil = nib.load(giinamel) + giir = nib.load(giinamer) + Nimg = len(giil.darrays) + Nvert = len(giil.darrays[0].data) + if Nimg == 1: + out = np.concatenate((giil.darrays[0].data, giir.darrays[0].data), + axis=0) + else: + Gl = np.zeros((Nvert, Nimg)) + Gr = np.zeros((Nvert, Nimg)) + for i in range(0, Nimg): + Gl[:, i] = giil.darrays[i].data + Gr[:, i] = giir.darrays[i].data + out = np.concatenate((Gl, Gr), axis=0) + if rmtmp: + # clean up temporary files + os.remove(giinamel) + os.remove(giinamer) + + if vol: + niiname = outstem + '-vol.nii' + print("Extracting cifti volume data to ", niiname, sep="") + os.system('wb_command -cifti-separate ' + filename + + ' COLUMN -volume-all ' + niiname) + vol = load_nifti(niiname, vol=True) + volmask = create_mask(vol) + out = np.concatenate((out, vol2vec(vol, volmask)), axis=0) + if rmtmp: + os.remove(niiname) + + return out + + +def save_cifti(data, filename, example, mask=None, vol=True, volatlas=None): + """ Write output to nifti """ + + # do some sanity checks + if data.dtype == 'float32' or \ + data.dtype == 'float' or \ + data.dtype == 'float64': + data = data.astype('float32') # force 32 bit output + dtype = 'NIFTI_TYPE_FLOAT32' + else: + raise(ValueError, 'Only float data types currently handled') + + if len(data.shape) == 1: + Nimg = 1 + data = data[:, np.newaxis] + else: + Nimg = data.shape[1] + + # get the base filename + dnam, fnam = os.path.split(filename) + fstem = file_stem(fnam) + + # Split the template + estem = os.path.join(tempfile.gettempdir(), str(os.getpid()) + "-" + fstem) + giiexnamel = estem + '-left.func.gii' + giiexnamer = estem + '-right.func.gii' + os.system('wb_command -cifti-separate ' + example + + ' COLUMN -metric CORTEX_LEFT ' + giiexnamel) + os.system('wb_command -cifti-separate ' + example + + ' COLUMN -metric CORTEX_RIGHT ' + giiexnamer) + + # write left hemisphere + giiexl = nib.load(giiexnamel) + Nvertl = len(giiexl.darrays[0].data) + garraysl = [] + for i in range(0, Nimg): + garraysl.append( + nib.gifti.gifti.GiftiDataArray(data=data[0:Nvertl, i], + datatype=dtype)) + giil = nib.gifti.gifti.GiftiImage(darrays=garraysl) + fnamel = fstem + '-left.func.gii' + nib.save(giil, fnamel) + + # write right hemisphere + giiexr = nib.load(giiexnamer) + Nvertr = len(giiexr.darrays[0].data) + garraysr = [] + for i in range(0, Nimg): + garraysr.append( + nib.gifti.gifti.GiftiDataArray(data=data[Nvertl:Nvertl+Nvertr, i], + datatype=dtype)) + giir = nib.gifti.gifti.GiftiImage(darrays=garraysr) + fnamer = fstem + '-right.func.gii' + nib.save(giir, fnamer) + + tmpfiles = [fnamer, fnamel, giiexnamel, giiexnamer] + + # process volumetric data + if vol: + niiexname = estem + '-vol.nii' + os.system('wb_command -cifti-separate ' + example + + ' COLUMN -volume-all ' + niiexname) + niivol = load_nifti(niiexname, vol=True) + if mask is None: + mask = create_mask(niivol) + + if volatlas is None: + volatlas = CIFTI_VOL_ATLAS + fnamev = fstem + '-vol.nii' + + save_nifti(data[Nvertr+Nvertl:, :], fnamev, niiexname, mask) + tmpfiles.extend([fnamev, niiexname]) + + # write cifti + fname = fstem + '.dtseries.nii' + os.system('wb_command -cifti-create-dense-timeseries ' + fname + + ' -volume ' + fnamev + ' ' + volatlas + + ' -left-metric ' + fnamel + ' -right-metric ' + fnamer) + + # clean up + for f in tmpfiles: + os.remove(f) + +# -------------- +# ascii routines +# -------------- + + +def load_pd(filename): + # based on pandas + x = pd.read_csv(filename, + sep=' ', + header=None) + return x + + +def save_pd(data, filename): + # based on pandas + data.to_csv(filename, + index=None, + header=None, + sep=' ', + na_rep='NaN') + + +def load_ascii(filename): + # based on pandas + x = np.loadtxt(filename) + return x + + +def save_ascii(data, filename): + # based on pandas + np.savetxt(filename, data) + +# ---------------- +# generic routines +# ---------------- + + +def save(data, filename, example=None, mask=None, text=False, dtype=None): + + if file_type(filename) == 'cifti': + save_cifti(data.T, filename, example, vol=True) + elif file_type(filename) == 'nifti': + save_nifti(data.T, filename, example, mask, dtype=dtype) + elif text or file_type(filename) == 'text': + save_ascii(data, filename) + elif file_type(filename) == 'binary': + data = pd.DataFrame(data) + data.to_pickle(filename, protocol=PICKLE_PROTOCOL) + + +def load(filename, mask=None, text=False, vol=True): + + if file_type(filename) == 'cifti': + x = load_cifti(filename, vol=vol) + elif file_type(filename) == 'nifti': + x = load_nifti(filename, mask, vol=vol) + elif text or file_type(filename) == 'text': + x = load_ascii(filename) + elif file_type(filename) == 'binary': + x = pd.read_pickle(filename) + x = x.to_numpy() + + return x + +# ------------------- +# sorting routines for batched in normative parallel +# ------------------- + + +def tryint(s): + try: + return int(s) + except ValueError: + return s + + +def alphanum_key(s): + return [tryint(c) for c in re.split('([0-9]+)', s)] + + +def sort_nicely(l): + return sorted(l, key=alphanum_key) diff --git a/build/lib/pcntoolkit/model/NP.py b/build/lib/pcntoolkit/model/NP.py new file mode 100644 index 00000000..13370286 --- /dev/null +++ b/build/lib/pcntoolkit/model/NP.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Mon Jun 24 15:06:06 2019 + +@author: seykia +""" + +import torch +from torch import nn +from torch.nn import functional as F + +##################################### NP Model ################################ + +class NP(nn.Module): + def __init__(self, encoder, decoder, args): + super(NP, self).__init__() + self.r_dim = encoder.r_dim + self.z_dim = encoder.z_dim + self.dp_level = encoder.dp_level + self.encoder = encoder + self.decoder = decoder + self.r_to_z_mean_dp = nn.Dropout(p = self.dp_level) + self.r_to_z_mean = nn.Linear(self.r_dim, self.z_dim) + self.r_to_z_logvar_dp = nn.Dropout(p = self.dp_level) + self.r_to_z_logvar = nn.Linear(self.r_dim, self.z_dim) + self.device = args.device + self.type = args.type + + def xy_to_z_params(self, x, y): + r = self.encoder.forward(x, y) + mu = self.r_to_z_mean(self.r_to_z_mean_dp(r)) + logvar = self.r_to_z_logvar(self.r_to_z_logvar_dp(r)) + return mu, logvar + + def reparameterise(self, z): + mu, logvar = z + std = torch.exp(0.5 * logvar) + eps = torch.randn_like(std) + z_sample = eps.mul(std).add_(mu) + return z_sample + + def forward(self, x_context, y_context, x_all=None, y_all=None, n = 10): + y_sigma = None + z_context = self.xy_to_z_params(x_context, y_context) + if self.training: + z_all = self.xy_to_z_params(x_all, y_all) + z_sample = self.reparameterise(z_all) + y_hat = self.decoder.forward(z_sample, x_all) + else: + z_all = z_context + if self.type == 'ST': + temp = torch.zeros([n,y_context.shape[0], y_context.shape[2]], device = 'cpu') + elif self.type == 'MT': + temp = torch.zeros([n,y_context.shape[0],1,y_context.shape[2],y_context.shape[3], + y_context.shape[4]], device = 'cpu') + for i in range(n): + z_sample = self.reparameterise(z_all) + temp[i,:] = self.decoder.forward(z_sample, x_context) + y_hat = torch.mean(temp, dim=0).to(self.device) + if n > 1: + y_sigma = torch.std(temp, dim=0).to(self.device) + return y_hat, z_all, z_context, y_sigma + +############################################################################### + +def apply_dropout_test(m): + if type(m) == nn.Dropout: + m.train() + +def kl_div_gaussians(mu_q, logvar_q, mu_p, logvar_p): + var_p = torch.exp(logvar_p) + kl_div = (torch.exp(logvar_q) + (mu_q - mu_p) ** 2) / (var_p) \ + - 1.0 \ + + logvar_p - logvar_q + kl_div = 0.5 * kl_div.sum() + return kl_div + +def np_loss(y_hat, y, z_all, z_context): + BCE = F.binary_cross_entropy(torch.squeeze(y_hat), torch.mean(y,dim=1), reduction="sum") + KLD = kl_div_gaussians(z_all[0], z_all[1], z_context[0], z_context[1]) + return BCE + KLD diff --git a/build/lib/pcntoolkit/model/NPR.py b/build/lib/pcntoolkit/model/NPR.py new file mode 100644 index 00000000..07bee34c --- /dev/null +++ b/build/lib/pcntoolkit/model/NPR.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Nov 22 14:32:37 2019 + +@author: seykia +""" + +import torch +from torch import nn +from torch.nn import functional as F + +##################################### NP Model ################################ + +class NPR(nn.Module): + def __init__(self, encoder, decoder, args): + super(NPR, self).__init__() + self.r_dim = encoder.r_dim + self.z_dim = encoder.z_dim + self.encoder = encoder + self.decoder = decoder + self.r_to_z_mean = nn.Linear(self.r_dim, self.z_dim) + self.r_to_z_logvar = nn.Linear(self.r_dim, self.z_dim) + self.device = args.device + + def xy_to_z_params(self, x, y): + r = self.encoder.forward(x, y) + mu = self.r_to_z_mean(r) + logvar = self.r_to_z_logvar(r) + return mu, logvar + + def reparameterise(self, z): + mu, logvar = z + std = torch.exp(0.5 * logvar) + eps = torch.randn_like(std) + z_sample = eps.mul(std).add_(mu) + return z_sample + + def forward(self, x_context, y_context, x_all=None, y_all=None, n = 10): + y_sigma = None + y_sigma_84 = None + z_context = self.xy_to_z_params(x_context, y_context) + if self.training: + z_all = self.xy_to_z_params(x_all, y_all) + z_sample = self.reparameterise(z_all) + y_hat, y_hat_84 = self.decoder.forward(z_sample) + else: + z_all = z_context + temp = torch.zeros([n,y_context.shape[0], y_context.shape[2]], device = self.device) + temp_84 = torch.zeros([n,y_context.shape[0], y_context.shape[2]], device = self.device) + for i in range(n): + z_sample = self.reparameterise(z_all) + temp[i,:], temp_84[i,:] = self.decoder.forward(z_sample) + y_hat = torch.mean(temp, dim=0).to(self.device) + y_hat_84 = torch.mean(temp_84, dim=0).to(self.device) + if n > 1: + y_sigma = torch.std(temp, dim=0).to(self.device) + y_sigma_84 = torch.std(temp_84, dim=0).to(self.device) + return y_hat, y_hat_84, z_all, z_context, y_sigma, y_sigma_84 + +############################################################################### + +def kl_div_gaussians(mu_q, logvar_q, mu_p, logvar_p): + var_p = torch.exp(logvar_p) + kl_div = (torch.exp(logvar_q) + (mu_q - mu_p) ** 2) / (var_p) \ + - 1.0 \ + + logvar_p - logvar_q + kl_div = 0.5 * kl_div.sum() + return kl_div + +def np_loss(y_hat, y_hat_84, y, z_all, z_context): + #PBL = pinball_loss(y, y_hat, 0.05) + BCE = F.binary_cross_entropy(torch.squeeze(y_hat), torch.mean(y,dim=1), reduction="sum") + idx1 = (y >= y_hat_84).squeeze() + idx2 = (y < y_hat_84).squeeze() + BCE84 = 0.84 * F.binary_cross_entropy(torch.squeeze(y_hat_84[idx1,:]), torch.mean(y[idx1,:],dim=1), reduction="sum") + \ + 0.16 * F.binary_cross_entropy(torch.squeeze(y_hat_84[idx2,:]), torch.mean(y[idx2,:],dim=1), reduction="sum") + KLD = kl_div_gaussians(z_all[0], z_all[1], z_context[0], z_context[1]) + return BCE + KLD + BCE84 + diff --git a/build/lib/pcntoolkit/model/SHASH.py b/build/lib/pcntoolkit/model/SHASH.py new file mode 100644 index 00000000..5ab91e51 --- /dev/null +++ b/build/lib/pcntoolkit/model/SHASH.py @@ -0,0 +1,271 @@ +import theano.tensor +from pymc3.distributions import Continuous, draw_values, generate_samples +import theano.tensor as tt +import numpy as np +from pymc3.distributions.dist_math import bound +import scipy.special as spp +from theano import as_op +from theano.gof.fg import NullType +from theano.gof.op import Op +from theano.gof.graph import Apply +from theano.gradient import grad_not_implemented + +""" +@author: Stijn de Boer (AuguB) + +See: Jones et al. (2009), Sinh-Arcsinh distributions. +""" + + +class K(Op): + """ + Modified Bessel function of the second kind, theano implementation + """ + def make_node(self, p, x): + p = theano.tensor.as_tensor_variable(p, 'floatX') + x = theano.tensor.as_tensor_variable(x, 'floatX') + return Apply(self, [p,x], [p.type()]) + + def perform(self, node, inputs, output_storage, params=None): + # Doing this on the unique values avoids doing A LOT OF double work, apparently scipy doesn't do this by itself + unique_inputs, inverse_indices = np.unique(inputs[0], return_inverse=True) + unique_outputs = spp.kv(unique_inputs, inputs[1]) + outputs = unique_outputs[inverse_indices].reshape(inputs[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)] + +class SHASH(Continuous): + """ + SHASH described by Jones et al., based on a standard normal + All SHASH subclasses inherit from this + """ + def __init__(self, epsilon, delta, **kwargs): + super().__init__(**kwargs) + self.epsilon = tt.as_tensor_variable(epsilon) + self.delta = tt.as_tensor_variable(delta) + self.K = K() + + + def random(self, point=None, size=None): + epsilon, delta = draw_values([self.epsilon, self.delta], + point=point, size=size) + + def _random(epsilon, delta, size=None): + samples_transformed = np.sinh((np.arcsinh(np.random.randn(*size)) + epsilon) / delta) + return samples_transformed + + return generate_samples(_random, epsilon=epsilon, delta=delta, dist_shape=self.shape, size=size) + + def logp(self, value): + epsilon = self.epsilon + delta = self.delta + tt.np.finfo(np.float32).eps + + this_S = self.S(value) + this_S_sqr = tt.sqr(this_S) + this_C_sqr = 1+this_S_sqr + frac1 = -tt.log(tt.constant(2 * tt.np.pi))/2 + frac2 = tt.log(delta) + tt.log(this_C_sqr)/2 - tt.log(1 + tt.sqr(value)) / 2 + exp = -this_S_sqr / 2 + + return bound(frac1 + frac2 + exp, delta > 0) + + def S(self, x): + """ + + :param epsilon: + :param delta: + :param x: + :return: The sinharcsinh transformation of x + """ + return tt.sinh(tt.arcsinh(x) * self.delta - self.epsilon) + + def S_inv(self, x): + return tt.sinh((tt.arcsinh(x) + self.epsilon) / self.delta) + + def C(self, x): + """ + :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. + """ + return tt.cosh(tt.arcsinh(x) * self.delta - self.epsilon) + + def P(self, q): + """ + The P function as given in Jones et al. + :param q: + :return: + """ + frac = np.exp(1 / 4) / np.power(8 * np.pi, 1 / 2) + K1 = self.K((q+1)/2,1/4) + K2 = self.K((q-1)/2,1/4) + a = (K1 + K2) * frac + return a + + def m(self, 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. + """ + frac1 = tt.as_tensor_variable(1 / np.power(2, r)) + acc = tt.as_tensor_variable(0) + for i in range(r + 1): + combs = spp.comb(r, i) + flip = np.power(-1, i) + ex = np.exp((r - 2 * i) * self.epsilon / self.delta) + # This is the reason we can not sample delta/kurtosis using NUTS; the gradient of P is unknown to pymc3 + # TODO write a class that inherits theano.Op and do the gradient in there :) + p = self.P((r - 2 * i) / self.delta) + acc += combs * flip * ex * p + return frac1 * acc + +class SHASHo(SHASH): + """ + This is the shash where the location and scale parameters have simply been applied as an linear transformation + directly on the original shash. + """ + + def __init__(self, mu, sigma, epsilon, delta, **kwargs): + super().__init__(epsilon, delta, **kwargs) + self.mu = tt.as_tensor_variable(mu) + self.sigma = tt.as_tensor_variable(sigma) + + def random(self, point=None, size=None): + mu, sigma, epsilon, delta = draw_values([self.mu, self.sigma, self.epsilon, self.delta], + point=point, size=size) + + def _random(mu, sigma, epsilon, delta, size=None): + samples_transformed = np.sinh((np.arcsinh(np.random.randn(*size)) + epsilon) / delta) * sigma + mu + return samples_transformed + + return generate_samples(_random, mu=mu, sigma=sigma, epsilon=epsilon, delta=delta, + dist_shape=self.shape, + size=size) + + def logp(self, value): + mu = self.mu + sigma = self.sigma + tt.np.finfo(np.float32).eps + epsilon = self.epsilon + delta = self.delta + tt.np.finfo(np.float32).eps + + value_transformed = (value - mu) / sigma + + this_S = self.S( value_transformed) + this_S_sqr = tt.sqr(this_S) + this_C_sqr = 1+this_S_sqr + frac1 = -tt.log(tt.constant(2 * tt.np.pi))/2 + frac2 = tt.log(delta) + tt.log(this_C_sqr)/2 - tt.log( + 1 + tt.sqr(value_transformed)) / 2 + exp = -this_S_sqr / 2 + change_of_variable = -tt.log(sigma) + + return bound(frac1 + frac2 + exp + change_of_variable, sigma > 0, delta > 0) + + +class SHASHo2(SHASH): + """ + This is the shash where we apply the reparameterization provided in section 4.3 in Jones et al. + """ + + def __init__(self, mu, sigma, epsilon, delta, **kwargs): + super().__init__(epsilon, delta, **kwargs) + self.mu = tt.as_tensor_variable(mu) + self.sigma = tt.as_tensor_variable(sigma) + + def random(self, point=None, size=None): + mu, sigma, epsilon, delta = draw_values( + [self.mu, self.sigma, self.epsilon, self.delta], + point=point, size=size) + sigma_d = sigma / delta + + def _random(mu, sigma, epsilon, delta, size=None): + samples_transformed = np.sinh( + (np.arcsinh(np.random.randn(*size)) + epsilon) / delta) * sigma_d + mu + return samples_transformed + + return generate_samples(_random, mu=mu, sigma=sigma_d, epsilon=epsilon, delta=delta, + dist_shape=self.shape, + size=size) + + def logp(self, value): + mu = self.mu + sigma = self.sigma + tt.np.finfo(np.float32).eps + epsilon = self.epsilon + delta = self.delta + tt.np.finfo(np.float32).eps + sigma_d = sigma / delta + + + # Here a double change of variables is applied + value_transformed = ((value - mu) / sigma_d) + + this_S = self.S(value_transformed) + this_S_sqr = tt.sqr(this_S) + this_C = tt.sqrt(1+this_S_sqr) + frac1 = -tt.log(tt.sqrt(tt.constant(2 * tt.np.pi))) + frac2 = tt.log(delta) + tt.log(this_C) - tt.log( + 1 + tt.sqr(value_transformed)) / 2 + exp = -this_S_sqr / 2 + change_of_variable = -tt.log(sigma_d) + + # the change of variables is accounted for in the density by division and multiplication (adding and subtracting for logp) + return bound(frac1 + frac2 + exp + change_of_variable, delta > 0, sigma > 0) + +class SHASHb(SHASH): + """ + This is the shash where the location and scale parameters been applied as an linear transformation on the shash + distribution which was corrected for mean and variance. + """ + + def __init__(self, mu, sigma, epsilon, delta, **kwargs): + super().__init__(epsilon, delta, **kwargs) + self.mu = tt.as_tensor_variable(mu) + self.sigma = tt.as_tensor_variable(sigma) + + def random(self, point=None, size=None): + mu, sigma, epsilon, delta = draw_values( + [self.mu, self.sigma, self.epsilon, self.delta], + point=point, size=size) + mean = (tt.sinh(epsilon/delta)*self.P(1/delta)).eval() + var = ((tt.cosh(2*epsilon/delta)*self.P(2/delta)-1)/2).eval() - mean**2 + + def _random(mean, var, mu, sigma, epsilon, delta, size=None): + samples_transformed = ((np.sinh( + (np.arcsinh(np.random.randn(*size)) + epsilon) / delta) - mean) / np.sqrt(var)) * sigma + mu + return samples_transformed + + return generate_samples(_random, mean=mean, var=var, mu=mu, sigma=sigma, epsilon=epsilon, delta=delta, + dist_shape=self.shape, + size=size) + + def logp(self, value): + mu = self.mu + sigma = self.sigma + tt.np.finfo(np.float32).eps + epsilon = self.epsilon + delta = self.delta + tt.np.finfo(np.float32).eps + mean = tt.sinh(epsilon/delta)*self.P(1/delta) + var = (tt.cosh(2*epsilon/delta)*self.P(2/delta)-1)/2 - tt.sqr(mean) + + # Here a double change of variables is applied + value_transformed = ((value - mu) / sigma) * tt.sqrt(var) + mean + + this_S = self.S(value_transformed) + this_S_sqr = tt.sqr(this_S) + this_C_sqr = 1+this_S_sqr + frac1 = -tt.log(tt.constant(2 * tt.np.pi))/2 + frac2 = tt.log(delta) + tt.log(this_C_sqr)/2 - tt.log(1 + tt.sqr(value_transformed)) / 2 + exp = -this_S_sqr / 2 + change_of_variable = tt.log(var)/2 - tt.log(sigma) + + # the change of variables is accounted for in the density by division and multiplication (addition and subtraction in the log domain) + return bound(frac1 + frac2 + exp + change_of_variable, delta > 0, sigma > 0, var > 0) diff --git a/build/lib/pcntoolkit/model/__init__.py b/build/lib/pcntoolkit/model/__init__.py new file mode 100644 index 00000000..fe59b2d4 --- /dev/null +++ b/build/lib/pcntoolkit/model/__init__.py @@ -0,0 +1,6 @@ +from . import bayesreg +from . import gp +from . import rfa +from . import architecture +from . import NP +from . import hbr diff --git a/build/lib/pcntoolkit/model/architecture.py b/build/lib/pcntoolkit/model/architecture.py new file mode 100644 index 00000000..569d4336 --- /dev/null +++ b/build/lib/pcntoolkit/model/architecture.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Aug 30 09:45:35 2019 + +@author: seykia +""" + +import torch +from torch import nn +from torch.nn import functional as F +import numpy as np + +def compute_conv_out_size(d_in, h_in, w_in, padding, dilation, kernel_size, stride, UPorDW): + if UPorDW == 'down': + d_out = np.floor((d_in + 2 * padding[0] - dilation * (kernel_size - 1) - 1) / stride + 1) + h_out = np.floor((h_in + 2 * padding[1] - dilation * (kernel_size - 1) - 1) / stride + 1) + w_out = np.floor((w_in + 2 * padding[2] - dilation * (kernel_size - 1) - 1) / stride + 1) + elif UPorDW == 'up': + d_out = (d_in-1) * stride - 2 * padding[0] + dilation * (kernel_size - 1) + 1 + h_out = (h_in-1) * stride - 2 * padding[1] + dilation * (kernel_size - 1) + 1 + w_out = (w_in-1) * stride - 2 * padding[2] + dilation * (kernel_size - 1) + 1 + return d_out, h_out, w_out + +################################ ARCHITECTURES ################################ + +class Encoder(nn.Module): + def __init__(self, x, y, args): + super(Encoder, self).__init__() + self.r_dim = 25 + self.r_conv_dim = 100 + self.lrlu_neg_slope = 0.01 + self.dp_level = 0.1 + + self.factor=args.m + self.x_dim = x.shape[2] + + # Conv 1 + self.encoder_y_layer_1_conv = nn.Conv3d(in_channels = self.factor, out_channels=self.factor, + kernel_size=5, stride=2, padding=0, + dilation=1, groups=self.factor, bias=True) # in:(90,108,90) out:(43,52,43) + self.encoder_y_layer_1_bn = nn.BatchNorm3d(self.factor) + d_out_1, h_out_1, w_out_1 = compute_conv_out_size(y.shape[2], y.shape[3], + y.shape[4], padding=[0,0,0], + dilation=1, kernel_size=5, + stride=2, UPorDW='down') + + # Conv 2 + self.encoder_y_layer_2_conv = nn.Conv3d(in_channels=self.factor, out_channels=self.factor, + kernel_size=3, stride=2, padding=0, + dilation=1, groups=self.factor, bias=True) # out: (21,25,21) + self.encoder_y_layer_2_bn = nn.BatchNorm3d(self.factor) + d_out_2, h_out_2, w_out_2 = compute_conv_out_size(d_out_1, h_out_1, + w_out_1, padding=[0,0,0], + dilation=1, kernel_size=3, + stride=2, UPorDW='down') + + # Conv 3 + self.encoder_y_layer_3_conv = nn.Conv3d(in_channels=self.factor, out_channels=self.factor, + kernel_size=3, stride=2, padding=0, + dilation=1, groups=self.factor, bias=True) # out: (10,12,10) + self.encoder_y_layer_3_bn = nn.BatchNorm3d(self.factor) + d_out_3, h_out_3, w_out_3 = compute_conv_out_size(d_out_2, h_out_2, + w_out_2, padding=[0,0,0], + dilation=1, kernel_size=3, + stride=2, UPorDW='down') + + # Conv 4 + self.encoder_y_layer_4_conv = nn.Conv3d(in_channels=self.factor, out_channels=1, + kernel_size=3, stride=2, padding=0, + dilation=1, groups=1, bias=True) # out: (4,5,4) + self.encoder_y_layer_4_bn = nn.BatchNorm3d(1) + d_out_4, h_out_4, w_out_4 = compute_conv_out_size(d_out_3, h_out_3, + w_out_3, padding=[0,0,0], + dilation=1, kernel_size=3, + stride=2, UPorDW='down') + self.cnn_feature_num = [1, int(d_out_4), int(h_out_4), int(w_out_4)] + + # FC 5 + self.encoder_y_layer_5_dp = nn.Dropout(p = self.dp_level) + self.encoder_y_layer_5_linear = nn.Linear(int(np.prod(self.cnn_feature_num)), self.r_conv_dim) + + # FC 6 + self.encoder_xy_layer_6_dp = nn.Dropout(p = self.dp_level) + self.encoder_xy_layer_6_linear = nn.Linear(self.r_conv_dim + self.x_dim, 50) + + # FC 7 + self.encoder_xy_layer_7_dp = nn.Dropout(p = self.dp_level) + self.encoder_xy_layer_7_linear = nn.Linear(50, self.r_dim) + + def forward(self, x, y): + y = F.leaky_relu(self.encoder_y_layer_1_bn( + self.encoder_y_layer_1_conv(y)), self.lrlu_neg_slope) + y = F.leaky_relu(self.encoder_y_layer_2_bn( + self.encoder_y_layer_2_conv(y)),self.lrlu_neg_slope) + y = F.leaky_relu(self.encoder_y_layer_3_bn( + self.encoder_y_layer_3_conv(y)),self.lrlu_neg_slope) + y = F.leaky_relu(self.encoder_y_layer_4_bn( + self.encoder_y_layer_4_conv(y)),self.lrlu_neg_slope) + y = F.leaky_relu(self.encoder_y_layer_5_linear(self.encoder_y_layer_5_dp( + y.view(y.shape[0], np.prod(self.cnn_feature_num)))), self.lrlu_neg_slope) + x_y = torch.cat((y, torch.mean(x, dim=1)), 1) + x_y = F.leaky_relu(self.encoder_xy_layer_6_linear( + self.encoder_xy_layer_6_dp(x_y)),self.lrlu_neg_slope) + x_y = F.leaky_relu(self.encoder_xy_layer_7_linear( + self.encoder_xy_layer_7_dp(x_y)),self.lrlu_neg_slope) + return x_y + + +class Decoder(nn.Module): + def __init__(self, x, y, args): + super(Decoder, self).__init__() + self.r_dim = 25 + self.r_conv_dim = 100 + self.lrlu_neg_slope = 0.01 + self.dp_level = 0.1 + self.z_dim = 10 + self.x_dim = x.shape[2] + self.cnn_feature_num = args.cnn_feature_num + self.factor=args.m + + # FC 1 + self.decoder_zx_layer_1_dp = nn.Dropout(p = self.dp_level) + self.decoder_zx_layer_1_linear = nn.Linear(self.z_dim + self.x_dim, 50) + + # FC 2 + self.decoder_zx_layer_2_dp = nn.Dropout(p = self.dp_level) + self.decoder_zx_layer_2_linear = nn.Linear(50, int(np.prod(self.cnn_feature_num))) + + # Iconv 1 + self.decoder_zx_layer_1_iconv = nn.ConvTranspose3d(in_channels=1, out_channels=self.factor, + kernel_size=3, stride=1, + padding=0, output_padding=(0,0,0), + groups=1, bias=True, dilation=1) + self.decoder_zx_layer_1_bn = nn.BatchNorm3d(self.factor) + d_out_4, h_out_4, w_out_4 = compute_conv_out_size(args.cnn_feature_num[1]*2, + args.cnn_feature_num[2]*2, + args.cnn_feature_num[3]*2, + padding=[0,0,0], + dilation=1, kernel_size=3, + stride=1, UPorDW='up') + + # Iconv 2 + self.decoder_zx_layer_2_iconv = nn.ConvTranspose3d(in_channels=self.factor, out_channels=self.factor, + kernel_size=3, stride=1, padding=0, + output_padding=(0,0,0), groups=self.factor, + bias=True, dilation=1) + self.decoder_zx_layer_2_bn = nn.BatchNorm3d(self.factor) + d_out_3, h_out_3, w_out_3 = compute_conv_out_size(d_out_4*2, + h_out_4*2, + w_out_4*2, + padding=[0,0,0], + dilation=1, kernel_size=3, + stride=1, UPorDW='up') + # Iconv 3 + self.decoder_zx_layer_3_iconv = nn.ConvTranspose3d(in_channels=self.factor, out_channels=self.factor, + kernel_size=3, stride=1, padding=0, + output_padding=(0,0,0), groups=self.factor, + bias=True, dilation=1) + self.decoder_zx_layer_3_bn = nn.BatchNorm3d(self.factor) + d_out_2, h_out_2, w_out_2 = compute_conv_out_size(d_out_3*2, + h_out_3*2, + w_out_3*2, + padding=[0,0,0], + dilation=1, kernel_size=3, + stride=1, UPorDW='up') + + # Iconv 4 + self.decoder_zx_layer_4_iconv = nn.ConvTranspose3d(in_channels=self.factor, out_channels=1, + kernel_size=3, stride=1, padding=(0,0,0), + output_padding= (0,0,0), groups=1, + bias=True, dilation=1) + d_out_1, h_out_1, w_out_1 = compute_conv_out_size(d_out_2*2, + h_out_2*2, + w_out_2*2, + padding=[0,0,0], + dilation=1, kernel_size=3, + stride=1, UPorDW='up') + + self.scaling = [y.shape[2]/d_out_1, y.shape[3]/h_out_1, + y.shape[4]/w_out_1] + + def forward(self, z_sample, x_target): + z_x = torch.cat([z_sample, torch.mean(x_target,dim=1)], dim=1) + z_x = F.leaky_relu(self.decoder_zx_layer_1_linear(self.decoder_zx_layer_1_dp(z_x)), + self.lrlu_neg_slope) + z_x = F.leaky_relu(self.decoder_zx_layer_2_linear(self.decoder_zx_layer_2_dp(z_x)), + self.lrlu_neg_slope) + z_x = z_x.view(x_target.shape[0], self.cnn_feature_num[0], self.cnn_feature_num[1], + self.cnn_feature_num[2], self.cnn_feature_num[3]) + z_x = F.leaky_relu(self.decoder_zx_layer_1_bn(self.decoder_zx_layer_1_iconv( + F.interpolate(z_x, scale_factor=2))), self.lrlu_neg_slope) + z_x = F.leaky_relu(self.decoder_zx_layer_2_bn(self.decoder_zx_layer_2_iconv( + F.interpolate(z_x, scale_factor=2))), self.lrlu_neg_slope) + z_x = F.leaky_relu(self.decoder_zx_layer_3_bn(self.decoder_zx_layer_3_iconv( + F.interpolate(z_x, scale_factor=2))), self.lrlu_neg_slope) + z_x = self.decoder_zx_layer_4_iconv(F.interpolate(z_x, scale_factor=2)) + y_hat = torch.sigmoid(F.interpolate(z_x, scale_factor=(self.scaling[0], + self.scaling[1],self.scaling[2]))) + return y_hat + \ No newline at end of file diff --git a/build/lib/pcntoolkit/model/bayesreg.py b/build/lib/pcntoolkit/model/bayesreg.py new file mode 100644 index 00000000..c353081d --- /dev/null +++ b/build/lib/pcntoolkit/model/bayesreg.py @@ -0,0 +1,568 @@ +from __future__ import print_function +from __future__ import division + +import numpy as np +from scipy import optimize , linalg +from scipy.linalg import LinAlgError + + +class BLR: + """Bayesian linear regression + + Estimation and prediction of Bayesian linear regression models + + Basic usage:: + + B = BLR() + hyp = B.estimate(hyp0, X, y) + ys,s2 = B.predict(hyp, X, y, Xs) + + where the variables are + + :param hyp: vector of hyperparmaters. + :param X: N x D data array + :param y: 1D Array of targets (length N) + :param Xs: Nte x D array of test cases + :param hyp0: starting estimates for hyperparameter optimisation + + :returns: * ys - predictive mean + * s2 - predictive variance + + The hyperparameters are:: + + hyp = ( log(beta), log(alpha) ) # hyp is a list or numpy array + + The implementation and notation mostly follows Bishop (2006). + The hyperparameter beta is the noise precision and alpha is the precision + over lengthscale parameters. This can be either a scalar variable (a + common lengthscale for all input variables), or a vector of length D (a + different lengthscale for each input variable, derived using an automatic + relevance determination formulation). These are estimated using conjugate + gradient optimisation of the marginal likelihood. + + Reference: + Bishop (2006) Pattern Recognition and Machine Learning, Springer + + Written by A. Marquand + """ + + def __init__(self, **kwargs): + # parse arguments + n_iter = kwargs.get('n_iter', 100) + tol = kwargs.get('tol', 1e-3) + verbose = kwargs.get('verbose', False) + var_groups = kwargs.get('var_groups', None) + var_covariates = kwargs.get('var_covariates', None) + warp = kwargs.get('warp', None) + warp_reparam = kwargs.get('warp_reparam', False) + + if var_groups is not None and var_covariates is not None: + raise ValueError("var_covariates and var_groups cannot both be used") + + # basic parameters + self.hyp = np.nan + self.nlZ = np.nan + self.tol = tol # not used at present + self.n_iter = n_iter + self.verbose = verbose + self.var_groups = var_groups + if var_covariates is not None: + self.hetero_var = True + else: + self.hetero_var = False + if self.var_groups is not None: + self.var_ids = set(self.var_groups) + self.var_ids = sorted(list(self.var_ids)) + + # set up warped likelihood + if verbose: + print('warp:', warp, 'warp_reparam:', warp_reparam) + if warp is None: + self.warp = None + self.n_warp_param = 0 + else: + self.warp = warp + self.n_warp_param = warp.get_n_params() + self.warp_reparam = warp_reparam + + self.gamma = None + + def _parse_hyps(self, hyp, X, Xv=None): + + N = X.shape[0] + + # noise precision + if Xv is not None: + if len(Xv.shape) == 1: + Dv = 1 + Xv = Xv[:, np.newaxis] + else: + Dv = Xv.shape[1] + w_d = np.asarray(hyp[0:Dv]) + beta = np.exp(Xv.dot(w_d)) + n_lik_param = len(w_d) + elif self.var_groups is not None: + beta = np.exp(hyp[0:len(self.var_ids)]) + n_lik_param = len(beta) + else: + beta = np.asarray([np.exp(hyp[0])]) + n_lik_param = len(beta) + + # parameters for warping the likelhood function + if self.warp is not None: + gamma = hyp[n_lik_param:(n_lik_param + self.n_warp_param)] + n_lik_param += self.n_warp_param + else: + gamma = None + + # precision for the coefficients + if isinstance(beta, list) or type(beta) is np.ndarray: + alpha = np.exp(hyp[n_lik_param:]) + else: + alpha = np.exp(hyp[1:]) + + # reparameterise the warp (WarpSinArcsinh only) + if self.warp is not None and self.warp_reparam: + delta = np.exp(gamma[1]) + beta = beta/(delta**2) + + # Create precision matrix from noise precision + if Xv is not None: + self.lambda_n_vec = beta + elif self.var_groups is not None: + beta_all = np.ones(N) + for v in range(len(self.var_ids)): + beta_all[self.var_groups == self.var_ids[v]] = beta[v] + self.lambda_n_vec = beta_all + else: + self.lambda_n_vec = np.ones(N)*beta + + return beta, alpha, gamma + + def post(self, hyp, X, y, Xv=None): + """ Generic function to compute posterior distribution. + + This function will save the posterior mean and precision matrix as + self.m and self.A and will also update internal parameters (e.g. + N, D and the prior covariance (Sigma_a) and precision (Lambda_a). + + :param hyp: hyperparameter vector + :param X: covariates + :param y: responses + :param Xv: covariates for heteroskedastic noise + """ + + N = X.shape[0] + if len(X.shape) == 1: + D = 1 + else: + D = X.shape[1] + + if (hyp == self.hyp).all() and hasattr(self, 'N'): + print("hyperparameters have not changed, exiting") + return + + beta, alpha, gamma = self._parse_hyps(hyp, X, Xv) + + if self.verbose: + print("estimating posterior ... | hyp=", hyp) + + # prior variance + if len(alpha) == 1 or len(alpha) == D: + self.Sigma_a = np.diag(np.ones(D))/alpha + self.Lambda_a = np.diag(np.ones(D))*alpha + else: + raise ValueError("hyperparameter vector has invalid length") + + # compute posterior precision and mean + # this is equivalent to the following operation but makes much more + # efficient use of memory by avoiding the need to store Lambda_n + # + # self.A = X.T.dot(self.Lambda_n).dot(X) + self.Lambda_a + # self.m = linalg.solve(self.A, X.T, + # check_finite=False).dot(self.Lambda_n).dot(y) + + XtLambda_n = X.T*self.lambda_n_vec + self.A = XtLambda_n.dot(X) + self.Lambda_a + invAXt = linalg.solve(self.A, X.T, check_finite=False) + self.m = (invAXt*self.lambda_n_vec).dot(y) + + # save stuff + self.N = N + self.D = D + self.hyp = hyp + + def loglik(self, hyp, X, y, Xv=None): + """ Function to compute compute log (marginal) likelihood """ + + # hyperparameters (alpha not needed) + beta, alpha, gamma = self._parse_hyps(hyp, X, Xv) + + # warp the likelihood? + if self.warp is not None: + if self.verbose: + print('warping input...') + y_unwarped = y + y = self.warp.f(y, gamma) + + # load posterior and prior covariance + if (hyp != self.hyp).any() or not(hasattr(self, 'A')): + try: + self.post(hyp, X, y, Xv) + except ValueError: + print("Warning: Estimation of posterior distribution failed") + nlZ = 1/np.finfo(float).eps + return nlZ + + try: + # compute the log determinants in a numerically stable way + logdetA = 2*sum(np.log(np.diag(np.linalg.cholesky(self.A)))) + except (ValueError, LinAlgError): + print("Warning: Estimation of posterior distribution failed") + nlZ = 1/np.finfo(float).eps + return nlZ + + logdetSigma_a = sum(np.log(np.diag(self.Sigma_a))) # diagonal + logdetSigma_n = sum(np.log(1/self.lambda_n_vec)) + + # compute negative marginal log likelihood + X_y_t_sLambda_n = (y-X.dot(self.m))*np.sqrt(self.lambda_n_vec) + nlZ = -0.5 * (-self.N*np.log(2*np.pi) - + logdetSigma_n - + logdetSigma_a - + X_y_t_sLambda_n.T.dot(X_y_t_sLambda_n) - + self.m.T.dot(self.Lambda_a).dot(self.m) - + logdetA + ) + + + if self.warp is not None: + # add in the Jacobian + nlZ = nlZ - sum(np.log(self.warp.df(y_unwarped, gamma))) + + # make sure the output is finite to stop the minimizer getting upset + if not np.isfinite(nlZ): + nlZ = 1/np.finfo(float).eps + + if self.verbose: + print("nlZ= ", nlZ, " | hyp=", hyp) + + self.nlZ = nlZ + return nlZ + + def penalized_loglik(self, hyp, X, y, Xv=None, l=0.1, norm='L1'): + """ Function to compute the penalized log (marginal) likelihood + + :param hyp: hyperparameter vector + :param X: covariates + :param y: responses + :param Xv: covariates for heteroskedastic noise + :param l: regularisation penalty + :param norm: type of regulariser (L1 or L2) + """ + + if norm.lower() == 'l1': + L = self.loglik(hyp, X, y, Xv) + l * sum(abs(hyp)) + elif norm.lower() == 'l2': + L = self.loglik(hyp, X, y, Xv) + l * sum(np.sqrt(hyp**2)) + else: + print("Requested penalty not recognized, choose between 'L1' or 'L2'.") + return L + + def dloglik(self, hyp, X, y, Xv=None): + """ Function to compute derivatives """ + + # hyperparameters + beta, alpha, gamma = self._parse_hyps(hyp, X, Xv) + + if self.warp is not None: + raise ValueError('optimization with derivatives is not yet ' + \ + 'supported for warped liklihood') + + # load posterior and prior covariance + if (hyp != self.hyp).any() or not(hasattr(self, 'A')): + try: + self.post(hyp, X, y, Xv) + except ValueError: + print("Warning: Estimation of posterior distribution failed") + dnlZ = np.sign(self.dnlZ) / np.finfo(float).eps + return dnlZ + + # precompute re-used quantities to maximise speed + # todo: revise implementation to use Cholesky throughout + # that would remove the need to explicitly compute the inverse + S = np.linalg.inv(self.A) # posterior covariance + SX = S.dot(X.T) + XLn = X.T*self.lambda_n_vec # = X.T.dot(self.Lambda_n) + XLny = XLn.dot(y) + SXLny = S.dot(XLny) + XLnXm = XLn.dot(X).dot(self.m) + + # initialise derivatives + dnlZ = np.zeros(hyp.shape) + dnl2 = np.zeros(hyp.shape) + + # noise precision parameter(s) + for i in range(0, len(beta)): + # first compute derivative of Lambda_n with respect to beta + dL_n_vec = np.zeros(self.N) + if self.var_groups is None: + dL_n_vec = np.ones(self.N) + else: + dL_n_vec[np.where(self.var_groups == self.var_ids[i])[0]] = 1 + dLambda_n = np.diag(dL_n_vec) + + # compute quantities used multiple times + XdLnX = X.T.dot(dLambda_n).dot(X) + dA = XdLnX + + # derivative of posterior parameters with respect to beta + b = -S.dot(dA).dot(SXLny) + SX.dot(dLambda_n).dot(y) + + # compute np.trace(self.Sigma_n.dot(dLambda_n)) efficiently + trSigma_ndLambda_n = sum((1/self.lambda_n_vec)*np.diag(dLambda_n)) + + # compute y.T.dot(Lambda_n) efficiently + ytLn = (y*self.lambda_n_vec).T + + # compute derivatives + dnlZ[i] = - (0.5 * trSigma_ndLambda_n - + 0.5 * y.dot(dLambda_n).dot(y) + + y.dot(dLambda_n).dot(X).dot(self.m) + + ytLn.dot(X).dot(b) - + 0.5 * self.m.T.dot(XdLnX).dot(self.m) - + b.T.dot(XLnXm) - + b.T.dot(self.Lambda_a).dot(self.m) - + 0.5 * np.trace(S.dot(dA)) + ) * beta[i] + + # scaling parameter(s) + for i in range(0, len(alpha)): + # first compute derivatives with respect to alpha + if len(alpha) == self.D: # are we using ARD? + dLambda_a = np.zeros((self.D, self.D)) + dLambda_a[i, i] = 1 + else: + dLambda_a = np.eye(self.D) + + F = dLambda_a + c = -S.dot(F).dot(SXLny) + + # compute np.trace(self.Sigma_a.dot(dLambda_a)) efficiently + trSigma_adLambda_a = sum(np.diag(self.Sigma_a)*np.diag(dLambda_a)) + + dnlZ[i+len(beta)] = -(0.5* trSigma_adLambda_a + + XLny.T.dot(c) - + c.T.dot(XLnXm) - + c.T.dot(self.Lambda_a).dot(self.m) - + 0.5 * self.m.T.dot(F).dot(self.m) - + 0.5*np.trace(linalg.solve(self.A, F)) + ) * alpha[i] + + # make sure the gradient is finite to stop the minimizer getting upset + if not all(np.isfinite(dnlZ)): + bad = np.where(np.logical_not(np.isfinite(dnlZ))) + for b in bad: + dnlZ[b] = np.sign(self.dnlZ[b]) / np.finfo(float).eps + + if self.verbose: + print("dnlZ= ", dnlZ, " | hyp=", hyp) + + self.dnlZ = dnlZ + return dnlZ + + # model estimation (optimization) + def estimate(self, hyp0, X, y, **kwargs): + """ Function to estimate the model + + :param hyp: hyperparameter vector + :param X: covariates + :param y: responses + :param optimizer: optimisation algorithm ('cg','powell','nelder-mead','l0bfgs-b') + """ + + optimizer = kwargs.get('optimizer','cg') + + # covariates for heteroskedastic noise + Xv = kwargs.get('var_covariates', None) + + # options for l-bfgs-b + l = float(kwargs.get('l', 0.1)) + epsilon = float(kwargs.get('epsilon', 0.1)) + norm = kwargs.get('norm', 'l2') + + if optimizer.lower() == 'cg': # conjugate gradients + out = optimize.fmin_cg(self.loglik, hyp0, self.dloglik, (X, y, Xv), + disp=True, gtol=self.tol, + maxiter=self.n_iter, full_output=1) + elif optimizer.lower() == 'powell': # Powell's method + out = optimize.fmin_powell(self.loglik, hyp0, (X, y, Xv), + full_output=1) + elif optimizer.lower() == 'nelder-mead': + out = optimize.fmin(self.loglik, hyp0, (X, y, Xv), + full_output=1) + elif optimizer.lower() == 'l-bfgs-b': + all_hyp_i = [hyp0] + def store(X): + hyp = X + all_hyp_i.append(hyp) + try: + out = optimize.fmin_l_bfgs_b(self.penalized_loglik, x0=hyp0, + args=(X, y, Xv, l, norm), approx_grad=True, + epsilon=epsilon, callback=store) + # If the matrix becomes singular restart at last found hyp + except np.linalg.LinAlgError: + print(f'Restarting estimation at hyp = {all_hyp_i[-1]}, due to *** numpy.linalg.LinAlgError: Matrix is singular.') + out = optimize.fmin_l_bfgs_b(self.penalized_loglik, x0=all_hyp_i[-1], + args=(X, y, Xv, l, norm), approx_grad=True, + epsilon=epsilon) + else: + raise ValueError("unknown optimizer") + + self.hyp = out[0] + self.nlZ = out[1] + self.optimizer = optimizer + + return self.hyp + + def predict(self, hyp, X, y, Xs, + var_groups_test=None, + var_covariates_test=None, **kwargs): + """ Function to make predictions from the model + + :param hyp: hyperparameter vector + :param X: covariates for training data + :param y: responses for training data + :param Xs: covariates for test data + :param var_covariates_test: test covariates for heteroskedastic noise + + This always returns Gaussian predictions, i.e. + + :returns: * ys - predictive mean + * s2 - predictive variance + """ + + Xvs = var_covariates_test + if Xvs is not None and len(Xvs.shape) == 1: + Xvs = Xvs[:, np.newaxis] + + if X is None or y is None: + # set dummy hyperparameters + beta, alpha, gamma = self._parse_hyps(hyp, np.zeros((self.N, self.D)), Xvs) + else: + + # set hyperparameters + beta, alpha, gamma = self._parse_hyps(hyp, X, Xvs) + + # do we need to re-estimate the posterior? + if (hyp != self.hyp).any() or not(hasattr(self, 'A')): + raise(ValueError, 'posterior not properly estimated') + + N_test = Xs.shape[0] + + ys = Xs.dot(self.m) + + if self.var_groups is not None: + if len(var_groups_test) != N_test: + raise(ValueError, 'Invalid variance groups for test') + # separate variance groups + s2n = np.ones(N_test) + for v in range(len(self.var_ids)): + s2n[var_groups_test == self.var_ids[v]] = 1/beta[v] + else: + s2n = 1/beta + + # compute xs.dot(S).dot(xs.T) avoiding computing off-diagonal entries + s2 = s2n + np.sum(Xs*linalg.solve(self.A, Xs.T).T, axis=1) + + return ys, s2 + + def predict_and_adjust(self, hyp, X, y, Xs=None, + ys=None, + var_groups_test=None, + var_groups_adapt=None, **kwargs): + """ Function to transfer the model to a new site. This is done by + first making predictions on the adaptation data given by X, + adjusting by the residuals with respect to y. + + :param hyp: hyperparameter vector + :param X: covariates for adaptation (i.e. calibration) data + :param y: responses for adaptation data + :param Xs: covariate data (for which predictions should be adjusted) + :param ys: true response variables (to be adjusted) + :param var_groups_test: variance groups (e.g. sites) for test data + :param var_groups_adapt: variance groups for adaptation data + + There are two possible ways of using this function, depending on + whether ys or Xs is specified + + If ys is specified, this is applied directly to the data, which is + assumed to be in the input space (i.e. not warped). In this case + the adjusted true data points are returned in the same space + + Alternatively, Xs is specified, then the predictions are made and + adjusted. In this case the predictive variance are returned in the + warped (i.e. Gaussian) space. + + This function needs to know which sites are associated with which + data points, which provided by var_groups_xxx, which is a list or + array of scalar ids . + """ + + if ys is None: + if Xs is None: + raise ValueError('Either ys or Xs must be specified') + else: + N = Xs.shape[0] + else: + if len(ys.shape) < 1: + raise ValueError('ys is specified but has insufficent length') + N = ys.shape[0] + + if var_groups_test is None: + var_groups_test = np.ones(N) + var_groups_adapt = np.ones(X.shape[0]) + + ys_out = np.zeros(N) + s2_out = np.zeros(N) + for g in np.unique(var_groups_test): + idx_s = var_groups_test == g + idx_a = var_groups_adapt == g + + if sum(idx_a) < 2: + raise ValueError('Insufficient adaptation data to estimate variance') + + # Get predictions from old model on new data X + ys_ref, s2_ref = self.predict(hyp, None, None, X[idx_a,:]) + + # Subtract the predictions from true data to get the residuals + if self.warp is None: + residuals = ys_ref-y[idx_a] + else: + # Calculate the residuals in warped space + y_ref_ws = self.warp.f(y[idx_a], hyp[1:self.warp.get_n_params()+1]) + residuals = ys_ref - y_ref_ws + + residuals_mu = np.mean(residuals) + residuals_sd = np.std(residuals) + + # Adjust the mean with the mean of the residuals + if ys is None: + # make and adjust predictions + ys_out[idx_s], s2_out[idx_s] = self.predict(hyp, None, None, Xs[idx_s,:]) + ys_out[idx_s] = ys_out[idx_s] - residuals_mu + + # Set the deviation to the devations of the residuals + s2_out[idx_s] = np.ones(len(s2_out[idx_s]))*residuals_sd**2 + else: + # adjust the data + if self.warp is not None: + y_ws = self.warp.f(ys[idx_s], hyp[1:self.warp.get_n_params()+1]) + ys_out[idx_s] = y_ws + residuals_mu + ys_out[idx_s] = self.warp.invf(ys_out[idx_s], hyp[1:self.warp.get_n_params()+1]) + else: + ys = ys - residuals_mu + s2_out = None + + return ys_out, s2_out + diff --git a/build/lib/pcntoolkit/model/gp.py b/build/lib/pcntoolkit/model/gp.py new file mode 100644 index 00000000..b2fb1b75 --- /dev/null +++ b/build/lib/pcntoolkit/model/gp.py @@ -0,0 +1,488 @@ +from __future__ import print_function +from __future__ import division + +import os +import sys +import numpy as np +from scipy import optimize +from numpy.linalg import solve, LinAlgError +from numpy.linalg import cholesky as chol +from six import with_metaclass +from abc import ABCMeta, abstractmethod + + +try: # Run as a package if installed + from pcntoolkit.util.utils import squared_dist +except ImportError: + pass + + path = os.path.abspath(os.path.dirname(__file__)) + path = os.path.dirname(path) # parent directory + if path not in sys.path: + sys.path.append(path) + del path + + from util.utils import squared_dist + +# -------------------- +# Covariance functions +# -------------------- + + +class CovBase(with_metaclass(ABCMeta)): + """ Base class for covariance functions. + + All covariance functions must define the following methods:: + + CovFunction.get_n_params() + CovFunction.cov() + CovFunction.xcov() + CovFunction.dcov() + """ + + def __init__(self, x=None): + self.n_params = np.nan + + def get_n_params(self): + """ Report the number of parameters required """ + + assert not np.isnan(self.n_params), \ + "Covariance function not initialised" + + return self.n_params + + @abstractmethod + def cov(self, theta, x, z=None): + """ Return the full covariance (or cross-covariance if z is given) """ + + @abstractmethod + def dcov(self, theta, x, i): + """ Return the derivative of the covariance function with respect to + the i-th hyperparameter """ + + +class CovLin(CovBase): + """ Linear covariance function (no hyperparameters) + """ + + def __init__(self, x=None): + self.n_params = 0 + self.first_call = False + + 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: + print("CovLin: ignoring unnecessary hyperparameter ...") + + if z is None: + z = x + + K = x.dot(z.T) + return K + + def dcov(self, theta, x, i): + raise ValueError("Invalid covariance function parameter") + + +class CovSqExp(CovBase): + """ Ordinary squared exponential covariance function. + The hyperparameters are:: + + theta = ( log(ell), log(sf) ) + + where ell is a lengthscale parameter and sf2 is the signal variance + """ + + def __init__(self, x=None): + self.n_params = 2 + + def cov(self, theta, x, z=None): + self.ell = np.exp(theta[0]) + self.sf2 = np.exp(2*theta[1]) + + if z is None: + z = x + + R = squared_dist(x/self.ell, z/self.ell) + K = self.sf2 * np.exp(-R/2) + return K + + def dcov(self, theta, x, i): + self.ell = np.exp(theta[0]) + self.sf2 = np.exp(2*theta[1]) + + R = squared_dist(x/self.ell, x/self.ell) + + if i == 0: # return derivative of lengthscale parameter + dK = self.sf2 * np.exp(-R/2) * R + return dK + elif i == 1: # return derivative of signal variance parameter + dK = 2*self.sf2 * np.exp(-R/2) + return dK + else: + raise ValueError("Invalid covariance function parameter") + + +class CovSqExpARD(CovBase): + """ Squared exponential covariance function with ARD + The hyperparameters are:: + + theta = (log(ell_1, ..., log_ell_D), log(sf)) + + where ell_i are lengthscale parameters and sf2 is the signal variance + """ + + def __init__(self, x=None): + if x is None: + raise ValueError("N x D data matrix must be supplied as input") + if len(x.shape) == 1: + self.D = 1 + else: + self.D = x.shape[1] + self.n_params = self.D + 1 + + def cov(self, theta, x, z=None): + self.ell = np.exp(theta[0:self.D]) + self.sf2 = np.exp(2*theta[self.D]) + + if z is None: + z = x + + R = squared_dist(x.dot(np.diag(1./self.ell)), + z.dot(np.diag(1./self.ell))) + K = self.sf2*np.exp(-R/2) + return K + + 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]) + return dK + elif i == self.D: # return derivative of signal variance parameter + dK = 2*K + return dK + else: + raise ValueError("Invalid covariance function parameter") + + +class CovSum(CovBase): + """ Sum of covariance functions. These are passed in as a cell array and + intialised automatically. For example:: + + C = CovSum(x,(CovLin, CovSqExpARD)) + C = CovSum.cov(x, ) + + The hyperparameters are:: + + theta = ( log(ell_1, ..., log_ell_D), log(sf2) ) + + where ell_i are lengthscale parameters and sf2 is the signal variance + """ + + def __init__(self, x=None, covfuncnames=None): + if x is None: + raise ValueError("N x D data matrix must be supplied as input") + if covfuncnames is None: + raise ValueError("A list of covariance functions is required") + self.covfuncs = [] + self.n_params = 0 + for cname in covfuncnames: + covfunc = eval(cname + '(x)') + self.n_params += covfunc.get_n_params() + self.covfuncs.append(covfunc) + + if len(x.shape) == 1: + self.N = len(x) + self.D = 1 + else: + self.N, self.D = x.shape + + def cov(self, theta, x, z=None): + theta_offset = 0 + for ci, covfunc in enumerate(self.covfuncs): + try: + n_params_c = covfunc.get_n_params() + theta_c = [theta[c] for c in + range(theta_offset, theta_offset + n_params_c)] + theta_offset += n_params_c + except Exception as e: + print(e) + + if ci == 0: + K = covfunc.cov(theta_c, x, z) + else: + K += covfunc.cov(theta_c, x, z) + return K + + def dcov(self, theta, x, i): + theta_offset = 0 + for covfunc in self.covfuncs: + n_params_c = covfunc.get_n_params() + theta_c = [theta[c] for c in + range(theta_offset, theta_offset + n_params_c)] + theta_offset += n_params_c + + if theta_c: # does the variable have any hyperparameters? + if 'dK' not in locals(): + dK = covfunc.dcov(theta_c, x, i) + else: + dK += covfunc.dcov(theta_c, x, i) + return dK + +# ----------------------- +# Gaussian process models +# ----------------------- + + +class GPR: + """Gaussian process regression + + Estimation and prediction of Gaussian process regression models + + Basic usage:: + + G = GPR() + hyp = B.estimate(hyp0, cov, X, y) + ys, ys2 = B.predict(hyp, cov, X, y, Xs) + + where the variables are + + :param hyp: vector of hyperparmaters + :param cov: covariance function + :param X: N x D data array + :param y: 1D Array of targets (length N) + :param Xs: Nte x D array of test cases + :param hyp0: starting estimates for hyperparameter optimisation + + :returns: * ys - predictive mean + * ys2 - predictive variance + + The hyperparameters are:: + + hyp = ( log(sn), (cov function params) ) # hyp is a list or array + + The implementation and notation follows Rasmussen and Williams (2006). + As in the gpml toolbox, these parameters are estimated using conjugate + gradient optimisation of the marginal likelihood. Note that there is no + explicit mean function, thus the gpr routines are limited to modelling + zero-mean processes. + + Reference: + C. Rasmussen and C. Williams (2006) Gaussian Processes for Machine Learning + + Written by A. Marquand + """ + + def __init__(self, hyp=None, covfunc=None, X=None, y=None, n_iter=100, + tol=1e-3, verbose=False, warp=None): + + self.hyp = np.nan + self.nlZ = np.nan + self.tol = tol # not used at present + self.n_iter = n_iter + self.verbose = verbose + + # set up warped likelihood + if warp is None: + self.warp = None + self.n_warp_param = 0 + else: + self.warp = warp + self.n_warp_param = warp.get_n_params() + + self.gamma = None + + def _updatepost(self, hyp, covfunc): + + hypeq = np.asarray(hyp == self.hyp) + if hypeq.all() and hasattr(self, 'alpha') and \ + (hasattr(self, 'covfunc') and covfunc == self.covfunc): + return False + else: + return True + + def post(self, hyp, covfunc, X, y): + """ Generic function to compute posterior distribution. + """ + + if len(hyp.shape) > 1: # force 1d hyperparameter array + hyp = hyp.flatten() + + if len(X.shape) == 1: + X = X[:, np.newaxis] + self.N, self.D = X.shape + + # hyperparameters + sn2 = np.exp(2*hyp[0]) # noise variance + if self.warp is not None: # parameters for warping the likelhood + n_lik_param = self.n_warp_param+1 + else: + n_lik_param = 1 + theta = hyp[n_lik_param:] # (generic) covariance hyperparameters + + if self.verbose: + print("estimating posterior ... | hyp=", hyp) + + self.K = covfunc.cov(theta, X) + self.L = chol(self.K + sn2*np.eye(self.N)) + self.alpha = solve(self.L.T, solve(self.L, y)) + self.hyp = hyp + self.covfunc = covfunc + + def loglik(self, hyp, covfunc, X, y): + """ Function to compute compute log (marginal) likelihood + """ + + # load or recompute posterior + if self.verbose: + print("computing likelihood ... | hyp=", hyp) + + # parameters for warping the likelhood function + if self.warp is not None: + gamma = hyp[1:(self.n_warp_param+1)] + y = self.warp.f(y, gamma) + y_unwarped = y + + if len(hyp.shape) > 1: # force 1d hyperparameter array + hyp = hyp.flatten() + if self._updatepost(hyp, covfunc): + try: + self.post(hyp, covfunc, X, y) + except (ValueError, LinAlgError): + print("Warning: Estimation of posterior distribution failed") + self.nlZ = 1/np.finfo(float).eps + return self.nlZ + + self.nlZ = 0.5*y.T.dot(self.alpha) + sum(np.log(np.diag(self.L))) + \ + 0.5*self.N*np.log(2*np.pi) + + if self.warp is not None: + # add in the Jacobian + self.nlZ = self.nlZ - sum(np.log(self.warp.df(y_unwarped, gamma))) + + # make sure the output is finite to stop the minimizer getting upset + if not np.isfinite(self.nlZ): + self.nlZ = 1/np.finfo(float).eps + + if self.verbose: + print("nlZ= ", self.nlZ, " | hyp=", hyp) + + return self.nlZ + + def dloglik(self, hyp, covfunc, X, y): + """ Function to compute derivatives + """ + + if len(hyp.shape) > 1: # force 1d hyperparameter array + hyp = hyp.flatten() + + if self.warp is not None: + raise ValueError('optimization with derivatives is not yet ' + \ + 'supported for warped liklihood') + + # hyperparameters + sn2 = np.exp(2*hyp[0]) # noise variance + theta = hyp[1:] # (generic) covariance hyperparameters + + # load posterior and prior covariance + if self._updatepost(hyp, covfunc): + try: + self.post(hyp, covfunc, X, y) + except (ValueError, LinAlgError): + print("Warning: Estimation of posterior distribution failed") + dnlZ = np.sign(self.dnlZ) / np.finfo(float).eps + return dnlZ + + # compute Q = alpha*alpha' - inv(K) + Q = np.outer(self.alpha, self.alpha) - \ + solve(self.L.T, solve(self.L, np.eye(self.N))) + + # initialise derivatives + self.dnlZ = np.zeros(len(hyp)) + + # noise variance + self.dnlZ[0] = -sn2*np.trace(Q) + + # covariance parameter(s) + for par in range(0, len(theta)): + # compute -0.5*trace(Q.dot(dK/d[theta_i])) efficiently + dK = covfunc.dcov(theta, X, i=par) + self.dnlZ[par+1] = -0.5*np.sum(np.sum(Q*dK.T)) + + # make sure the gradient is finite to stop the minimizer getting upset + if not all(np.isfinite(self.dnlZ)): + bad = np.where(np.logical_not(np.isfinite(self.dnlZ))) + for b in bad: + self.dnlZ[b] = np.sign(self.dnlZ[b]) / np.finfo(float).eps + + if self.verbose: + print("dnlZ= ", self.dnlZ, " | hyp=", hyp) + + return self.dnlZ + + # model estimation (optimization) + def estimate(self, hyp0, covfunc, X, y, optimizer='cg'): + """ Function to estimate the model + """ + if len(X.shape) == 1: + X = X[:, np.newaxis] + + self.hyp0 = hyp0 + + if optimizer.lower() == 'cg': # conjugate gradients + out = optimize.fmin_cg(self.loglik, hyp0, self.dloglik, + (covfunc, X, y), disp=True, gtol=self.tol, + maxiter=self.n_iter, full_output=1) + + elif optimizer.lower() == 'powell': # Powell's method + out = optimize.fmin_powell(self.loglik, hyp0, (covfunc, X, y), + full_output=1) + else: + raise ValueError("unknown optimizer") + + # Always return a 1d array. The optimizer sometimes changes dimesnions + if len(out[0].shape) > 1: + self.hyp = out[0].flatten() + else: + self.hyp = out[0] + self.nlZ = out[1] + self.optimizer = optimizer + + return self.hyp + + def predict(self, hyp, X, y, Xs): + """ Function to make predictions from the model + """ + if len(hyp.shape) > 1: # force 1d hyperparameter array + hyp = hyp.flatten() + + # ensure X and Xs are multi-dimensional arrays + if len(Xs.shape) == 1: + Xs = Xs[:, np.newaxis] + if len(X.shape) == 1: + X = X[:, np.newaxis] + + # parameters for warping the likelhood function + if self.warp is not None: + gamma = hyp[1:(self.n_warp_param+1)] + y = self.warp.f(y, gamma) + + # reestimate posterior (avoids numerical problems with optimizer) + self.post(hyp, self.covfunc, X, y) + + # hyperparameters + sn2 = np.exp(2*hyp[0]) # noise variance + theta = hyp[(self.n_warp_param + 1):] # (generic) covariance hyperparameters + + Ks = self.covfunc.cov(theta, Xs, X) + kss = self.covfunc.cov(theta, Xs) + + # predictive mean + ymu = Ks.dot(self.alpha) + + # predictive variance (for a noisy test input) + v = solve(self.L, Ks.T) + ys2 = kss - v.T.dot(v) + sn2 + + return ymu, ys2 diff --git a/build/lib/pcntoolkit/model/hbr.py b/build/lib/pcntoolkit/model/hbr.py new file mode 100644 index 00000000..afb86899 --- /dev/null +++ b/build/lib/pcntoolkit/model/hbr.py @@ -0,0 +1,923 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Thu Jul 25 13:23:15 2019 + +@author: seykia +@author: augub +""" + +from __future__ import print_function +from __future__ import division +from ast import Param +from tkinter.font import names + +import numpy as np +import pymc3 as pm +import theano +from itertools import product +from functools import reduce + +import theano +from pymc3 import Metropolis, NUTS, Slice, HamiltonianMC +from scipy import stats +import bspline +from bspline import splinelab + +from model.SHASH import SHASHo2, SHASHb, SHASHo +from util.utils import create_poly_basis +from util.utils import expand_all +from pcntoolkit.util.utils import cartesian_product + +from theano import printing, function + +def bspline_fit(X, order, nknots): + feature_num = X.shape[1] + bsp_basis = [] + + for i in range(feature_num): + minx = np.min(X[:,i]) + maxx = np.max(X[:,i]) + delta = maxx-minx + # Expand range by 20% (10% on both sides) + splinemin = minx-0.1*delta + splinemax = maxx+0.1*delta + knots = np.linspace(splinemin, splinemax, nknots) + k = splinelab.augknt(knots, order) + bsp_basis.append(bspline.Bspline(k, order)) + + return bsp_basis + +def bspline_transform(X, bsp_basis): + if type(bsp_basis) != list: + temp = [] + temp.append(bsp_basis) + bsp_basis = temp + + feature_num = len(bsp_basis) + X_transformed = [] + for f in range(feature_num): + X_transformed.append(np.array([bsp_basis[f](i) for i in X[:, f]])) + X_transformed = np.concatenate(X_transformed, axis=1) + + return X_transformed + +def create_poly_basis(X, order): + """ compute a polynomial basis expansion of the specified order""" + + if len(X.shape) == 1: + X = X[:, np.newaxis] + D = X.shape[1] + Phi = np.zeros((X.shape[0], D * order)) + colid = np.arange(0, D) + for d in range(1, order + 1): + Phi[:, colid] = X ** d + colid += D + + return Phi + + +def from_posterior(param, samples, distribution=None, half=False, freedom=1): + if len(samples.shape) > 1: + shape = samples.shape[1:] + else: + shape = None + + if (distribution is None): + smin, smax = np.min(samples), np.max(samples) + width = smax - smin + x = np.linspace(smin, smax, 1000) + y = stats.gaussian_kde(np.ravel(samples))(x) + if half: + x = np.concatenate([x, [x[-1] + 0.1 * width]]) + y = np.concatenate([y, [0]]) + else: + x = np.concatenate([[x[0] - 0.1 * width], x, [x[-1] + 0.1 * width]]) + y = np.concatenate([[0], y, [0]]) + if shape is None: + return pm.distributions.Interpolated(param, x, y) + else: + return pm.distributions.Interpolated(param, x, y, shape=shape) + elif (distribution == 'normal'): + temp = stats.norm.fit(samples) + if shape is None: + return pm.Normal(param, mu=temp[0], sigma=freedom * temp[1]) + else: + return pm.Normal(param, mu=temp[0], sigma=freedom * temp[1], shape=shape) + elif (distribution == 'hnormal'): + temp = stats.halfnorm.fit(samples) + if shape is None: + return pm.HalfNormal(param, sigma=freedom * temp[1]) + else: + return pm.HalfNormal(param, sigma=freedom * temp[1], shape=shape) + elif (distribution == 'hcauchy'): + temp = stats.halfcauchy.fit(samples) + if shape is None: + return pm.HalfCauchy(param, freedom * temp[1]) + else: + return pm.HalfCauchy(param, freedom * temp[1], shape=shape) + elif (distribution == 'uniform'): + upper_bound = np.percentile(samples, 95) + lower_bound = np.percentile(samples, 5) + r = np.abs(upper_bound - lower_bound) + if shape is None: + return pm.Uniform(param, lower=lower_bound - freedom * r, + upper=upper_bound + freedom * r) + else: + return pm.Uniform(param, lower=lower_bound - freedom * r, + upper=upper_bound + freedom * r, shape=shape) + elif (distribution == 'huniform'): + upper_bound = np.percentile(samples, 95) + lower_bound = np.percentile(samples, 5) + r = np.abs(upper_bound - lower_bound) + if shape is None: + return pm.Uniform(param, lower=0, upper=upper_bound + freedom * r) + else: + return pm.Uniform(param, lower=0, upper=upper_bound + freedom * r, shape=shape) + + elif (distribution == 'gamma'): + alpha_fit, loc_fit, invbeta_fit = stats.gamma.fit(samples) + if shape is None: + return pm.Gamma(param, alpha=freedom * alpha_fit, beta=freedom / invbeta_fit) + else: + return pm.Gamma(param, alpha=freedom * alpha_fit, beta=freedom / invbeta_fit, shape=shape) + + elif (distribution == 'igamma'): + alpha_fit, loc_fit, beta_fit = stats.gamma.fit(samples) + if shape is None: + return pm.InverseGamma(param, alpha=freedom * alpha_fit, beta=freedom * beta_fit) + else: + return pm.InverseGamma(param, alpha=freedom * alpha_fit, beta=freedom * beta_fit, shape=shape) + + +def hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): + """ + :param X: [N×P] array of clinical covariates + :param y: [N×1] array of neuroimaging measures + :param batch_effects: [N×M] array of batch effects + :param batch_effects_size: [b1, b2,...,bM] List of counts of unique values of batch effects + :param configs: + :param trace: + :param return_shared_variables: If true, returns references to the shared variables. The values of the shared variables can be set manually, allowing running the same model on different data without re-compiling it. + :return: + """ + X = theano.shared(X) + X = theano.tensor.cast(X,'floatX') + y = theano.shared(y) + y = theano.tensor.cast(y,'floatX') + + + with pm.Model() as model: + + # Make a param builder that will make the correct calls + pb = ParamBuilder(model, X, y, batch_effects, trace, configs) + + if configs['likelihood'] == 'Normal': + mu = pb.make_param("mu").get_samples(pb) + sigma = pb.make_param("sigma").get_samples(pb) + sigma_plus = pm.math.log(1+pm.math.exp(sigma)) + y_like = pm.Normal('y_like',mu=mu, sigma=sigma_plus, observed=y) + + elif configs['likelihood'] in ['SHASHb','SHASHo','SHASHo2']: + """ + Comment 1 + The current parameterizations are tuned towards standardized in- and output data. + It is possible to adjust the priors through the XXX_dist and XXX_params kwargs, like here we do with epsilon_params. + Supported distributions are listed in the Prior class. + + Comment 2 + Any mapping that is applied here after sampling should also be applied in util.hbr_utils.forward in order for the functions there to properly work. + For example, the softplus applied to sigma here is also applied in util.hbr_utils.forward + """ + SHASH_map = {'SHASHb':SHASHb,'SHASHo':SHASHo,'SHASHo2':SHASHo2} + + mu = pb.make_param("mu", slope_mu_params = (0.,3.), mu_intercept_mu_params=(0.,1.), sigma_intercept_mu_params = (1.,)).get_samples(pb) + sigma = pb.make_param("sigma", sigma_params = (1.,2.), slope_sigma_params=(0.,1.), intercept_sigma_params = (1., 1.)).get_samples(pb) + sigma_plus = pm.math.log(1+pm.math.exp(sigma)) + epsilon = pb.make_param("epsilon", epsilon_params = (0.,1.), slope_epsilon_params=(0.,1.), intercept_epsilon_params=(0.,1)).get_samples(pb) + delta = pb.make_param("delta", delta_params=(1.5,2.), slope_delta_params=(0.,1), intercept_delta_params=(2., 1)).get_samples(pb) + delta_plus = pm.math.log(1+pm.math.exp(delta)) + 0.3 + y_like = SHASH_map[configs['likelihood']]('y_like', mu=mu, sigma=sigma_plus, epsilon=epsilon, delta=delta_plus, observed = y) + + return model + + + +class HBR: + + """Hierarchical Bayesian Regression for normative modeling + + Basic usage:: + + model = HBR(configs) + trace = model.estimate(X, y, batch_effects) + ys,s2 = model.predict(X, batch_effects) + + where the variables are + + :param configs: a dictionary of model configurations. + :param X: N-by-P input matrix of P features for N subjects + :param y: N-by-1 vector of outputs. + :param batch_effects: N-by-B matrix of B batch ids for N subjects. + + :returns: * ys - predictive mean + * s2 - predictive variance + + Written by S.M. Kia + """ + + def get_step_methods(self, m): + """ + This can be used to assign default step functions. However, the nuts initialization keyword doesnt work together with this... so better not use it. + + STEP_METHODS = ( + NUTS, + HamiltonianMC, + Metropolis, + BinaryMetropolis, + BinaryGibbsMetropolis, + Slice, + CategoricalGibbsMetropolis, + ) + :param m: a PyMC3 model + :return: + """ + samplermap = {'NUTS': NUTS, 'MH': Metropolis, 'Slice': Slice, 'HMC': HamiltonianMC} + fallbacks = [Metropolis] # We are using MH as a fallback method here + if self.configs['sampler'] == 'NUTS': + step_kwargs = {'nuts': {'target_accept': self.configs['target_accept']}} + else: + step_kwargs = None + return pm.sampling.assign_step_methods(m, methods=[samplermap[self.configs['sampler']]] + fallbacks, + step_kwargs=step_kwargs) + + def __init__(self, configs): + self.bsp = None + self.model_type = configs['type'] + self.configs = configs + + def get_modeler(self): + return {'nn': nn_hbr}.get(self.model_type, hbr) + + def transform_X(self, X): + if self.model_type == 'polynomial': + Phi = create_poly_basis(X, self.configs['order']) + elif self.model_type == 'bspline': + if self.bsp is None: + self.bsp = bspline_fit(X, self.configs['order'], self.configs['nknots']) + bspline = bspline_transform(X, self.bsp) + Phi = np.concatenate((X, bspline), axis = 1) + else: + Phi = X + return Phi + + + def find_map(self, X, y, batch_effects,method='L-BFGS-B'): + """ Function to estimate the model """ + X, y, batch_effects = expand_all(X, y, batch_effects) + + self.batch_effects_num = batch_effects.shape[1] + self.batch_effects_size = [] + for i in range(self.batch_effects_num): + self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) + + X = self.transform_X(X) + modeler = self.get_modeler() + with modeler(X, y, batch_effects, self.batch_effects_size, self.configs) as m: + self.MAP = pm.find_MAP(method=method) + return self.MAP + + def estimate(self, X, y, batch_effects): + + """ Function to estimate the model """ + X, y, batch_effects = expand_all(X, y, batch_effects) + + self.batch_effects_num = batch_effects.shape[1] + self.batch_effects_size = [] + for i in range(self.batch_effects_num): + self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) + + X = self.transform_X(X) + modeler = self.get_modeler() + with modeler(X, y, batch_effects, self.batch_effects_size, self.configs) as m: + self.trace = pm.sample(draws=self.configs['n_samples'], + tune=self.configs['n_tuning'], + chains=self.configs['n_chains'], + init=self.configs['init'], n_init=500000, + cores=self.configs['cores']) + return self.trace + + def predict(self, X, batch_effects, pred='single'): + """ Function to make predictions from the model """ + X, batch_effects = expand_all(X, batch_effects) + + samples = self.configs['n_samples'] + y = np.zeros([X.shape[0], 1]) + + if pred == 'single': + X = self.transform_X(X) + modeler = self.get_modeler() + with modeler(X, y, batch_effects, self.batch_effects_size, self.configs): + ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) + pred_mean = ppc['y_like'].mean(axis=0) + pred_var = ppc['y_like'].var(axis=0) + + return pred_mean, pred_var + + def estimate_on_new_site(self, X, y, batch_effects): + """ Function to adapt the model """ + X, y, batch_effects = expand_all(X, y, batch_effects) + + self.batch_effects_num = batch_effects.shape[1] + self.batch_effects_size = [] + for i in range(self.batch_effects_num): + self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) + + X = self.transform_X(X) + modeler = self.get_modeler() + with modeler(X, y, batch_effects, self.batch_effects_size, + self.configs, trace=self.trace) as m: + self.trace = pm.sample(self.configs['n_samples'], + tune=self.configs['n_tuning'], + chains=self.configs['n_chains'], + target_accept=self.configs['target_accept'], + init=self.configs['init'], n_init=50000, + cores=self.configs['cores']) + return self.trace + + def predict_on_new_site(self, X, batch_effects): + """ Function to make predictions from the model """ + X, batch_effects = expand_all(X, batch_effects) + + samples = self.configs['n_samples'] + y = np.zeros([X.shape[0], 1]) + + X = self.transform_X(X) + modeler = self.get_modeler() + with modeler(X, y, batch_effects, self.batch_effects_size, self.configs, trace=self.trace): + ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) + pred_mean = ppc['y_like'].mean(axis=0) + pred_var = ppc['y_like'].var(axis=0) + + return pred_mean, pred_var + + def generate(self, X, batch_effects, samples): + """ Function to generate samples from posterior predictive distribution """ + X, batch_effects = expand_all(X, batch_effects) + + y = np.zeros([X.shape[0], 1]) + + X = self.transform_X(X) + modeler = self.get_modeler() + with modeler(X, y, batch_effects, self.batch_effects_size, self.configs): + ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) + + generated_samples = np.reshape(ppc['y_like'].squeeze().T, [X.shape[0] * samples, 1]) + X = np.repeat(X, samples) + if len(X.shape) == 1: + X = np.expand_dims(X, axis=1) + batch_effects = np.repeat(batch_effects, samples, axis=0) + if len(batch_effects.shape) == 1: + batch_effects = np.expand_dims(batch_effects, axis=1) + + return X, batch_effects, generated_samples + + def sample_prior_predictive(self, X, batch_effects, samples, trace=None): + """ Function to sample from prior predictive distribution """ + + if len(X.shape) == 1: + X = np.expand_dims(X, axis=1) + if len(batch_effects.shape) == 1: + batch_effects = np.expand_dims(batch_effects, axis=1) + + self.batch_effects_num = batch_effects.shape[1] + self.batch_effects_size = [] + for i in range(self.batch_effects_num): + self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) + + y = np.zeros([X.shape[0], 1]) + + if self.model_type == 'linear': + with hbr(X, y, batch_effects, self.batch_effects_size, self.configs, + trace): + ppc = pm.sample_prior_predictive(samples=samples) + return ppc + + def get_model(self, X, y, batch_effects): + X, y, batch_effects = expand_all(X, y, batch_effects) + + self.batch_effects_num = batch_effects.shape[1] + self.batch_effects_size = [] + for i in range(self.batch_effects_num): + self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) + modeler = self.get_modeler() + X = self.transform_X(X) + return modeler(X, y, batch_effects, self.batch_effects_size, self.configs, self.trace) + + def create_dummy_inputs(self, covariate_ranges = [[0.1,0.9,0.01]]): + + arrays = [] + for i in range(len(covariate_ranges)): + arrays.append(np.arange(covariate_ranges[i][0],covariate_ranges[i][1], covariate_ranges[i][2])) + X = cartesian_product(arrays) + X_dummy = np.concatenate([X for i in range(np.prod(self.batch_effects_size))]) + + arrays = [] + for i in range(self.batch_effects_num): + arrays.append(np.arange(0, self.batch_effects_size[i])) + batch_effects = cartesian_product(arrays) + batch_effects_dummy = np.repeat(batch_effects, X.shape[0], axis=0) + + return X_dummy, batch_effects_dummy + +class Prior: + """ + A wrapper class for a PyMC3 distribution. + - creates a fitted distribution from the trace, if one is present + - overloads the __getitem__ function with something that switches between indexing or not, based on the shape + """ + def __init__(self, name, dist, params, pb, shape=(1,)) -> None: + self.dist = None + self.name = name + self.shape = shape + self.has_random_effect = True if len(shape)>1 else False + self.distmap = {'normal': pm.Normal, + 'hnormal': pm.HalfNormal, + 'gamma': pm.Gamma, + 'uniform': pm.Uniform, + 'igamma': pm.InverseGamma, + 'hcauchy': pm.HalfCauchy} + self.make_dist(dist, params, pb) + + def make_dist(self, dist, params, pb): + """This creates a pymc3 distribution. If there is a trace, the distribution is fitted to the trace. If there isn't a trace, the prior is parameterized by the values in (params)""" + with pb.model as m: + if (pb.trace is not None) and (not self.has_random_effect): + int_dist = from_posterior(param=self.name, + samples=pb.trace[self.name], + distribution=dist, + freedom=pb.configs['freedom']) + self.dist = int_dist.reshape(self.shape) + else: + shape_prod = np.product(np.array(self.shape)) + print(self.name) + print(f"dist={dist}") + print(f"params={params}") + int_dist = self.distmap[dist](self.name, *params, shape=shape_prod) + self.dist = int_dist.reshape(self.shape) + + def __getitem__(self, idx): + """The idx here is the index of the batch-effect. If the prior does not model batch effects, this should return the same value for each index""" + assert self.dist is not None, "Distribution not initialized" + if self.has_random_effect: + return self.dist[idx] + else: + return self.dist + + +class ParamBuilder: + """ + A class that simplifies the construction of parameterizations. + It has a lot of attributes necessary for creating the model, including the data, but it is never saved with the model. + It also contains a lot of decision logic for creating the parameterizations. + """ + + def __init__(self, model, X, y, batch_effects, trace, configs): + """ + + :param model: model to attach all the distributions to + :param X: Covariates + :param y: IDPs + :param batch_effects: I guess this speaks for itself + :param trace: idem + :param configs: idem + """ + self.model = model + self.X = X + self.y = y + self.batch_effects = batch_effects + self.trace = trace + self.configs = configs + + self.feature_num = X.shape[1].eval().item() + self.y_shape = y.shape.eval() + self.n_ys = y.shape[0].eval().item() + self.batch_effects_num = batch_effects.shape[1] + + self.batch_effects_size = [] + self.all_idx = [] + for i in range(self.batch_effects_num): + # Count the unique values for each batch effect + self.batch_effects_size.append(len(np.unique(self.batch_effects[:, i]))) + # Store the unique values for each batch effect + self.all_idx.append(np.int16(np.unique(self.batch_effects[:, i]))) + # Make a cartesian product of all the unique values of each batch effect + self.be_idx = list(product(*self.all_idx)) + + # Make tuples of batch effects ID's and indices of datapoints with that specific combination of batch effects + self.be_idx_tups = [] + for be in self.be_idx: + a = [] + for i, b in enumerate(be): + a.append(self.batch_effects[:, i] == b) + idx = reduce(np.logical_and, a).nonzero() + if idx[0].shape[0] != 0: + self.be_idx_tups.append((be, idx)) + + def make_param(self, name, dim = (1,), **kwargs): + if self.configs.get(f'linear_{name}', False): + # First make a slope and intercept, and use those to make a linear parameterization + slope_parameterization = self.make_param(f'slope_{name}', dim=[self.feature_num], **kwargs) + intercept_parameterization = self.make_param(f'intercept_{name}', **kwargs) + return LinearParameterization(name=name, dim=dim, + slope_parameterization=slope_parameterization, intercept_parameterization=intercept_parameterization, + pb=self, + **kwargs) + + elif self.configs.get(f'random_{name}', False): + if self.configs.get(f'centered_{name}', True): + return CentralRandomFixedParameterization(name=name, pb=self, dim=dim, **kwargs) + else: + return NonCentralRandomFixedParameterization(name=name, pb=self, dim=dim, **kwargs) + else: + return FixedParameterization(name=name, dim=dim, pb=self,**kwargs) + + +class Parameterization: + """ + This is the top-level parameterization class from which all the other parameterizations inherit. + """ + def __init__(self, name, dim): + self.name = name + self.dim = dim + print(name, type(self)) + + def get_samples(self, pb): + + with pb.model: + samples = theano.tensor.zeros([pb.n_ys, *self.dim]) + for be, idx in pb.be_idx_tups: + samples = theano.tensor.set_subtensor(samples[idx], self.dist[be]) + return samples + + +class FixedParameterization(Parameterization): + """ + A parameterization that takes a single value for all input. It does not depend on anything except its hyperparameters + """ + def __init__(self, name, dim, pb:ParamBuilder, **kwargs): + super().__init__(name, dim) + dist = kwargs.get(f'{name}_dist','normal') + params = kwargs.get(f'{name}_params',(0.,1.)) + self.dist = Prior(name, dist, params, pb, shape = dim) + + +class CentralRandomFixedParameterization(Parameterization): + """ + A parameterization that is fixed for each batch effect. This is sampled in a central fashion; + the values are sampled from normal distribution with a group mean and group variance + """ + def __init__(self, name, dim, pb:ParamBuilder, **kwargs): + super().__init__(name, dim) + + # Normal distribution is default for mean + mu_dist = kwargs.get(f'mu_{name}_dist','normal') + mu_params = kwargs.get(f'mu_{name}_params',(0.,1.)) + mu_prior = Prior(f'mu_{name}', mu_dist, mu_params, pb, shape = dim) + + # HalfCauchy is default for sigma + sigma_dist = kwargs.get(f'sigma_{name}_dist','hcauchy') + sigma_params = kwargs.get(f'sigma_{name}_params',(1.,)) + sigma_prior = Prior(f'sigma_{name}',sigma_dist, sigma_params, pb, shape = [*pb.batch_effects_size, *dim]) + + self.dist = pm.Normal(name=name, mu=mu_prior.dist, sigma=sigma_prior.dist, shape = [*pb.batch_effects_size, *dim]) + + +class NonCentralRandomFixedParameterization(Parameterization): + """ + A parameterization that is fixed for each batch effect. This is sampled in a non-central fashion; + the values are a sum of a group mean and noise values scaled with a group scaling factor + """ + def __init__(self, name,dim, pb:ParamBuilder, **kwargs): + super().__init__(name, dim) + + # Normal distribution is default for mean + mu_dist = kwargs.get(f'mu_{name}_dist','normal') + mu_params = kwargs.get(f'mu_{name}_params',(0.,1.)) + mu_prior = Prior(f'mu_{name}', mu_dist, mu_params, pb, shape = dim) + + # HalfCauchy is default for sigma + sigma_dist = kwargs.get(f'sigma_{name}_dist','hcauchy') + sigma_params = kwargs.get(f'sigma_{name}_params',(1.,)) + sigma_prior = Prior(f'sigma_{name}',sigma_dist, sigma_params, pb, shape = dim) + + # Normal is default for offset + offset_dist = kwargs.get(f'offset_{name}_dist','normal') + offset_params = kwargs.get(f'offset_{name}_params',(0.,1.)) + offset_prior = Prior(f'offset_{name}',offset_dist, offset_params, pb, shape = [*pb.batch_effects_size, *dim]) + + self.dist = pm.Deterministic(name=name, var=mu_prior.dist+sigma_prior.dist*offset_prior.dist) + + +class LinearParameterization(Parameterization): + """ + A parameterization that can model a linear dependence on X. + """ + def __init__(self, name, dim, slope_parameterization, intercept_parameterization, pb, **kwargs): + super().__init__( name, dim) + self.slope_parameterization = slope_parameterization + self.intercept_parameterization = intercept_parameterization + + def get_samples(self, pb:ParamBuilder): + with pb.model: + samples = theano.tensor.zeros([pb.n_ys, *self.dim]) + for be, idx in pb.be_idx_tups: + dot = theano.tensor.dot(pb.X[idx,:], self.slope_parameterization.dist[be]).T + intercept = self.intercept_parameterization.dist[be] + samples = theano.tensor.set_subtensor(samples[idx,:],dot+intercept) + return samples + + +def get_design_matrix(X, nm, basis="linear"): + if basis == "bspline": + Phi = bspline_transform(X, nm.hbr.bsp) + elif basis == "polynomial": + Phi = create_poly_basis(X, 3) + else: + Phi = X + return Phi + + + +def nn_hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): + n_hidden = configs['nn_hidden_neuron_num'] + n_layers = configs['nn_hidden_layers_num'] + feature_num = X.shape[1] + batch_effects_num = batch_effects.shape[1] + all_idx = [] + for i in range(batch_effects_num): + all_idx.append(np.int16(np.unique(batch_effects[:, i]))) + be_idx = list(product(*all_idx)) + + # Initialize random weights between each layer for the mu: + init_1 = pm.floatX(np.random.randn(feature_num, n_hidden) * np.sqrt(1 / feature_num)) + init_out = pm.floatX(np.random.randn(n_hidden) * np.sqrt(1 / n_hidden)) + + std_init_1 = pm.floatX(np.random.rand(feature_num, n_hidden)) + std_init_out = pm.floatX(np.random.rand(n_hidden)) + + # And initialize random weights between each layer for sigma_noise: + init_1_noise = pm.floatX(np.random.randn(feature_num, n_hidden) * np.sqrt(1 / feature_num)) + init_out_noise = pm.floatX(np.random.randn(n_hidden) * np.sqrt(1 / n_hidden)) + + std_init_1_noise = pm.floatX(np.random.rand(feature_num, n_hidden)) + std_init_out_noise = pm.floatX(np.random.rand(n_hidden)) + + # If there are two hidden layers, then initialize weights for the second layer: + if n_layers == 2: + init_2 = pm.floatX(np.random.randn(n_hidden, n_hidden) * np.sqrt(1 / n_hidden)) + std_init_2 = pm.floatX(np.random.rand(n_hidden, n_hidden)) + init_2_noise = pm.floatX(np.random.randn(n_hidden, n_hidden) * np.sqrt(1 / n_hidden)) + std_init_2_noise = pm.floatX(np.random.rand(n_hidden, n_hidden)) + + with pm.Model() as model: + + X = pm.Data('X', X) + y = pm.Data('y', y) + + if trace is not None: # Used when estimating/predicting on a new site + weights_in_1_grp = from_posterior('w_in_1_grp', trace['w_in_1_grp'], + distribution='normal', freedom=configs['freedom']) + + weights_in_1_grp_sd = from_posterior('w_in_1_grp_sd', trace['w_in_1_grp_sd'], + distribution='hcauchy', freedom=configs['freedom']) + + if n_layers == 2: + weights_1_2_grp = from_posterior('w_1_2_grp', trace['w_1_2_grp'], + distribution='normal', freedom=configs['freedom']) + + weights_1_2_grp_sd = from_posterior('w_1_2_grp_sd', trace['w_1_2_grp_sd'], + distribution='hcauchy', freedom=configs['freedom']) + + weights_2_out_grp = from_posterior('w_2_out_grp', trace['w_2_out_grp'], + distribution='normal', freedom=configs['freedom']) + + weights_2_out_grp_sd = from_posterior('w_2_out_grp_sd', trace['w_2_out_grp_sd'], + distribution='hcauchy', freedom=configs['freedom']) + + mu_prior_intercept = from_posterior('mu_prior_intercept', trace['mu_prior_intercept'], + distribution='normal', freedom=configs['freedom']) + sigma_prior_intercept = from_posterior('sigma_prior_intercept', trace['sigma_prior_intercept'], + distribution='hcauchy', freedom=configs['freedom']) + + else: + # Group the mean distribution for input to the hidden layer: + weights_in_1_grp = pm.Normal('w_in_1_grp', 0, sd=1, + shape=(feature_num, n_hidden), testval=init_1) + + # Group standard deviation: + weights_in_1_grp_sd = pm.HalfCauchy('w_in_1_grp_sd', 1., + shape=(feature_num, n_hidden), testval=std_init_1) + + if n_layers == 2: + # Group the mean distribution for hidden layer 1 to hidden layer 2: + weights_1_2_grp = pm.Normal('w_1_2_grp', 0, sd=1, + shape=(n_hidden, n_hidden), testval=init_2) + + # Group standard deviation: + weights_1_2_grp_sd = pm.HalfCauchy('w_1_2_grp_sd', 1., + shape=(n_hidden, n_hidden), testval=std_init_2) + + # Group the mean distribution for hidden to output: + weights_2_out_grp = pm.Normal('w_2_out_grp', 0, sd=1, + shape=(n_hidden,), testval=init_out) + + # Group standard deviation: + weights_2_out_grp_sd = pm.HalfCauchy('w_2_out_grp_sd', 1., + shape=(n_hidden,), testval=std_init_out) + + # mu_prior_intercept = pm.Uniform('mu_prior_intercept', lower=-100, upper=100) + mu_prior_intercept = pm.Normal('mu_prior_intercept', mu=0., sigma=1e3) + sigma_prior_intercept = pm.HalfCauchy('sigma_prior_intercept', 5) + + # Now create separate weights for each group, by doing + # weights * group_sd + group_mean, we make sure the new weights are + # coming from the (group_mean, group_sd) distribution. + weights_in_1_raw = pm.Normal('w_in_1', 0, sd=1, + shape=(batch_effects_size + [feature_num, n_hidden])) + weights_in_1 = weights_in_1_raw * weights_in_1_grp_sd + weights_in_1_grp + + if n_layers == 2: + weights_1_2_raw = pm.Normal('w_1_2', 0, sd=1, + shape=(batch_effects_size + [n_hidden, n_hidden])) + weights_1_2 = weights_1_2_raw * weights_1_2_grp_sd + weights_1_2_grp + + weights_2_out_raw = pm.Normal('w_2_out', 0, sd=1, + shape=(batch_effects_size + [n_hidden])) + weights_2_out = weights_2_out_raw * weights_2_out_grp_sd + weights_2_out_grp + + intercepts_offset = pm.Normal('intercepts_offset', mu=0, sd=1, + shape=(batch_effects_size)) + + intercepts = pm.Deterministic('intercepts', intercepts_offset + + mu_prior_intercept * sigma_prior_intercept) + + # Build the neural network and estimate y_hat: + y_hat = theano.tensor.zeros(y.shape) + for be in be_idx: + # Find the indices corresponding to 'group be': + a = [] + for i, b in enumerate(be): + a.append(batch_effects[:, i] == b) + idx = reduce(np.logical_and, a).nonzero() + if idx[0].shape[0] != 0: + act_1 = pm.math.tanh(theano.tensor.dot(X[idx, :], weights_in_1[be])) + if n_layers == 2: + act_2 = pm.math.tanh(theano.tensor.dot(act_1, weights_1_2[be])) + y_hat = theano.tensor.set_subtensor(y_hat[idx, 0], + intercepts[be] + theano.tensor.dot(act_2, weights_2_out[be])) + else: + y_hat = theano.tensor.set_subtensor(y_hat[idx, 0], + intercepts[be] + theano.tensor.dot(act_1, weights_2_out[be])) + + # If we want to estimate varying noise terms across groups: + if configs['random_noise']: + if configs['hetero_noise']: + if trace is not None: # # Used when estimating/predicting on a new site + weights_in_1_grp_noise = from_posterior('w_in_1_grp_noise', + trace['w_in_1_grp_noise'], + distribution='normal', freedom=configs['freedom']) + + weights_in_1_grp_sd_noise = from_posterior('w_in_1_grp_sd_noise', + trace['w_in_1_grp_sd_noise'], + distribution='hcauchy', freedom=configs['freedom']) + + if n_layers == 2: + weights_1_2_grp_noise = from_posterior('w_1_2_grp_noise', + trace['w_1_2_grp_noise'], + distribution='normal', freedom=configs['freedom']) + + weights_1_2_grp_sd_noise = from_posterior('w_1_2_grp_sd_noise', + trace['w_1_2_grp_sd_noise'], + distribution='hcauchy', freedom=configs['freedom']) + + weights_2_out_grp_noise = from_posterior('w_2_out_grp_noise', + trace['w_2_out_grp_noise'], + distribution='normal', freedom=configs['freedom']) + + weights_2_out_grp_sd_noise = from_posterior('w_2_out_grp_sd_noise', + trace['w_2_out_grp_sd_noise'], + distribution='hcauchy', freedom=configs['freedom']) + + else: + # The input layer to the first hidden layer: + weights_in_1_grp_noise = pm.Normal('w_in_1_grp_noise', 0, sd=1, + shape=(feature_num, n_hidden), + testval=init_1_noise) + weights_in_1_grp_sd_noise = pm.HalfCauchy('w_in_1_grp_sd_noise', 1, + shape=(feature_num, n_hidden), + testval=std_init_1_noise) + + # The first hidden layer to second hidden layer: + if n_layers == 2: + weights_1_2_grp_noise = pm.Normal('w_1_2_grp_noise', 0, sd=1, + shape=(n_hidden, n_hidden), + testval=init_2_noise) + weights_1_2_grp_sd_noise = pm.HalfCauchy('w_1_2_grp_sd_noise', 1, + shape=(n_hidden, n_hidden), + testval=std_init_2_noise) + + # The second hidden layer to output layer: + weights_2_out_grp_noise = pm.Normal('w_2_out_grp_noise', 0, sd=1, + shape=(n_hidden,), + testval=init_out_noise) + weights_2_out_grp_sd_noise = pm.HalfCauchy('w_2_out_grp_sd_noise', 1, + shape=(n_hidden,), + testval=std_init_out_noise) + + # mu_prior_intercept_noise = pm.HalfNormal('mu_prior_intercept_noise', sigma=1e3) + # sigma_prior_intercept_noise = pm.HalfCauchy('sigma_prior_intercept_noise', 5) + + # Now create separate weights for each group: + weights_in_1_raw_noise = pm.Normal('w_in_1_noise', 0, sd=1, + shape=(batch_effects_size + [feature_num, n_hidden])) + weights_in_1_noise = weights_in_1_raw_noise * weights_in_1_grp_sd_noise + weights_in_1_grp_noise + + if n_layers == 2: + weights_1_2_raw_noise = pm.Normal('w_1_2_noise', 0, sd=1, + shape=(batch_effects_size + [n_hidden, n_hidden])) + weights_1_2_noise = weights_1_2_raw_noise * weights_1_2_grp_sd_noise + weights_1_2_grp_noise + + weights_2_out_raw_noise = pm.Normal('w_2_out_noise', 0, sd=1, + shape=(batch_effects_size + [n_hidden])) + weights_2_out_noise = weights_2_out_raw_noise * weights_2_out_grp_sd_noise + weights_2_out_grp_noise + + # intercepts_offset_noise = pm.Normal('intercepts_offset_noise', mu=0, sd=1, + # shape=(batch_effects_size)) + + # intercepts_noise = pm.Deterministic('intercepts_noise', mu_prior_intercept_noise + + # intercepts_offset_noise * sigma_prior_intercept_noise) + + # Build the neural network and estimate the sigma_y: + sigma_y = theano.tensor.zeros(y.shape) + for be in be_idx: + a = [] + for i, b in enumerate(be): + a.append(batch_effects[:, i] == b) + idx = reduce(np.logical_and, a).nonzero() + if idx[0].shape[0] != 0: + act_1_noise = pm.math.sigmoid(theano.tensor.dot(X[idx, :], weights_in_1_noise[be])) + if n_layers == 2: + act_2_noise = pm.math.sigmoid(theano.tensor.dot(act_1_noise, weights_1_2_noise[be])) + temp = pm.math.log1pexp(theano.tensor.dot(act_2_noise, weights_2_out_noise[be])) + 1e-5 + else: + temp = pm.math.log1pexp(theano.tensor.dot(act_1_noise, weights_2_out_noise[be])) + 1e-5 + sigma_y = theano.tensor.set_subtensor(sigma_y[idx, 0], temp) + + else: # homoscedastic noise: + if trace is not None: # Used for transferring the priors + upper_bound = np.percentile(trace['sigma_noise'], 95) + sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=2 * upper_bound, shape=(batch_effects_size)) + else: + sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=100, shape=(batch_effects_size)) + + sigma_y = theano.tensor.zeros(y.shape) + for be in be_idx: + a = [] + for i, b in enumerate(be): + a.append(batch_effects[:, i] == b) + idx = reduce(np.logical_and, a).nonzero() + if idx[0].shape[0] != 0: + sigma_y = theano.tensor.set_subtensor(sigma_y[idx, 0], sigma_noise[be]) + + else: # do not allow for random noise terms across groups: + if trace is not None: # Used for transferring the priors + upper_bound = np.percentile(trace['sigma_noise'], 95) + sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=2 * upper_bound) + else: + sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=100) + sigma_y = theano.tensor.zeros(y.shape) + for be in be_idx: + a = [] + for i, b in enumerate(be): + a.append(batch_effects[:, i] == b) + idx = reduce(np.logical_and, a).nonzero() + if idx[0].shape[0] != 0: + sigma_y = theano.tensor.set_subtensor(sigma_y[idx, 0], sigma_noise) + + if configs['skewed_likelihood']: + skewness = pm.Uniform('skewness', lower=-10, upper=10, shape=(batch_effects_size)) + alpha = theano.tensor.zeros(y.shape) + for be in be_idx: + a = [] + for i, b in enumerate(be): + a.append(batch_effects[:, i] == b) + idx = reduce(np.logical_and, a).nonzero() + if idx[0].shape[0] != 0: + alpha = theano.tensor.set_subtensor(alpha[idx, 0], skewness[be]) + else: + alpha = 0 # symmetrical normal distribution + + y_like = pm.SkewNormal('y_like', mu=y_hat, sigma=sigma_y, alpha=alpha, observed=y) + + return model diff --git a/build/lib/pcntoolkit/model/rfa.py b/build/lib/pcntoolkit/model/rfa.py new file mode 100644 index 00000000..7e67169b --- /dev/null +++ b/build/lib/pcntoolkit/model/rfa.py @@ -0,0 +1,243 @@ +from __future__ import print_function +from __future__ import division + +import numpy as np +import torch + +class GPRRFA: + """Random Feature Approximation for Gaussian Process Regression + + Estimation and prediction of Bayesian linear regression models + + Basic usage:: + + R = GPRRFA() + hyp = R.estimate(hyp0, X, y) + ys,s2 = R.predict(hyp, X, y, Xs) + + where the variables are + + :param hyp: vector of hyperparmaters. + :param X: N x D data array + :param y: 1D Array of targets (length N) + :param Xs: Nte x D array of test cases + :param hyp0: starting estimates for hyperparameter optimisation + + :returns: * ys - predictive mean + * s2 - predictive variance + + The hyperparameters are:: + + hyp = [ log(sn), log(ell), log(sf) ] # hyp is a numpy array + + where sn^2 is the noise variance, ell are lengthscale parameters and + sf^2 is the signal variance. This provides an approximation to the + covariance function:: + + k(x,z) = x'*z + sn2*exp(0.5*(x-z)'*Lambda*(x-z)) + + where Lambda = diag((ell_1^2, ... ell_D^2)) + + Written by A. Marquand + """ + + def __init__(self, hyp=None, X=None, y=None, n_feat=None, + n_iter=100, tol=1e-3, verbose=False): + + self.hyp = np.nan + self.nlZ = np.nan + self.tol = tol # not used at present + self.Nf = n_feat + self.n_iter = n_iter + self.verbose = verbose + self._n_restarts = 5 + + if (hyp is not None) and (X is not None) and (y is not None): + self.post(hyp, X, y) + + def _numpy2torch(self, X, y=None, hyp=None): + + if type(X) is torch.Tensor: + pass + elif type(X) is np.ndarray: + X = torch.from_numpy(X) + else: + raise(ValueError, 'Unknown data type (X)') + X = X.double() + + if y is not None: + if type(y) is torch.Tensor: + pass + elif type(y) is np.ndarray: + y = torch.from_numpy(y) + else: + raise(ValueError, 'Unknown data type (y)') + + if len(y.shape) == 1: + y.resize_(y.shape[0],1) + y = y.double() + + if hyp is not None: + if type(hyp) is torch.Tensor: + pass + else: + hyp = torch.tensor(hyp, requires_grad=True) + + return X, y, hyp + + def get_n_params(self, X): + + return X.shape[1] + 2 + + def post(self, hyp, X, y): + """ Generic function to compute posterior distribution. + + This function will save the posterior mean and precision matrix as + self.m and self.A and will also update internal parameters (e.g. + N, D and the prior covariance (Sigma) and precision (iSigma). + """ + + # make sure all variables are the right type + X, y, hyp = self._numpy2torch(X, y, hyp) + + self.N, self.Dx = X.shape + + # ensure the number of features is specified (use 75% as a default) + if self.Nf is None: + self.Nf = int(0.75 * self.N) + + self.Omega = torch.zeros((self.Dx, self.Nf), dtype=torch.double) + for f in range(self.Nf): + self.Omega[:,f] = torch.exp(hyp[1:-1]) * \ + torch.randn((self.Dx, 1), dtype=torch.double).squeeze() + + XO = torch.mm(X, self.Omega) + self.Phi = torch.exp(hyp[-1])/np.sqrt(self.Nf) * \ + torch.cat((torch.cos(XO), torch.sin(XO)), 1) + + # concatenate linear weights + self.Phi = torch.cat((self.Phi, X), 1) + self.D = self.Phi.shape[1] + + if self.verbose: + print("estimating posterior ... | hyp=", hyp) + + self.A = torch.mm(torch.t(self.Phi), self.Phi) / torch.exp(2*hyp[0]) + \ + torch.eye(self.D, dtype=torch.double) + self.m = torch.mm(torch.solve(torch.t(self.Phi), self.A)[0], y) / \ + torch.exp(2*hyp[0]) + + # save hyperparameters + self.hyp = hyp + + # update optimizer iteration count + if hasattr(self,'_iterations'): + self._iterations += 1 + + def loglik(self, hyp, X, y): + """ Function to compute compute log (marginal) likelihood """ + X, y, hyp = self._numpy2torch(X, y, hyp) + + # always recompute the posterior + self.post(hyp, X, y) + + #logdetA = 2*torch.sum(torch.log(torch.diag(torch.cholesky(self.A)))) + try: + # compute the log determinants in a numerically stable way + logdetA = 2*torch.sum(torch.log(torch.diag(torch.cholesky(self.A)))) + except Exception as e: + print("Warning: Estimation of posterior distribution failed") + print(e) + #nlZ = torch.tensor(1/np.finfo(float).eps) + nlZ = torch.tensor(np.nan) + self._optim_failed = True + return nlZ + + # compute negative marginal log likelihood + nlZ = -0.5 * (self.N*torch.log(1/torch.exp(2*hyp[0])) - + self.N*np.log(2*np.pi) - + torch.mm(torch.t(y - torch.mm(self.Phi,self.m)), + (y - torch.mm(self.Phi,self.m))) / + torch.exp(2*hyp[0]) - + torch.mm(torch.t(self.m), self.m) - logdetA) + + if self.verbose: + print("nlZ= ", nlZ, " | hyp=", hyp) + + # save marginal likelihood + self.nlZ = nlZ + return nlZ + + def dloglik(self, hyp, X, y): + """ Function to compute derivatives """ + + print("derivatives not available") + + return + + def estimate(self, hyp0, X, y, optimizer='lbfgs'): + """ Function to estimate the model """ + + if type(hyp0) is torch.Tensor: + hyp = hyp0 + hyp0.requires_grad_() + else: + hyp = torch.tensor(hyp0, requires_grad=True) + # save the starting values + self.hyp0 = hyp + + if optimizer.lower() == 'lbfgs': + opt = torch.optim.LBFGS([hyp]) + else: + raise(ValueError, "Optimizer " + " not implemented") + self._iterations = 0 + + def closure(): + opt.zero_grad() + nlZ = self.loglik(hyp, X, y) + if not torch.isnan(nlZ): + nlZ.backward() + return nlZ + + for r in range(self._n_restarts): + self._optim_failed = False + + nlZ = opt.step(closure) + + if self._optim_failed: + print("optimization failed. retrying (", r+1, "of", + self._n_restarts,")") + hyp = torch.randn_like(hyp, requires_grad=True) + self.hyp0 = hyp + else: + print("Optimzation complete after", self._iterations, + "evaluations. Function value =", + nlZ.detach().numpy().squeeze()) + break + + return self.hyp.detach().numpy() + + def predict(self, hyp, X, y, Xs): + """ Function to make predictions from the model """ + + X, y, hyp = self._numpy2torch(X, y, hyp) + Xs, *_ = self._numpy2torch(Xs) + + if (hyp != self.hyp).all() or not(hasattr(self, 'A')): + self.post(hyp, X, y) + + # generate prediction tensors + XsO = torch.mm(Xs, self.Omega) + Phis = torch.exp(hyp[-1])/np.sqrt(self.Nf) * \ + torch.cat((torch.cos(XsO), torch.sin(XsO)), 1) + # add linear component + Phis = torch.cat((Phis, Xs), 1) + + ys = torch.mm(Phis, self.m) + + # compute diag(Phis*(Phis'\A)) avoiding computing off-diagonal entries + s2 = torch.exp(2*hyp[0]) + \ + torch.sum(Phis * torch.t(torch.solve(torch.t(Phis), self.A)[0]), 1) + + # return output as numpy arrays + return ys.detach().numpy().squeeze(), s2.detach().numpy().squeeze() diff --git a/build/lib/pcntoolkit/normative.py b/build/lib/pcntoolkit/normative.py new file mode 100644 index 00000000..bfd50553 --- /dev/null +++ b/build/lib/pcntoolkit/normative.py @@ -0,0 +1,1434 @@ +#!/Users/andre/sfw/anaconda3/bin/python + +# ------------------------------------------------------------------------------ +# Usage: +# python normative.py -m [maskfile] -k [number of CV folds] -c +# -t [test covariates] -r [test responses] +# +# Either the -k switch or -t switch should be specified, but not both. +# If -t is selected, a set of responses should be provided with the -r switch +# +# Written by A. Marquand +# ------------------------------------------------------------------------------ + +from __future__ import print_function +from __future__ import division + +import os +import sys +import numpy as np +import argparse +import pickle +import glob + +from sklearn.model_selection import KFold +from pathlib import Path + +try: # run as a package if installed + from pcntoolkit import configs + from pcntoolkit.dataio import fileio + 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 +except ImportError: + pass + + path = os.path.abspath(os.path.dirname(__file__)) + if path not in sys.path: + sys.path.append(path) + #sys.path.append(os.path.join(path,'normative_model')) + del path + + 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 + +PICKLE_PROTOCOL = configs.PICKLE_PROTOCOL + +def load_response_vars(datafile, maskfile=None, vol=True): + """ load response variables (of any data type)""" + + if fileio.file_type(datafile) == 'nifti': + dat = fileio.load_nifti(datafile, vol=vol) + volmask = fileio.create_mask(dat, mask=maskfile) + Y = fileio.vol2vec(dat, volmask).T + else: + Y = fileio.load(datafile) + volmask = None + if fileio.file_type(datafile) == 'cifti': + Y = Y.T + + return Y, volmask + + +def get_args(*args): + """ Parse command line arguments""" + + # 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("-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("-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() + + # Process required arguemnts + wdir = os.path.realpath(os.path.curdir) + respfile = os.path.join(wdir, args.responses) + if args.covfile is None: + raise(ValueError, "No covariates specified") + else: + covfile = args.covfile + + # Process optional arguments + if 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: + testcov = None + testresp = None + cvfolds = int(args.cvfolds) + print("Running under " + str(cvfolds) + " fold cross-validation.") + else: + print("Test covariates specified") + testcov = args.testcov + cvfolds = None + if args.testresp is None: + testresp = None + print("No test response variables specified") + else: + testresp = args.testresp + if 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: + 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 + + +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 + and a set of true response variables Y, namely: + + * Rho: Pearson correlation + * RMSE: root mean squared error + * SMSE: standardized mean squared error + * EXPV: explained variance + + If the predictive variance is also specified the log loss will be computed + (which also takes into account the predictive variance). If the mean and + standard deviation are also specified these will be used to standardize + this, yielding the mean standardized log loss + + :param Y: N x P array of true response variables + :param Yhat: N x P array of predicted response variables + :param S2: predictive variance + :param mY: mean of the training set + :param sY: standard deviation of the training set + + :returns metrics: evaluation metrics + + ''' + + feature_num = Y.shape[1] + + # Remove metrics that cannot be computed with only a single data point + if Y.shape[0] == 1: + if 'MSLL' in metrics: + metrics.remove('MSLL') + if 'SMSE' in metrics: + metrics.remove('SMSE') + + # find and remove bad variables from the response variables + nz = np.where(np.bitwise_and(np.isfinite(Y).any(axis=0), + np.var(Y, axis=0) != 0))[0] + + MSE = np.mean((Y - Yhat)**2, axis=0) + + results = dict() + + if 'RMSE' in metrics: + RMSE = np.sqrt(MSE) + results['RMSE'] = RMSE + + if 'Rho' in metrics: + Rho = np.zeros(feature_num) + pRho = np.ones(feature_num) + Rho[nz], pRho[nz] = compute_pearsonr(Y[:,nz], Yhat[:,nz]) + results['Rho'] = Rho + results['pRho'] = pRho + + if 'SMSE' in metrics: + SMSE = np.zeros_like(MSE) + SMSE[nz] = MSE[nz] / np.var(Y[:,nz], axis=0) + results['SMSE'] = SMSE + + if 'EXPV' in metrics: + EXPV = np.zeros(feature_num) + EXPV[nz] = explained_var(Y[:,nz], Yhat[:,nz]) + results['EXPV'] = EXPV + + if 'MSLL' in metrics: + if ((S2 is not None) and (mY is not None) and (sY is not None)): + MSLL = np.zeros(feature_num) + MSLL[nz] = compute_MSLL(Y[:,nz], Yhat[:,nz], S2[:,nz], + mY.reshape(-1,1).T, + (sY**2).reshape(-1,1).T) + results['MSLL'] = MSLL + + if 'NLL' in metrics: + results['NLL'] = nlZ + + if 'BIC' in metrics: + if hasattr(getattr(nm, alg), 'hyp'): + n = Xz_tr.shape[0] + k = len(getattr(nm, alg).hyp) + BIC = k * np.log(n) + 2 * nlZ + results['BIC'] = BIC + + return results + +def save_results(respfile, Yhat, S2, maskvol, Z=None, outputsuffix=None, + results=None, save_path=''): + + print("Writing outputs ...") + if respfile is None: + exfile = None + file_ext = '.pkl' + else: + if fileio.file_type(respfile) == 'cifti' or \ + fileio.file_type(respfile) == 'nifti': + exfile = respfile + else: + exfile = None + file_ext = fileio.file_extension(respfile) + + if outputsuffix is not None: + ext = str(outputsuffix) + file_ext + else: + ext = file_ext + + fileio.save(Yhat, os.path.join(save_path, 'yhat' + ext), example=exfile, + mask=maskvol) + fileio.save(S2, os.path.join(save_path, 'ys2' + ext), example=exfile, + mask=maskvol) + if Z is not None: + fileio.save(Z, os.path.join(save_path, 'Z' + ext), example=exfile, + mask=maskvol) + + if results is not None: + for metric in list(results.keys()): + if (metric == 'NLL' or metric == 'BIC') and file_ext == '.nii.gz': + fileio.save(results[metric], os.path.join(save_path, metric + str(outputsuffix) + '.pkl'), + example=exfile, mask=maskvol) + else: + fileio.save(results[metric], os.path.join(save_path, metric + ext), + example=exfile, mask=maskvol) + +def estimate(covfile, respfile, **kwargs): + """ Estimate a normative model + + This will estimate a model in one of two settings according to + theparticular parameters specified (see below) + + * under k-fold cross-validation. + requires respfile, covfile and cvfolds>=2 + * estimating a training dataset then applying to a second test dataset. + requires respfile, covfile, testcov and testresp. + * estimating on a training dataset ouput of forward maps mean and se. + requires respfile, covfile and testcov + + The models are estimated on the basis of data stored on disk in ascii or + neuroimaging data formats (nifti or cifti). Ascii data should be in + tab or space delimited format with the number of subjects in rows and the + number of variables in columns. Neuroimaging data will be reshaped + into the appropriate format + + Basic usage:: + + estimate(covfile, respfile, [extra_arguments]) + + where the variables are defined below. Note that either the cfolds + parameter or (testcov, testresp) should be specified, but not both. + + :param respfile: response variables for the normative model + :param covfile: covariates used to predict the response variable + :param maskfile: mask used to apply to the data (nifti only) + :param cvfolds: Number of cross-validation folds + :param testcov: Test covariates + :param testresp: Test responses + :param alg: Algorithm for normative model + :param configparam: Parameters controlling the estimation algorithm + :param saveoutput: Save the output to disk? Otherwise returned as arrays + :param outputsuffix: Text string to add to the output filenames + :param inscale: Scaling approach for input covariates, could be 'None' (Default), + 'standardize', 'minmax', or 'robminmax'. + :param outscale: Scaling approach for output responses, could be 'None' (Default), + 'standardize', 'minmax', or 'robminmax'. + + All outputs are written to disk in the same format as the input. These are: + + :outputs: * 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 + + The outputsuffix may be useful to estimate multiple normative models in the + same directory (e.g. for custom cross-validation schemes) + """ + + # parse keyword arguments + maskfile = kwargs.pop('maskfile',None) + cvfolds = kwargs.pop('cvfolds', None) + testcov = kwargs.pop('testcov', None) + testresp = kwargs.pop('testresp',None) + alg = kwargs.pop('alg','gpr') + outputsuffix = kwargs.pop('outputsuffix','estimate') + outputsuffix = "_" + outputsuffix.replace("_", "") # Making sure there is only one + # '_' is in the outputsuffix to + # avoid file name parsing problem. + inscaler = kwargs.pop('inscaler','None') + outscaler = kwargs.pop('outscaler','None') + warp = kwargs.get('warp', None) + + # convert from strings if necessary + saveoutput = kwargs.pop('saveoutput','True') + if type(saveoutput) is str: + saveoutput = saveoutput=='True' + savemodel = kwargs.pop('savemodel','False') + if type(savemodel) is str: + savemodel = savemodel=='True' + + if savemodel and not os.path.isdir('Models'): + os.mkdir('Models') + + # which output metrics to compute + metrics = ['Rho', 'RMSE', 'SMSE', 'EXPV', 'MSLL','NLL', 'BIC'] + + # load data + print("Processing data in " + respfile) + X = fileio.load(covfile) + Y, maskvol = load_response_vars(respfile, maskfile) + if len(Y.shape) == 1: + Y = Y[:, np.newaxis] + if len(X.shape) == 1: + X = X[:, np.newaxis] + Nmod = Y.shape[1] + + if (testcov is not None) and (cvfolds is None): # a separate test dataset + + run_cv = False + cvfolds = 1 + Xte = fileio.load(testcov) + if len(Xte.shape) == 1: + Xte = Xte[:, np.newaxis] + if testresp is not None: + Yte, testmask = load_response_vars(testresp, maskfile) + if len(Yte.shape) == 1: + Yte = Yte[:, np.newaxis] + else: + sub_te = Xte.shape[0] + Yte = np.zeros([sub_te, Nmod]) + + # treat as a single train-test split + testids = range(X.shape[0], X.shape[0]+Xte.shape[0]) + splits = CustomCV((range(0, X.shape[0]),), (testids,)) + + Y = np.concatenate((Y, Yte), axis=0) + X = np.concatenate((X, Xte), axis=0) + + else: + run_cv = True + # we are running under cross-validation + splits = KFold(n_splits=cvfolds, shuffle=True) + testids = range(0, X.shape[0]) + if alg=='hbr': + trbefile = kwargs.get('trbefile', None) + if trbefile is not None: + be = fileio.load(trbefile) + if len(be.shape) == 1: + be = be[:, np.newaxis] + else: + print('No batch-effects file! Initilizing all as zeros!') + be = np.zeros([X.shape[0],1]) + + # find and remove bad variables from the response variables + # note: the covariates are assumed to have already been checked + nz = np.where(np.bitwise_and(np.isfinite(Y).any(axis=0), + np.var(Y, axis=0) != 0))[0] + + # run cross-validation loop + Yhat = np.zeros_like(Y) + S2 = np.zeros_like(Y) + Z = np.zeros_like(Y) + nlZ = np.zeros((Nmod, cvfolds)) + + scaler_resp = [] + scaler_cov = [] + mean_resp = [] # this is just for computing MSLL + std_resp = [] # this is just for computing MSLL + + if warp is not None: + Ywarp = np.zeros_like(Yhat) + + # for warping we need to compute metrics separately for each fold + results_folds = dict() + for m in metrics: + results_folds[m]= np.zeros((Nmod, cvfolds)) + + for idx in enumerate(splits.split(X)): + + fold = idx[0] + tr = idx[1][0] + ts = idx[1][1] + + # standardize responses and covariates, ignoring invalid entries + iy_tr, jy_tr = np.ix_(tr, nz) + iy_ts, jy_ts = np.ix_(ts, nz) + mY = np.mean(Y[iy_tr, jy_tr], axis=0) + sY = np.std(Y[iy_tr, jy_tr], axis=0) + mean_resp.append(mY) + std_resp.append(sY) + + if inscaler in ['standardize', 'minmax', 'robminmax']: + X_scaler = scaler(inscaler) + Xz_tr = X_scaler.fit_transform(X[tr, :]) + Xz_ts = X_scaler.transform(X[ts, :]) + scaler_cov.append(X_scaler) + else: + Xz_tr = X[tr, :] + Xz_ts = X[ts, :] + + if outscaler in ['standardize', 'minmax', 'robminmax']: + Y_scaler = scaler(outscaler) + Yz_tr = Y_scaler.fit_transform(Y[iy_tr, jy_tr]) + scaler_resp.append(Y_scaler) + else: + Yz_tr = Y[iy_tr, jy_tr] + + if (run_cv==True and alg=='hbr'): + fileio.save(be[tr,:], 'be_kfold_tr_tempfile.pkl') + fileio.save(be[ts,:], 'be_kfold_ts_tempfile.pkl') + kwargs['trbefile'] = 'be_kfold_tr_tempfile.pkl' + kwargs['tsbefile'] = 'be_kfold_ts_tempfile.pkl' + + # estimate the models for all subjects + for i in range(0, len(nz)): + print("Estimating model ", i+1, "of", len(nz)) + nm = norm_init(Xz_tr, Yz_tr[:, i], alg=alg, **kwargs) + + try: + nm = nm.estimate(Xz_tr, Yz_tr[:, i], **kwargs) + yhat, s2 = nm.predict(Xz_ts, Xz_tr, Yz_tr[:, i], **kwargs) + + if savemodel: + nm.save('Models/NM_' + str(fold) + '_' + str(nz[i]) + + outputsuffix + '.pkl' ) + + if outscaler == 'standardize': + Yhat[ts, nz[i]] = Y_scaler.inverse_transform(yhat, index=i) + S2[ts, nz[i]] = s2 * sY[i]**2 + elif outscaler in ['minmax', 'robminmax']: + Yhat[ts, nz[i]] = Y_scaler.inverse_transform(yhat, index=i) + S2[ts, nz[i]] = s2 * (Y_scaler.max[i] - Y_scaler.min[i])**2 + else: + Yhat[ts, nz[i]] = yhat + S2[ts, nz[i]] = s2 + + nlZ[nz[i], fold] = nm.neg_log_lik + + if (run_cv or testresp is not None): + if warp is not None: + # TODO: Warping for scaled data + if outscaler is not None and outscaler != 'None': + raise(ValueError, "outscaler not yet supported warping") + warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] + Ywarp[ts, nz[i]] = nm.blr.warp.f(Y[ts, nz[i]], warp_param) + Ytest = Ywarp[ts, nz[i]] + + # Save warped mean of the training data (for MSLL) + yw = nm.blr.warp.f(Y[tr, nz[i]], warp_param) + + # create arrays for evaluation + Yhati = Yhat[ts, nz[i]] + Yhati = Yhati[:, np.newaxis] + S2i = S2[ts, nz[i]] + S2i = S2i[:, np.newaxis] + + # evaluate and save results + mf = evaluate(Ytest[:, np.newaxis], Yhati, S2=S2i, + mY=np.mean(yw), sY=np.std(yw), + nlZ=nm.neg_log_lik, nm=nm, Xz_tr=Xz_tr, + alg=alg, metrics = metrics) + for k in metrics: + results_folds[k][nz[i]][fold] = mf[k] + else: + Ytest = Y[ts, nz[i]] + + Z[ts, nz[i]] = (Ytest - Yhat[ts, nz[i]]) / \ + np.sqrt(S2[ts, nz[i]]) + + except Exception as e: + exc_type, exc_obj, exc_tb = sys.exc_info() + fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] + print("Model ", i+1, "of", len(nz), + "FAILED!..skipping and writing NaN to outputs") + print("Exception:") + print(e) + print(exc_type, fname, exc_tb.tb_lineno) + + Yhat[ts, nz[i]] = float('nan') + S2[ts, nz[i]] = float('nan') + nlZ[nz[i], fold] = float('nan') + if testcov is None: + Z[ts, nz[i]] = float('nan') + else: + if testresp is not None: + Z[ts, nz[i]] = float('nan') + + + if savemodel: + print('Saving model meta-data...') + v = get_package_versions() + with open('Models/meta_data.md', 'wb') as file: + pickle.dump({'valid_voxels':nz, 'fold_num':cvfolds, + 'mean_resp':mean_resp, 'std_resp':std_resp, + 'scaler_cov':scaler_cov, 'scaler_resp':scaler_resp, + 'regressor':alg, 'inscaler':inscaler, + 'outscaler':outscaler, 'versions':v}, + file, protocol=PICKLE_PROTOCOL) + + # compute performance metrics + if (run_cv or testresp is not None): + print("Evaluating the model ...") + if warp is None: + results = evaluate(Y[testids, :], Yhat[testids, :], + S2=S2[testids, :], mY=mean_resp[0], + sY=std_resp[0], nlZ=nlZ, nm=nm, Xz_tr=Xz_tr, alg=alg, + metrics = metrics) + else: + # for warped data we just aggregate across folds + results = dict() + for m in ['Rho', 'RMSE', 'SMSE', 'EXPV', 'MSLL']: + results[m] = np.mean(results_folds[m], axis=1) + results['NLL'] = results_folds['NLL'] + results['BIC'] = results_folds['BIC'] + + # Set writing options + if saveoutput: + if (run_cv or testresp is not None): + save_results(respfile, Yhat[testids, :], S2[testids, :], maskvol, + Z=Z[testids, :], results=results, + outputsuffix=outputsuffix) + + else: + save_results(respfile, Yhat[testids, :], S2[testids, :], maskvol, + outputsuffix=outputsuffix) + + else: + if (run_cv or testresp is not None): + output = (Yhat[testids, :], S2[testids, :], nm, Z[testids, :], + results) + else: + output = (Yhat[testids, :], S2[testids, :], nm) + + return output + + +def fit(covfile, respfile, **kwargs): + + # parse keyword arguments + maskfile = kwargs.pop('maskfile',None) + alg = kwargs.pop('alg','gpr') + savemodel = kwargs.pop('savemodel','True')=='True' + outputsuffix = kwargs.pop('outputsuffix','fit') + outputsuffix = "_" + outputsuffix.replace("_", "") + inscaler = kwargs.pop('inscaler','None') + outscaler = kwargs.pop('outscaler','None') + + if savemodel and not os.path.isdir('Models'): + os.mkdir('Models') + + # load data + print("Processing data in " + respfile) + X = fileio.load(covfile) + Y, maskvol = load_response_vars(respfile, maskfile) + if len(Y.shape) == 1: + Y = Y[:, np.newaxis] + if len(X.shape) == 1: + X = X[:, np.newaxis] + + # find and remove bad variables from the response variables + # note: the covariates are assumed to have already been checked + nz = np.where(np.bitwise_and(np.isfinite(Y).any(axis=0), + np.var(Y, axis=0) != 0))[0] + + scaler_resp = [] + scaler_cov = [] + mean_resp = [] # this is just for computing MSLL + std_resp = [] # this is just for computing MSLL + + # standardize responses and covariates, ignoring invalid entries + mY = np.mean(Y[:, nz], axis=0) + sY = np.std(Y[:, nz], axis=0) + mean_resp.append(mY) + std_resp.append(sY) + + if inscaler in ['standardize', 'minmax', 'robminmax']: + X_scaler = scaler(inscaler) + Xz = X_scaler.fit_transform(X) + scaler_cov.append(X_scaler) + else: + Xz = X + + if outscaler in ['standardize', 'minmax', 'robminmax']: + Yz = np.zeros_like(Y) + Y_scaler = scaler(outscaler) + Yz[:, nz] = Y_scaler.fit_transform(Y[:, nz]) + scaler_resp.append(Y_scaler) + else: + Yz = Y + + # estimate the models for all subjects + for i in range(0, len(nz)): + print("Estimating model ", i+1, "of", len(nz)) + nm = norm_init(Xz, Yz[:, nz[i]], alg=alg, **kwargs) + nm = nm.estimate(Xz, Yz[:, nz[i]], **kwargs) + + if savemodel: + nm.save('Models/NM_' + str(0) + '_' + str(nz[i]) + outputsuffix + + '.pkl' ) + + if savemodel: + print('Saving model meta-data...') + v = get_package_versions() + with open('Models/meta_data.md', 'wb') as file: + pickle.dump({'valid_voxels':nz, + 'mean_resp':mean_resp, 'std_resp':std_resp, + 'scaler_cov':scaler_cov, 'scaler_resp':scaler_resp, + 'regressor':alg, 'inscaler':inscaler, + 'outscaler':outscaler, 'versions':v}, + file, protocol=PICKLE_PROTOCOL) + + return nm + + +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 + will be returned. If the test responses are also specified then quantities + That depend on those will also be returned (Z scores and error metrics) + + Basic usage:: + + predict(covfile, [extra_arguments]) + + where the variables are defined below. + + :param covfile: test covariates used to predict the response variable + :param respfile: test response variables for the normative model + :param maskfile: mask used to apply to the data (nifti only) + :param model_path: Directory containing the normative model and metadata. + When using parallel prediction, do not pass the model path. It will be + automatically decided. + :param outputsuffix: Text string to add to the output filenames + :param batch_size: batch size (for use with normative_parallel) + :param job_id: batch id + :param fold: which cross-validation fold to use (default = 0) + :param fold: list of model IDs to predict (if not specified all are computed) + + All outputs are written to disk in the same format as the input. These are: + + :outputs: * Yhat - predictive mean + * S2 - predictive variance + * Z - Z scores + ''' + + + model_path = kwargs.pop('model_path', 'Models') + job_id = kwargs.pop('job_id', None) + batch_size = kwargs.pop('batch_size', None) + outputsuffix = kwargs.pop('outputsuffix', 'predict') + outputsuffix = "_" + outputsuffix.replace("_", "") + inputsuffix = kwargs.pop('inputsuffix', 'estimate') + inputsuffix = "_" + inputsuffix.replace("_", "") + alg = kwargs.pop('alg') + fold = kwargs.pop('fold',0) + models = kwargs.pop('models', None) + + if alg == 'gpr': + raise(ValueError, "gpr is not supported with predict()") + + if respfile is not None and not os.path.exists(respfile): + print("Response file does not exist. Only returning predictions") + respfile = None + if not os.path.isdir(model_path): + print('Models directory does not exist!') + return + else: + if os.path.exists(os.path.join(model_path, 'meta_data.md')): + with open(os.path.join(model_path, 'meta_data.md'), 'rb') as file: + meta_data = pickle.load(file) + inscaler = meta_data['inscaler'] + outscaler = meta_data['outscaler'] + mY = meta_data['mean_resp'] + sY = meta_data['std_resp'] + scaler_cov = meta_data['scaler_cov'] + scaler_resp = meta_data['scaler_resp'] + meta_data = True + else: + print("No meta-data file is found!") + inscaler = 'None' + outscaler = 'None' + meta_data = False + + if batch_size is not None: + batch_size = int(batch_size) + job_id = int(job_id) - 1 + + + # load data + print("Loading data ...") + X = fileio.load(covfile) + if len(X.shape) == 1: + X = X[:, np.newaxis] + + sample_num = X.shape[0] + if models is not None: + feature_num = len(models) + else: + feature_num = len(glob.glob(os.path.join(model_path, 'NM_'+ str(fold) + + '_*' + inputsuffix + '.pkl'))) + models = range(feature_num) + + Yhat = np.zeros([sample_num, feature_num]) + S2 = np.zeros([sample_num, feature_num]) + Z = np.zeros([sample_num, feature_num]) + + if inscaler in ['standardize', 'minmax', 'robminmax']: + Xz = scaler_cov[fold].transform(X) + else: + Xz = X + + # estimate the models for all subjects + for i, m in enumerate(models): + print("Prediction by model ", i+1, "of", feature_num) + nm = norm_init(Xz) + nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + + str(m) + inputsuffix + '.pkl')) + if (alg!='hbr' or nm.configs['transferred']==False): + yhat, s2 = nm.predict(Xz, **kwargs) + else: + tsbefile = kwargs.get('tsbefile') + batch_effects_test = fileio.load(tsbefile) + yhat, s2 = nm.predict_on_new_sites(Xz, batch_effects_test) + + if outscaler == 'standardize': + Yhat[:, i] = scaler_resp[fold].inverse_transform(yhat, index=i) + S2[:, i] = s2.squeeze() * sY[fold][i]**2 + elif outscaler in ['minmax', 'robminmax']: + Yhat[:, i] = scaler_resp[fold].inverse_transform(yhat, index=i) + S2[:, i] = s2 * (scaler_resp[fold].max[i] - scaler_resp[fold].min[i])**2 + else: + Yhat[:, i] = yhat.squeeze() + S2[:, i] = s2.squeeze() + + if respfile is None: + save_results(None, Yhat, S2, None, outputsuffix=outputsuffix) + + return (Yhat, S2) + + else: + Y, maskvol = load_response_vars(respfile, maskfile) + if models is not None and len(Y.shape) > 1: + Y = Y[:, models] + if meta_data: + # are we using cross-validation? + if type(mY) is list: + mY = mY[fold][models] + else: + mY = mY[models] + if type(sY) is list: + sY = sY[fold][models] + else: + sY = sY[models] + + if len(Y.shape) == 1: + Y = Y[:, np.newaxis] + + # warp the targets? + if alg == 'blr' and nm.blr.warp is not None: + warp = True + Yw = np.zeros_like(Y) + for i,m in enumerate(models): + nm = norm_init(Xz) + nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + + str(m) + inputsuffix + '.pkl')) + + warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] + Yw[:,i] = nm.blr.warp.f(Y[:,i], warp_param) + Y = Yw; + else: + warp = False + + Z = (Y - Yhat) / np.sqrt(S2) + + print("Evaluating the model ...") + if meta_data and not warp: + + results = evaluate(Y, Yhat, S2=S2, mY=mY, sY=sY) + else: + results = evaluate(Y, Yhat, S2=S2, + metrics = ['Rho', 'RMSE', 'SMSE', 'EXPV']) + + print("Evaluations Writing outputs ...") + save_results(respfile, Yhat, S2, maskvol, Z=Z, + outputsuffix=outputsuffix, results=results) + + return (Yhat, S2, Z) + + +def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, + **kwargs): + ''' + Transfer learning on the basis of a pre-estimated normative model by using + the posterior distribution over the parameters as an informed prior for + new data. currently only supported for HBR. + + Basic usage:: + + transfer(covfile, respfile [extra_arguments]) + + where the variables are defined below. + + :param covfile: transfer covariates used to predict the response variable + :param respfile: transfer response variables for the normative model + :param maskfile: mask used to apply to the data (nifti only) + :param testcov: Test covariates + :param testresp: Test responses + :param model_path: Directory containing the normative model and metadata + :param trbefile: Training batch effects file + :param batch_size: batch size (for use with normative_parallel) + :param job_id: batch id + + All outputs are written to disk in the same format as the input. These are: + + :outputs: * Yhat - predictive mean + * S2 - predictive variance + * Z - Z scores + ''' + alg = kwargs.pop('alg').lower() + + if alg != 'hbr' and alg != 'blr': + print('Model transfer function is only possible for HBR and BLR models.') + return + # testing should not be obligatory for HBR, + # but should be for BLR (since it doesn't produce transfer models) + elif (not 'model_path' in list(kwargs.keys())) or \ + (not 'trbefile' in list(kwargs.keys())): + print(f'{kwargs=}') + print('InputError: Some general mandatory arguments are missing.') + return + # hbr has one additional mandatory arguments + elif alg =='hbr': + if (not 'output_path' in list(kwargs.keys())): + print('InputError: Some mandatory arguments for hbr are missing.') + return + else: + output_path = kwargs.pop('output_path',None) + if not os.path.isdir(output_path): + os.mkdir(output_path) + + # for hbr, testing is not mandatory, for blr's predict/transfer it is. This will be an architectural choice. + #or (testresp==None) + elif alg =='blr': + if (testcov==None) or \ + (not 'tsbefile' in list(kwargs.keys())): + print('InputError: Some mandatory arguments for blr are missing.') + return + # general arguments + log_path = kwargs.pop('log_path', None) + model_path = kwargs.pop('model_path') + outputsuffix = kwargs.pop('outputsuffix', 'transfer') + outputsuffix = "_" + outputsuffix.replace("_", "") + inputsuffix = kwargs.pop('inputsuffix', 'estimate') + inputsuffix = "_" + inputsuffix.replace("_", "") + tsbefile = kwargs.pop('tsbefile', None) + trbefile = kwargs.pop('trbefile', None) + job_id = kwargs.pop('job_id', None) + batch_size = kwargs.pop('batch_size', None) + fold = kwargs.pop('fold',0) + + # for PCNonline automated parallel jobs loop + count_jobsdone = kwargs.pop('count_jobsdone','False') + if type(count_jobsdone) is str: + count_jobsdone = count_jobsdone=='True' + + if batch_size is not None: + batch_size = int(batch_size) + job_id = int(job_id) - 1 + + if not os.path.isdir(model_path): + print('Models directory does not exist!') + return + else: + if os.path.exists(os.path.join(model_path, 'meta_data.md')): + with open(os.path.join(model_path, 'meta_data.md'), 'rb') as file: + meta_data = pickle.load(file) + inscaler = meta_data['inscaler'] + outscaler = meta_data['outscaler'] + scaler_cov = meta_data['scaler_cov'] + scaler_resp = meta_data['scaler_resp'] + meta_data = True + else: + print("No meta-data file is found!") + inscaler = 'None' + outscaler = 'None' + meta_data = False + + # load adaptation data + print("Loading data ...") + X = fileio.load(covfile) + Y, maskvol = load_response_vars(respfile, maskfile) + if len(Y.shape) == 1: + Y = Y[:, np.newaxis] + if len(X.shape) == 1: + X = X[:, np.newaxis] + + if inscaler in ['standardize', 'minmax', 'robminmax']: + X = scaler_cov[0].transform(X) + + feature_num = Y.shape[1] + mY = np.mean(Y, axis=0) + sY = np.std(Y, axis=0) + + if outscaler in ['standardize', 'minmax', 'robminmax']: + Y = scaler_resp[0].transform(Y) + + batch_effects_train = fileio.load(trbefile) + + # load test data + if testcov is not None: + # we have a separate test dataset + Xte = fileio.load(testcov) + if len(Xte.shape) == 1: + Xte = Xte[:, np.newaxis] + ts_sample_num = Xte.shape[0] + if inscaler in ['standardize', 'minmax', 'robminmax']: + Xte = scaler_cov[0].transform(Xte) + + if testresp is not None: + Yte, testmask = load_response_vars(testresp, maskfile) + if len(Yte.shape) == 1: + Yte = Yte[:, np.newaxis] + else: + Yte = np.zeros([ts_sample_num, feature_num]) + + if tsbefile is not None: + batch_effects_test = fileio.load(tsbefile) + else: + batch_effects_test = np.zeros([Xte.shape[0],2]) + else: + ts_sample_num = 0 + + Yhat = np.zeros([ts_sample_num, feature_num]) + S2 = np.zeros([ts_sample_num, feature_num]) + Z = np.zeros([ts_sample_num, feature_num]) + + # estimate the models for all subjects + for i in range(feature_num): + + if alg == 'hbr': + print("Using HBR transform...") + nm = norm_init(X) + if batch_size is not None: # when using normative_parallel + print("Transferring model ", job_id*batch_size+i) + nm = nm.load(os.path.join(model_path, 'NM_0_' + + str(job_id*batch_size+i) + inputsuffix + + '.pkl')) + else: + print("Transferring model ", i+1, "of", feature_num) + nm = nm.load(os.path.join(model_path, 'NM_0_' + str(i) + + inputsuffix + '.pkl')) + + nm = nm.estimate_on_new_sites(X, Y[:,i], batch_effects_train) + if batch_size is not None: + nm.save(os.path.join(output_path, 'NM_0_' + + str(job_id*batch_size+i) + outputsuffix + '.pkl')) + else: + nm.save(os.path.join(output_path, 'NM_0_' + + str(i) + outputsuffix + '.pkl')) + + if testcov is not None: + yhat, s2 = nm.predict_on_new_sites(Xte, batch_effects_test) + + # We basically use normative.predict script here. + if alg == 'blr': + print("Using BLR transform...") + print("Transferring model ", i+1, "of", feature_num) + nm = norm_init(X) + nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + + str(i) + inputsuffix + '.pkl')) + + # translate the syntax to what blr understands + # first strip existing blr keyword arguments to avoid redundancy + adapt_cov = kwargs.pop('adaptcovfile', None) + adapt_res = kwargs.pop('adaptrespfile', None) + adapt_vg = kwargs.pop('adaptvargroupfile', None) + test_vg = kwargs.pop('testvargroupfile', None) + if adapt_cov is not None or adapt_res is not None \ + or adapt_vg is not None or test_vg is not None: + print("Warning: redundant batch effect parameterisation. Using HBR syntax") + + yhat, s2 = nm.predict(Xte, X, Y[:, i], + adaptcov = X, + adaptresp = Y[:, i], + adaptvargroup = batch_effects_train, + testvargroup = batch_effects_test, + **kwargs) + + if testcov is not None: + if outscaler == 'standardize': + Yhat[:, i] = scaler_resp[0].inverse_transform(yhat.squeeze(), index=i) + S2[:, i] = s2.squeeze() * sY[i]**2 + elif outscaler in ['minmax', 'robminmax']: + Yhat[:, i] = scaler_resp[0].inverse_transform(yhat, index=i) + S2[:, i] = s2 * (scaler_resp[0].max[i] - scaler_resp[0].min[i])**2 + else: + Yhat[:, i] = yhat.squeeze() + S2[:, i] = s2.squeeze() + + # Creates a file for every job succesfully completed (for tracking failed jobs). + if count_jobsdone==True: + done_path = os.path.join(log_path, str(job_id)+".jobsdone") + Path(done_path).touch() + + + if testresp is None: + save_results(respfile, Yhat, S2, maskvol, outputsuffix=outputsuffix) + return (Yhat, S2) + else: + # warp the targets? + if alg == 'blr' and nm.blr.warp is not None: + warp = True + Yw = np.zeros_like(Yte) + for i in range(feature_num): + nm = norm_init(Xte) + nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + + str(i) + inputsuffix + '.pkl')) + + warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] + Yw[:,i] = nm.blr.warp.f(Yte[:,i], warp_param) + Yte = Yw; + else: + warp = False + + Z = (Yte - Yhat) / np.sqrt(S2) + + print("Evaluating the model ...") + if meta_data and not warp: + results = evaluate(Yte, Yhat, S2=S2, mY=mY, sY=sY) + else: + results = evaluate(Yte, Yhat, S2=S2, + metrics = ['Rho', 'RMSE', 'SMSE', 'EXPV']) + + save_results(respfile, Yhat, S2, maskvol, Z=Z, results=results, + outputsuffix=outputsuffix) + + return (Yhat, S2, Z) + + +def extend(covfile, respfile, maskfile=None, **kwargs): + + ''' + This function extends an existing HBR model with data from new sites/scanners. + + Basic usage:: + + extend(covfile, respfile [extra_arguments]) + + where the variables are defined below. + + :param covfile: covariates for new data + :param respfile: response variables for new data + :param maskfile: mask used to apply to the data (nifti only) + :param model_path: Directory containing the normative model and metadata + :param trbefile: file address to batch effects file for new data + :param batch_size: batch size (for use with normative_parallel) + :param job_id: batch id + :param output_path: the path for saving the the extended model + :param informative_prior: use initial model prior or learn from scratch (default is False). + :param generation_factor: see below + + generation factor refers to the number of samples generated for each + combination of covariates and batch effects. Default is 10. + + + All outputs are written to disk in the same format as the input. + + ''' + + alg = kwargs.pop('alg') + if alg != 'hbr': + print('Model extention is only possible for HBR models.') + return + elif (not 'model_path' in list(kwargs.keys())) or \ + (not 'output_path' in list(kwargs.keys())) or \ + (not 'trbefile' in list(kwargs.keys())): + print('InputError: Some mandatory arguments are missing.') + return + else: + model_path = kwargs.pop('model_path') + output_path = kwargs.pop('output_path') + trbefile = kwargs.pop('trbefile') + + outputsuffix = kwargs.pop('outputsuffix', 'extend') + outputsuffix = "_" + outputsuffix.replace("_", "") + inputsuffix = kwargs.pop('inputsuffix', 'estimate') + inputsuffix = "_" + inputsuffix.replace("_", "") + informative_prior = kwargs.pop('informative_prior', 'False') == 'True' + generation_factor = int(kwargs.pop('generation_factor', '10')) + job_id = kwargs.pop('job_id', None) + batch_size = kwargs.pop('batch_size', None) + if batch_size is not None: + batch_size = int(batch_size) + job_id = int(job_id) - 1 + + if not os.path.isdir(model_path): + print('Models directory does not exist!') + return + else: + if os.path.exists(os.path.join(model_path, 'meta_data.md')): + with open(os.path.join(model_path, 'meta_data.md'), 'rb') as file: + meta_data = pickle.load(file) + if (meta_data['inscaler'] != 'None' or + meta_data['outscaler'] != 'None'): + print('Models extention on scaled data is not possible!') + return + + if not os.path.isdir(output_path): + os.mkdir(output_path) + + # load data + print("Loading data ...") + X = fileio.load(covfile) + Y, maskvol = load_response_vars(respfile, maskfile) + batch_effects_train = fileio.load(trbefile) + + if len(Y.shape) == 1: + Y = Y[:, np.newaxis] + if len(X.shape) == 1: + X = X[:, np.newaxis] + feature_num = Y.shape[1] + + # estimate the models for all subjects + for i in range(feature_num): + + nm = norm_init(X) + if batch_size is not None: # when using nirmative_parallel + print("Extending model ", job_id*batch_size+i) + nm = nm.load(os.path.join(model_path, 'NM_0_' + + str(job_id*batch_size+i) + inputsuffix + + '.pkl')) + else: + print("Extending model ", i+1, "of", feature_num) + nm = nm.load(os.path.join(model_path, 'NM_0_' + str(i) + + inputsuffix +'.pkl')) + + nm = nm.extend(X, Y[:,i:i+1], batch_effects_train, + samples=generation_factor, + informative_prior=informative_prior) + + if batch_size is not None: + nm.save(os.path.join(output_path, 'NM_0_' + + str(job_id*batch_size+i) + outputsuffix + '.pkl')) + nm.save(os.path.join('Models', 'NM_0_' + + str(i) + outputsuffix + '.pkl')) + else: + nm.save(os.path.join(output_path, 'NM_0_' + + str(i) + outputsuffix + '.pkl')) + + + +def tune(covfile, respfile, maskfile=None, **kwargs): + + ''' + This function tunes an existing HBR model with real data. + + Basic usage:: + + tune(covfile, respfile [extra_arguments]) + + where the variables are defined below. + + :param covfile: covariates for new data + :param respfile: response variables for new data + :param maskfile: mask used to apply to the data (nifti only) + :param model_path: Directory containing the normative model and metadata + :param trbefile: file address to batch effects file for new data + :param batch_size: batch size (for use with normative_parallel) + :param job_id: batch id + :param output_path: the path for saving the the extended model + :param informative_prior: use initial model prior or learn from scracth (default is False). + :param generation_factor: see below + + + generation factor refers to the number of samples generated for each + combination of covariates and batch effects. Default is 10. + + + All outputs are written to disk in the same format as the input. + + ''' + + alg = kwargs.pop('alg') + if alg != 'hbr': + print('Model extention is only possible for HBR models.') + return + elif (not 'model_path' in list(kwargs.keys())) or \ + (not 'output_path' in list(kwargs.keys())) or \ + (not 'trbefile' in list(kwargs.keys())): + print('InputError: Some mandatory arguments are missing.') + return + else: + model_path = kwargs.pop('model_path') + output_path = kwargs.pop('output_path') + trbefile = kwargs.pop('trbefile') + + outputsuffix = kwargs.pop('outputsuffix', 'tuned') + outputsuffix = "_" + outputsuffix.replace("_", "") + inputsuffix = kwargs.pop('inputsuffix', 'estimate') + inputsuffix = "_" + inputsuffix.replace("_", "") + informative_prior = kwargs.pop('informative_prior', 'False') == 'True' + generation_factor = int(kwargs.pop('generation_factor', '10')) + job_id = kwargs.pop('job_id', None) + batch_size = kwargs.pop('batch_size', None) + if batch_size is not None: + batch_size = int(batch_size) + job_id = int(job_id) - 1 + + if not os.path.isdir(model_path): + print('Models directory does not exist!') + return + else: + if os.path.exists(os.path.join(model_path, 'meta_data.md')): + with open(os.path.join(model_path, 'meta_data.md'), 'rb') as file: + meta_data = pickle.load(file) + if (meta_data['inscaler'] != 'None' or + meta_data['outscaler'] != 'None'): + print('Models extention on scaled data is not possible!') + return + + if not os.path.isdir(output_path): + os.mkdir(output_path) + + # load data + print("Loading data ...") + X = fileio.load(covfile) + Y, maskvol = load_response_vars(respfile, maskfile) + batch_effects_train = fileio.load(trbefile) + + if len(Y.shape) == 1: + Y = Y[:, np.newaxis] + if len(X.shape) == 1: + X = X[:, np.newaxis] + feature_num = Y.shape[1] + + # estimate the models for all subjects + for i in range(feature_num): + + nm = norm_init(X) + if batch_size is not None: # when using nirmative_parallel + print("Tuning model ", job_id*batch_size+i) + nm = nm.load(os.path.join(model_path, 'NM_0_' + + str(job_id*batch_size+i) + inputsuffix + + '.pkl')) + else: + print("Tuning model ", i+1, "of", feature_num) + nm = nm.load(os.path.join(model_path, 'NM_0_' + str(i) + + inputsuffix +'.pkl')) + + nm = nm.tune(X, Y[:,i:i+1], batch_effects_train, + samples=generation_factor, + informative_prior=informative_prior) + + if batch_size is not None: + nm.save(os.path.join(output_path, 'NM_0_' + + str(job_id*batch_size+i) + outputsuffix + '.pkl')) + nm.save(os.path.join('Models', 'NM_0_' + + str(i) + outputsuffix + '.pkl')) + else: + nm.save(os.path.join(output_path, 'NM_0_' + + str(i) + outputsuffix + '.pkl')) + + +def merge(covfile=None, respfile=None, **kwargs): + + ''' + This function extends an existing HBR model with data from new sites/scanners. + + Basic usage:: + + merge(model_path1, model_path2 [extra_arguments]) + + where the variables are defined below. + + :param covfile: Not required. Always set to None. + :param respfile: Not required. Always set to None. + :param model_path1: Directory containing the model and metadata (1st model) + :param model_path2: Directory containing the model and metadata (2nd model) + :param batch_size: batch size (for use with normative_parallel) + :param job_id: batch id + :param output_path: the path for saving the the extended model + :param generation_factor: see below + + The generation factor refers tothe number of samples generated for each + combination of covariates and batch effects. Default is 10. + + + All outputs are written to disk in the same format as the input. + + ''' + + alg = kwargs.pop('alg') + if alg != 'hbr': + print('Merging models is only possible for HBR models.') + return + elif (not 'model_path1' in list(kwargs.keys())) or \ + (not 'model_path2' in list(kwargs.keys())) or \ + (not 'output_path' in list(kwargs.keys())): + print('InputError: Some mandatory arguments are missing.') + return + else: + model_path1 = kwargs.pop('model_path1') + model_path2 = kwargs.pop('model_path2') + output_path = kwargs.pop('output_path') + + outputsuffix = kwargs.pop('outputsuffix', 'merge') + outputsuffix = "_" + outputsuffix.replace("_", "") + inputsuffix = kwargs.pop('inputsuffix', 'estimate') + inputsuffix = "_" + inputsuffix.replace("_", "") + generation_factor = int(kwargs.pop('generation_factor', '10')) + job_id = kwargs.pop('job_id', None) + batch_size = kwargs.pop('batch_size', None) + if batch_size is not None: + batch_size = int(batch_size) + job_id = int(job_id) - 1 + + if (not os.path.isdir(model_path1)) or (not os.path.isdir(model_path2)): + print('Models directory does not exist!') + return + else: + if batch_size is None: + with open(os.path.join(model_path1, 'meta_data.md'), 'rb') as file: + meta_data1 = pickle.load(file) + with open(os.path.join(model_path2, 'meta_data.md'), 'rb') as file: + meta_data2 = pickle.load(file) + if meta_data1['valid_voxels'].shape[0] != meta_data2['valid_voxels'].shape[0]: + print('Two models are trained on different features!') + return + else: + feature_num = meta_data1['valid_voxels'].shape[0] + else: + feature_num = batch_size + + + if not os.path.isdir(output_path): + os.mkdir(output_path) + + # mergeing the models + for i in range(feature_num): + + nm1 = norm_init(np.random.rand(100,10)) + nm2 = norm_init(np.random.rand(100,10)) + if batch_size is not None: # when using nirmative_parallel + print("Merging model ", job_id*batch_size+i) + nm1 = nm1.load(os.path.join(model_path1, 'NM_0_' + + str(job_id*batch_size+i) + inputsuffix + + '.pkl')) + nm2 = nm2.load(os.path.join(model_path2, 'NM_0_' + + str(job_id*batch_size+i) + inputsuffix + + '.pkl')) + else: + print("Merging model ", i+1, "of", feature_num) + nm1 = nm1.load(os.path.join(model_path1, 'NM_0_' + str(i) + + inputsuffix +'.pkl')) + nm2 = nm1.load(os.path.join(model_path2, 'NM_0_' + str(i) + + inputsuffix +'.pkl')) + + nm_merged = nm1.merge(nm2, samples=generation_factor) + + if batch_size is not None: + nm_merged.save(os.path.join(output_path, 'NM_0_' + + str(job_id*batch_size+i) + outputsuffix + '.pkl')) + nm_merged.save(os.path.join('Models', 'NM_0_' + + str(i) + outputsuffix + '.pkl')) + else: + nm_merged.save(os.path.join(output_path, 'NM_0_' + + str(i) + outputsuffix + '.pkl')) + + +def main(*args): + """ Parse arguments and estimate model + """ + + np.seterr(invalid='ignore') + + rfile, mfile, cfile, cv, tcfile, trfile, func, alg, cfg, kw = get_args(args) + + # collect required arguments + pos_args = ['cfile', 'rfile'] + + # collect basic keyword arguments controlling model estimation + kw_args = ['maskfile=mfile', + 'cvfolds=cv', + 'testcov=tcfile', + 'testresp=trfile', + 'alg=alg', + 'configparam=cfg'] + + # add additional keyword arguments + for k in kw: + kw_args.append(k + '=' + "'" + kw[k] + "'") + all_args = ', '.join(pos_args + kw_args) + + # Executing the target function + exec(func + '(' + all_args + ')') + +# For running from the command line: +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/build/lib/pcntoolkit/normative_NP.py b/build/lib/pcntoolkit/normative_NP.py new file mode 100644 index 00000000..3694e146 --- /dev/null +++ b/build/lib/pcntoolkit/normative_NP.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 + +# -*- coding: utf-8 -*- +""" +Created on Tue Jun 18 09:47:01 2019 + +@author: seykia +""" +# ------------------------------------------------------------------------------ +# Usage: +# python normative_NP.py -r /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/responses.nii.gz +# -c /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/covariates.pickle +# --tr /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/test_responses.nii.gz +# --tc /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/test_covariates.pickle +# -o /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/Results +# +# +# Written by S. M. Kia +# ------------------------------------------------------------------------------ + +from __future__ import print_function +from __future__ import division + +import sys +import argparse +import torch +from torch import optim +import numpy as np +import pickle +from pcntoolkit.model.NP import NP, apply_dropout_test, np_loss +from sklearn.preprocessing import MinMaxScaler, StandardScaler +from sklearn.linear_model import LinearRegression, MultiTaskLasso +from pcntoolkit.model.architecture import Encoder, Decoder +from pcntoolkit.util.utils import compute_pearsonr, explained_var, compute_MSLL +from pcntoolkit.util.utils import extreme_value_prob, extreme_value_prob_fit, ravel_2D, unravel_2D +from pcntoolkit.dataio import fileio +import os + +try: # run as a package if installed + from pcntoolkit import configs +except ImportError: + pass + + path = os.path.abspath(os.path.dirname(__file__)) + if path not in sys.path: + sys.path.append(path) + del path + import configs + +def get_args(*args): + """ Parse command line arguments""" + + ############################ Parsing inputs ############################### + + parser = argparse.ArgumentParser(description='Neural Processes (NP) for Deep Normative Modeling') + parser.add_argument("-r", help="Training response nifti file address", + required=True, dest="respfile", default=None) + parser.add_argument("-c", help="Training covariates pickle file address", + required=True, dest="covfile", default=None) + parser.add_argument("--tc", help="Test covariates pickle file address", + required=True, dest="testcovfile", default=None) + parser.add_argument("--tr", help="Test response nifti file address", + dest="testrespfile", default=None) + parser.add_argument("--mask", help="Mask nifti file address", + dest="mask", default=None) + parser.add_argument("-o", help="Output directory address", dest="outdir", default=None) + parser.add_argument('-m', type=int, default=10, dest='m', + help='number of fixed-effect estimations') + parser.add_argument('--batchnum', type=int, default=10, dest='batchnum', + help='input batch size for training') + parser.add_argument('--epochs', type=int, default=100, dest='epochs', + help='number of epochs to train') + parser.add_argument('--device', type=str, default='cuda', dest='device', + help='Either cpu or cuda') + parser.add_argument('--fxestimator', type=str, default='ST', dest='estimator', + help='Fixed-effect estimator type.') + + args = parser.parse_args() + + if (args.respfile == None or args.covfile == None or args.testcovfile == None): + raise(ValueError, "Training response nifti file, Training covariates pickle file, and \ + Test covariates pickle file must be specified.") + if (args.outdir == None): + args.outdir = os.getcwd() + + cuda = args.device=='cuda' and torch.cuda.is_available() + args.device = torch.device("cuda" if cuda else "cpu") + args.kwargs = {'num_workers': 1, 'pin_memory': True} if cuda else {} + args.type= 'MT' + + return args + +def estimate(args): + torch.set_default_dtype(torch.float32) + args.type = 'MT' + print('Loading the input Data ...') + responses = fileio.load_nifti(args.respfile, vol=True).transpose([3,0,1,2]) + response_shape = responses.shape + with open(args.covfile, 'rb') as handle: + covariates = pickle.load(handle)['covariates'] + with open(args.testcovfile, 'rb') as handle: + test_covariates = pickle.load(handle)['test_covariates'] + if args.mask is not None: + mask = fileio.load_nifti(args.mask, vol=True) + mask = fileio.create_mask(mask, mask=None) + else: + mask = fileio.create_mask(responses[0,:,:,:], mask=None) + if args.testrespfile is not None: + test_responses = fileio.load_nifti(args.testrespfile, vol=True).transpose([3,0,1,2]) + test_responses_shape = test_responses.shape + + print('Normalizing the input Data ...') + covariates_scaler = StandardScaler() + covariates = covariates_scaler.fit_transform(covariates) + test_covariates = covariates_scaler.transform(test_covariates) + response_scaler = MinMaxScaler() + responses = unravel_2D(response_scaler.fit_transform(ravel_2D(responses)), response_shape) + if args.testrespfile is not None: + test_responses = unravel_2D(response_scaler.transform(ravel_2D(test_responses)), test_responses_shape) + test_responses = np.expand_dims(test_responses, axis=1) + + factor = args.m + + x_context = np.zeros([covariates.shape[0], factor, covariates.shape[1]], dtype=np.float32) + y_context = np.zeros([responses.shape[0], factor, responses.shape[1], + responses.shape[2], responses.shape[3]], dtype=np.float32) + x_all = np.zeros([covariates.shape[0], factor, covariates.shape[1]], dtype=np.float32) + x_context_test = np.zeros([test_covariates.shape[0], factor, test_covariates.shape[1]], dtype=np.float32) + y_context_test = np.zeros([test_covariates.shape[0], factor, responses.shape[1], + responses.shape[2], responses.shape[3]], dtype=np.float32) + + print('Estimating the fixed-effects ...') + for i in range(factor): + x_context[:,i,:] = covariates[:,:] + x_context_test[:,i,:] = test_covariates[:,:] + idx = np.random.randint(0,covariates.shape[0], covariates.shape[0]) + if args.estimator=='ST': + for j in range(responses.shape[1]): + for k in range(responses.shape[2]): + for l in range(responses.shape[3]): + reg = LinearRegression() + reg.fit(x_context[idx,i,:], responses[idx,j,k,l]) + y_context[:,i,j,k,l] = reg.predict(x_context[:,i,:]) + y_context_test[:,i,j,k,l] = reg.predict(x_context_test[:,i,:]) + elif args.estimator=='MT': + reg = MultiTaskLasso(alpha=0.1) + reg.fit(x_context[idx,i,:], np.reshape(responses[idx,:,:,:], [covariates.shape[0],np.prod(responses.shape[1:])])) + y_context[:,i,:,:,:] = np.reshape(reg.predict(x_context[:,i,:]), + [x_context.shape[0],responses.shape[1],responses.shape[2],responses.shape[3]]) + y_context_test[:,i,:,:,:] = np.reshape(reg.predict(x_context_test[:,i,:]), + [x_context_test.shape[0],responses.shape[1],responses.shape[2],responses.shape[3]]) + print('Fixed-effect %d of %d is computed!' %(i+1, factor)) + + x_all = x_context + responses = np.expand_dims(responses, axis=1).repeat(factor, axis=1) + + ################################## TRAINING ################################# + + encoder = Encoder(x_context, y_context, args).to(args.device) + args.cnn_feature_num = encoder.cnn_feature_num + decoder = Decoder(x_context, y_context, args).to(args.device) + model = NP(encoder, decoder, args).to(args.device) + + print('Estimating the Random-effect ...') + k = 1 + epochs = [int(args.epochs/4),int(args.epochs/2),int(args.epochs/5),int(args.epochs-args.epochs/4-args.epochs/2-args.epochs/5)] + mini_batch_num = args.batchnum + batch_size = int(x_context.shape[0]/mini_batch_num) + model.train() + for e in range(len(epochs)): + optimizer = optim.Adam(model.parameters(), lr=10**(-e-2)) + for j in range(epochs[e]): + train_loss = 0 + rand_idx = np.random.permutation(x_context.shape[0]) + for i in range(mini_batch_num): + optimizer.zero_grad() + idx = rand_idx[i*batch_size:(i+1)*batch_size] + y_hat, z_all, z_context, dummy = model(torch.tensor(x_context[idx,:,:], device = args.device), + torch.tensor(y_context[idx,:,:,:,:], device = args.device), + torch.tensor(x_all[idx,:,:], device = args.device), + torch.tensor(responses[idx,:,:,:,:], device = args.device)) + loss = np_loss(y_hat, torch.tensor(responses[idx,:,:,:,:], device = args.device), z_all, z_context) + loss.backward() + train_loss += loss.item() + optimizer.step() + print('Epoch: %d, Loss:%f, Average Loss:%f' %(k, train_loss, train_loss/responses.shape[0])) + k += 1 + + ################################## Evaluation ################################# + + print('Predicting on Test Data ...') + model.eval() + model.apply(apply_dropout_test) + with torch.no_grad(): + y_hat, z_all, z_context, y_sigma = model(torch.tensor(x_context_test, device = args.device), + torch.tensor(y_context_test, device = args.device), n = 15) + if args.testrespfile is not None: + test_loss = np_loss(y_hat[0:test_responses_shape[0],:], + torch.tensor(test_responses, device = args.device), + z_all, z_context).item() + print('Average Test Loss:%f' %(test_loss/test_responses_shape[0])) + + RMSE = np.sqrt(np.mean((test_responses - y_hat[0:test_responses_shape[0],:].cpu().numpy())**2, axis = 0)).squeeze() * mask + SMSE = RMSE ** 2 / np.var(test_responses, axis=0).squeeze() + Rho, pRho = compute_pearsonr(test_responses.squeeze(), y_hat[0:test_responses_shape[0],:].cpu().numpy().squeeze()) + EXPV = explained_var(test_responses.squeeze(), y_hat[0:test_responses_shape[0],:].cpu().numpy().squeeze()) * mask + MSLL = compute_MSLL(test_responses.squeeze(), y_hat[0:test_responses_shape[0],:].cpu().numpy().squeeze(), + y_sigma[0:test_responses_shape[0],:].cpu().numpy().squeeze()**2, train_mean = test_responses.mean(0), + train_var = test_responses.var(0)).squeeze() * mask + + NPMs = (test_responses - y_hat[0:test_responses_shape[0],:].cpu().numpy()) / (y_sigma[0:test_responses_shape[0],:].cpu().numpy()) + NPMs = NPMs.squeeze() + NPMs = NPMs * mask + NPMs = np.nan_to_num(NPMs) + + temp=NPMs.reshape([NPMs.shape[0],NPMs.shape[1]*NPMs.shape[2]*NPMs.shape[3]]) + EVD_params = extreme_value_prob_fit(temp, 0.01) + abnormal_probs = extreme_value_prob(EVD_params, temp, 0.01) + + ############################## SAVING RESULTS ################################# + + print('Saving Results to: %s' %(args.outdir)) + exfile = args.respfile + y_hat = y_hat.squeeze().cpu().numpy() + y_hat = response_scaler.inverse_transform(ravel_2D(y_hat)) + y_hat = y_hat[:,mask.flatten()] + fileio.save(y_hat.T, args.outdir + + '/yhat.nii.gz', example=exfile, mask=mask) + ys2 = y_sigma.squeeze().cpu().numpy() + ys2 = ravel_2D(ys2) * (response_scaler.data_max_ - response_scaler.data_min_) + ys2 = ys2**2 + ys2 = ys2[:,mask.flatten()] + fileio.save(ys2.T, args.outdir + + '/ys2.nii.gz', example=exfile, mask=mask) + if args.testrespfile is not None: + NPMs = ravel_2D(NPMs)[:,mask.flatten()] + fileio.save(NPMs.T, args.outdir + + '/Z.nii.gz', example=exfile, mask=mask) + fileio.save(Rho.flatten()[mask.flatten()], args.outdir + + '/Rho.nii.gz', example=exfile, mask=mask) + fileio.save(pRho.flatten()[mask.flatten()], args.outdir + + '/pRho.nii.gz', example=exfile, mask=mask) + fileio.save(RMSE.flatten()[mask.flatten()], args.outdir + + '/rmse.nii.gz', example=exfile, mask=mask) + fileio.save(SMSE.flatten()[mask.flatten()], args.outdir + + '/smse.nii.gz', example=exfile, mask=mask) + fileio.save(EXPV.flatten()[mask.flatten()], args.outdir + + '/expv.nii.gz', example=exfile, mask=mask) + fileio.save(MSLL.flatten()[mask.flatten()], args.outdir + + '/msll.nii.gz', example=exfile, mask=mask) + + with open(args.outdir +'model.pkl', 'wb') as handle: + pickle.dump({'model':model, 'covariates_scaler':covariates_scaler, + 'response_scaler': response_scaler, 'EVD_params':EVD_params, + 'abnormal_probs':abnormal_probs}, handle, protocol=configs.PICKLE_PROTOCOL) + +############################################################################### + print('DONE!') + + +def main(*args): + """ Parse arguments and estimate model + """ + + np.seterr(invalid='ignore') + args = get_args(args) + estimate(args) + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/build/lib/pcntoolkit/normative_model/__init__.py b/build/lib/pcntoolkit/normative_model/__init__.py new file mode 100644 index 00000000..772a3653 --- /dev/null +++ b/build/lib/pcntoolkit/normative_model/__init__.py @@ -0,0 +1,6 @@ +from . import norm_gpr +from . import norm_base +from . import norm_blr +from . import norm_rfa +from . import norm_hbr +from . import norm_utils \ No newline at end of file diff --git a/build/lib/pcntoolkit/normative_model/norm_base.py b/build/lib/pcntoolkit/normative_model/norm_base.py new file mode 100644 index 00000000..3e46ef93 --- /dev/null +++ b/build/lib/pcntoolkit/normative_model/norm_base.py @@ -0,0 +1,60 @@ +import os +import sys +from six import with_metaclass +from abc import ABCMeta, abstractmethod +import pickle + +try: # run as a package if installed + from pcntoolkit import configs +except ImportError: + pass + + path = os.path.abspath(os.path.dirname(__file__)) + if path not in sys.path: + sys.path.append(path) + del path + import configs + + +class NormBase(with_metaclass(ABCMeta)): + """ Base class for normative model back-end. + + All normative modelling approaches must define the following methods:: + + NormativeModel.estimate() + NormativeModel.predict() + """ + + def __init__(self, x=None): + pass + + @abstractmethod + def estimate(self, X, y): + """ Estimate the normative model """ + + @abstractmethod + def predict(self, Xs, X, y): + """ Make predictions for new data """ + + @property + @abstractmethod + def n_params(self): + """ Report the number of parameters required by the model """ + + def save(self, save_path): + try: + with open(save_path, 'wb') as handle: + pickle.dump(self, handle, protocol=configs.PICKLE_PROTOCOL) + return True + except Exception as err: + print('Error:', err) + raise + + def load(self, load_path): + try: + with open(load_path, 'rb') as handle: + nm = pickle.load(handle) + return nm + except Exception as err: + print('Error:', err) + raise diff --git a/build/lib/pcntoolkit/normative_model/norm_blr.py b/build/lib/pcntoolkit/normative_model/norm_blr.py new file mode 100644 index 00000000..814e0b08 --- /dev/null +++ b/build/lib/pcntoolkit/normative_model/norm_blr.py @@ -0,0 +1,252 @@ +from __future__ import print_function +from __future__ import division + +import os +import sys +import numpy as np +import pandas as pd +from ast import literal_eval + +try: # run as a package if installed + from pcntoolkit.model.bayesreg import BLR + from pcntoolkit.normative_model.norm_base import NormBase + from pcntoolkit.dataio import fileio + from pcntoolkit.util.utils import create_poly_basis, WarpBoxCox, \ + WarpAffine, WarpCompose, WarpSinArcsinh +except ImportError: + pass + + path = os.path.abspath(os.path.dirname(__file__)) + if path not in sys.path: + sys.path.append(path) + del path + + from model.bayesreg import BLR + from norm_base import NormBase + from dataio import fileio + from util.utils import create_poly_basis, WarpBoxCox, \ + WarpAffine, WarpCompose, WarpSinArcsinh + +class NormBLR(NormBase): + """ Normative modelling based on Bayesian Linear Regression + """ + + def __init__(self, **kwargs): + X = kwargs.pop('X', None) + y = kwargs.pop('y', None) + theta = kwargs.pop('theta', None) + if isinstance(theta, str): + theta = np.array(literal_eval(theta)) + self.optim_alg = kwargs.get('optimizer','powell') + + if X is None: + raise(ValueError, "Data matrix must be specified") + + if len(X.shape) == 1: + self.D = 1 + else: + self.D = X.shape[1] + + # Parse model order + if kwargs is None: + model_order = 1 + elif 'configparam' in kwargs: # deprecated syntax + model_order = kwargs.pop('configparam') + elif 'model_order' in kwargs: + model_order = kwargs.pop('model_order') + else: + model_order = 1 + + # Force a default model order and check datatype + if model_order is None: + model_order = 1 + if type(model_order) is not int: + model_order = int(model_order) + + # configure heteroskedastic noise + if 'varcovfile' in kwargs: + var_cov_file = kwargs.get('varcovfile') + if var_cov_file.endswith('.pkl'): + self.var_covariates = pd.read_pickle(var_cov_file) + else: + self.var_covariates = np.loadtxt(var_cov_file) + if len(self.var_covariates.shape) == 1: + self.var_covariates = self.var_covariates[:, np.newaxis] + n_beta = self.var_covariates.shape[1] + self.var_groups = None + elif 'vargroupfile' in kwargs: + # configure variance groups (e.g. site specific variance) + var_groups_file = kwargs.pop('vargroupfile') + if var_groups_file.endswith('.pkl'): + self.var_groups = pd.read_pickle(var_groups_file) + else: + self.var_groups = np.loadtxt(var_groups_file) + var_ids = set(self.var_groups) + var_ids = sorted(list(var_ids)) + n_beta = len(var_ids) + else: + self.var_groups = None + self.var_covariates = None + n_beta = 1 + + # are we using ARD? + if 'use_ard' in kwargs: + self.use_ard = kwargs.pop('use_ard') + else: + self.use_ard = False + if self.use_ard: + n_alpha = self.D * model_order + else: + n_alpha = 1 + + # Configure warped likelihood + if 'warp' in kwargs: + warp_str = kwargs.pop('warp') + if warp_str is None: + self.warp = None + n_gamma = 0 + else: + # set up warp + exec('self.warp =' + warp_str + '()') + n_gamma = self.warp.get_n_params() + else: + self.warp = None + n_gamma = 0 + + self._n_params = n_alpha + n_beta + n_gamma + self._model_order = model_order + + print("configuring BLR ( order", model_order, ")") + if (theta is None) or (len(theta) != self._n_params): + print("Using default hyperparameters") + self.theta0 = np.zeros(self._n_params) + else: + self.theta0 = theta + self.theta = self.theta0 + + # initialise the BLR object if the required parameters are present + if (theta is not None) and (y is not None): + Phi = create_poly_basis(X, self._model_order) + self.blr = BLR(theta=theta, X=Phi, y=y, + warp=self.warp, **kwargs) + else: + self.blr = BLR(**kwargs) + + @property + def n_params(self): + return self._n_params + + @property + def neg_log_lik(self): + return self.blr.nlZ + + def estimate(self, X, y, **kwargs): + theta = kwargs.pop('theta', None) + if isinstance(theta, str): + theta = np.array(literal_eval(theta)) + + # remove warp string to prevent it being passed to the blr object + kwargs.pop('warp',None) + + Phi = create_poly_basis(X, self._model_order) + if len(y.shape) > 1: + y = y.ravel() + + if theta is None: + theta = self.theta0 + + # (re-)initialize BLR object because parameters were not specified + self.blr = BLR(theta=theta, X=Phi, y=y, + var_groups=self.var_groups, + warp=self.warp, **kwargs) + + self.theta = self.blr.estimate(theta, Phi, y, + var_covariates=self.var_covariates, **kwargs) + + return self + + def predict(self, Xs, X=None, y=None, **kwargs): + + theta = self.theta # always use the estimated coefficients + # remove from kwargs to avoid downstream problems + kwargs.pop('theta', None) + + Phis = create_poly_basis(Xs, self._model_order) + + if X is None: + Phi = None + else: + Phi = create_poly_basis(X, self._model_order) + + # process variance groups for the test data + if 'testvargroup' in kwargs: + var_groups_te = kwargs.pop('testvargroup') + else: + if 'testvargroupfile' in kwargs: + var_groups_test_file = kwargs.pop('testvargroupfile') + if var_groups_test_file.endswith('.pkl'): + var_groups_te = pd.read_pickle(var_groups_test_file) + else: + var_groups_te = np.loadtxt(var_groups_test_file) + else: + var_groups_te = None + + # process test variance covariates + if 'testvarcov' in kwargs: + var_cov_te = kwargs.pop('testvarcov') + else: + if 'testvarcovfile' in kwargs: + var_cov_test_file = kwargs.get('testvarcovfile') + if var_cov_test_file.endswith('.pkl'): + var_cov_te = pd.read_pickle(var_cov_test_file) + else: + var_cov_te = np.loadtxt(var_cov_test_file) + else: + var_cov_te = None + + # do we want to adjust the responses? + if 'adaptresp' in kwargs: + y_adapt = kwargs.pop('adaptresp') + else: + if 'adaptrespfile' in kwargs: + y_adapt = fileio.load(kwargs.pop('adaptrespfile')) + if len(y_adapt.shape) == 1: + y_adapt = y_adapt[:, np.newaxis] + else: + y_adapt = None + + if 'adaptcov' in kwargs: + X_adapt = kwargs.pop('adaptcov') + Phi_adapt = create_poly_basis(X_adapt, self._model_order) + else: + if 'adaptcovfile' in kwargs: + X_adapt = fileio.load(kwargs.pop('adaptcovfile')) + Phi_adapt = create_poly_basis(X_adapt, self._model_order) + else: + Phi_adapt = None + + if 'adaptvargroup' in kwargs: + var_groups_ad = kwargs.pop('adaptvargroup') + else: + if 'adaptvargroupfile' in kwargs: + var_groups_adapt_file = kwargs.pop('adaptvargroupfile') + if var_groups_adapt_file.endswith('.pkl'): + var_groups_ad = pd.read_pickle(var_groups_adapt_file) + else: + var_groups_ad = np.loadtxt(var_groups_adapt_file) + else: + var_groups_ad = None + + if y_adapt is None: + yhat, s2 = self.blr.predict(theta, Phi, y, Phis, + var_groups_test=var_groups_te, + var_covariates_test=var_cov_te, + **kwargs) + else: + yhat, s2 = self.blr.predict_and_adjust(theta, Phi_adapt, y_adapt, Phis, + var_groups_test=var_groups_te, + var_groups_adapt=var_groups_ad, + **kwargs) + + return yhat, s2 + \ No newline at end of file diff --git a/build/lib/pcntoolkit/normative_model/norm_gpr.py b/build/lib/pcntoolkit/normative_model/norm_gpr.py new file mode 100644 index 00000000..a74cc95b --- /dev/null +++ b/build/lib/pcntoolkit/normative_model/norm_gpr.py @@ -0,0 +1,72 @@ +from __future__ import print_function +from __future__ import division + +import os +import sys +import numpy as np + +try: # run as a package if installed + from pcntoolkit.model.gp import GPR, CovSum + from pcntoolkit.normative_model.norm_base import NormBase +except ImportError: + pass + + path = os.path.abspath(os.path.dirname(__file__)) + if path not in sys.path: + sys.path.append(path) + del path + + from model.gp import GPR, CovSum + from norm_base import NormBase + +class NormGPR(NormBase): + """ Classical GPR-based normative modelling approach + """ + + def __init__(self, **kwargs): #X=None, y=None, theta=None, + X = kwargs.pop('X', None) + y = kwargs.pop('y', None) + theta = kwargs.pop('theta', None) + + self.covfunc = CovSum(X, ('CovLin', 'CovSqExpARD')) + self.theta0 = np.zeros(self.covfunc.get_n_params() + 1) + self.theta = self.theta0 + + if (theta is not None) and (X is not None) and (y is not None): + self.gpr = GPR(theta, self.covfunc, X, y) + self._n_params = self.covfunc.get_n_params() + 1 + else: + self.gpr = GPR() + + @property + def n_params(self): + if not hasattr(self,'_n_params'): + self._n_params = self.covfunc.get_n_params() + 1 + + return self._n_params + + @property + def neg_log_lik(self): + return self.gpr.nlZ + + def estimate(self, X, y, **kwargs): + theta = kwargs.pop('theta', None) + if theta is None: + theta = self.theta0 + self.gpr = GPR(theta, self.covfunc, X, y) + self.theta = self.gpr.estimate(theta, self.covfunc, X, y) + + return self + + def predict(self, Xs, X, y, **kwargs): + theta = kwargs.pop('theta', None) + if theta is None: + theta = self.theta + yhat, s2 = self.gpr.predict(theta, X, y, Xs) + + # only return the marginal variances + if len(s2.shape) == 2: + s2 = np.diag(s2) + + return yhat, s2 + \ No newline at end of file diff --git a/build/lib/pcntoolkit/normative_model/norm_hbr.py b/build/lib/pcntoolkit/normative_model/norm_hbr.py new file mode 100644 index 00000000..8ad0b11a --- /dev/null +++ b/build/lib/pcntoolkit/normative_model/norm_hbr.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Thu Jul 25 17:01:24 2019 + +@author: seykia +@author: augub +""" + +from __future__ import print_function +from __future__ import division + + +import os +import warnings +import sys +import numpy as np +from ast import literal_eval as make_tuple + +try: + from pcntoolkit.dataio import fileio + from pcntoolkit.normative_model.norm_base import NormBase + from pcntoolkit.model.hbr import HBR +except ImportError: + pass + + path = os.path.abspath(os.path.dirname(__file__)) + if path not in sys.path: + sys.path.append(path) + del path + import dataio.fileio as fileio + from model.hbr import HBR + from norm_base import NormBase + + +class NormHBR(NormBase): + """ Classical GPR-based normative modelling approach + """ + + def __init__(self, **kwargs): + + self.configs = dict() + self.configs['transferred'] = False + self.configs['trbefile'] = kwargs.pop('trbefile', None) + self.configs['tsbefile'] = kwargs.pop('tsbefile', None) + self.configs['type'] = kwargs.pop('model_type', 'linear') + self.configs['skewed_likelihood'] = kwargs.pop('skewed_likelihood', 'False') == 'True' + self.configs['pred_type'] = kwargs.pop('pred_type', 'single') + self.configs['random_noise'] = kwargs.pop('random_noise', 'True') == 'True' + self.configs['n_samples'] = int(kwargs.pop('n_samples', '1000')) + self.configs['n_tuning'] = int(kwargs.pop('n_tuning', '500')) + self.configs['n_chains'] = int(kwargs.pop('n_chains', '1')) + self.configs['likelihood'] = kwargs.pop('likelihood', 'Normal') + self.configs['sampler'] = kwargs.pop('sampler', 'NUTS') + self.configs['target_accept'] = float(kwargs.pop('target_accept', '0.8')) + self.configs['init'] = kwargs.pop('init', 'jitter+adapt_diag') + self.configs['cores'] = int(kwargs.pop('cores', '1')) + self.configs['freedom'] = int(kwargs.pop('freedom', '1')) + + if self.configs['type'] == 'bspline': + self.configs['order'] = int(kwargs.pop('order', '3')) + self.configs['nknots'] = int(kwargs.pop('nknots', '5')) + elif self.configs['type'] == 'polynomial': + self.configs['order'] = int(kwargs.pop('order', '3')) + elif self.configs['type'] == 'nn': + self.configs['nn_hidden_neuron_num'] = int(kwargs.pop('nn_hidden_neuron_num', '2')) + self.configs['nn_hidden_layers_num'] = int(kwargs.pop('nn_hidden_layers_num', '2')) + if self.configs['nn_hidden_layers_num'] > 2: + raise ValueError("Using " + str(self.configs['nn_hidden_layers_num']) \ + + " layers was not implemented. The number of " \ + + " layers has to be less than 3.") + elif self.configs['type'] == 'linear': + pass + else: + raise ValueError("Unknown model type, please specify from 'linear', \ + 'polynomial', 'bspline', or 'nn'.") + + if self.configs['type'] in ['bspline', 'polynomial', 'linear']: + + for p in ['mu', 'sigma', 'epsilon', 'delta']: + self.configs[f'linear_{p}'] = kwargs.pop(f'linear_{p}', 'False') == 'True' + + ######## Deprecations (remove in later version) + if f'{p}_linear' in kwargs.keys(): + print(f'The keyword \'{p}_linear\' is deprecated. It is now automatically replaced with \'linear_{p}\'') + self.configs[f'linear_{p}'] = kwargs.pop(f'{p}_linear', 'False') == 'True' + ##### End Deprecations + + for c in ['centered','random']: + self.configs[f'{c}_{p}'] = kwargs.pop(f'{c}_{p}', 'False') == 'True' + for sp in ['slope','intercept']: + self.configs[f'{c}_{sp}_{p}'] = kwargs.pop(f'{c}_{sp}_{p}', 'False') == 'True' + + ######## Deprecations (remove in later version) + if self.configs['linear_sigma']: + if 'random_noise' in kwargs.keys(): + print("The keyword \'random_noise\' is deprecated. It is now automatically replaced with \'random_intercept_sigma\', because sigma is linear") + self.configs['random_intercept_sigma'] = kwargs.pop('random_noise','False') == 'True' + elif 'random_noise' in kwargs.keys(): + print("The keyword \'random_noise\' is deprecated. It is now automatically replaced with \'random_sigma\', because sigma is fixed") + self.configs['random_sigma'] = kwargs.pop('random_noise','False') == 'True' + if 'random_slope' in kwargs.keys(): + print("The keyword \'random_slope\' is deprecated. It is now automatically replaced with \'random_intercept_mu\'") + self.configs['random_intercept_mu'] = kwargs.pop('random_slope','False') == 'True' + ##### End Deprecations + + + self.hbr = HBR(self.configs) + + @property + def n_params(self): + return 1 + + @property + def neg_log_lik(self): + return -1 + + def estimate(self, X, y, **kwargs): + + trbefile = kwargs.pop('trbefile', None) + if trbefile is not None: + batch_effects_train = fileio.load(trbefile) + else: + print('Could not find batch-effects file! Initilizing all as zeros ...') + batch_effects_train = np.zeros([X.shape[0], 1]) + + self.hbr.estimate(X, y, batch_effects_train) + + return self + + def predict(self, Xs, X=None, Y=None, **kwargs): + + tsbefile = kwargs.pop('tsbefile', None) + if tsbefile is not None: + batch_effects_test = fileio.load(tsbefile) + else: + print('Could not find batch-effects file! Initilizing all as zeros ...') + batch_effects_test = np.zeros([Xs.shape[0], 1]) + + pred_type = self.configs['pred_type'] + + if self.configs['transferred'] == False: + yhat, s2 = self.hbr.predict(Xs, batch_effects_test, pred=pred_type) + else: + raise ValueError("This is a transferred model. Please use predict_on_new_sites function.") + + return yhat.squeeze(), s2.squeeze() + + def estimate_on_new_sites(self, X, y, batch_effects): + self.hbr.estimate_on_new_site(X, y, batch_effects) + self.configs['transferred'] = True + return self + + def predict_on_new_sites(self, X, batch_effects): + + yhat, s2 = self.hbr.predict_on_new_site(X, batch_effects) + return yhat, s2 + + def extend(self, X, y, batch_effects, X_dummy_ranges=[[0.1, 0.9, 0.01]], + merge_batch_dim=0, samples=10, informative_prior=False): + + X_dummy, batch_effects_dummy = self.hbr.create_dummy_inputs(X_dummy_ranges) + + X_dummy, batch_effects_dummy, Y_dummy = self.hbr.generate(X_dummy, + batch_effects_dummy, samples) + + batch_effects[:, merge_batch_dim] = batch_effects[:, merge_batch_dim] + \ + np.max(batch_effects_dummy[:, merge_batch_dim]) + 1 + + if informative_prior: + self.hbr.adapt(np.concatenate((X_dummy, X)), + np.concatenate((Y_dummy, y)), + np.concatenate((batch_effects_dummy, batch_effects))) + else: + self.hbr.estimate(np.concatenate((X_dummy, X)), + np.concatenate((Y_dummy, y)), + np.concatenate((batch_effects_dummy, batch_effects))) + + return self + + def tune(self, X, y, batch_effects, X_dummy_ranges=[[0.1, 0.9, 0.01]], + merge_batch_dim=0, samples=10, informative_prior=False): + + tune_ids = list(np.unique(batch_effects[:, merge_batch_dim])) + + X_dummy, batch_effects_dummy = self.hbr.create_dummy_inputs(X_dummy_ranges) + + for idx in tune_ids: + X_dummy = X_dummy[batch_effects_dummy[:, merge_batch_dim] != idx, :] + batch_effects_dummy = batch_effects_dummy[batch_effects_dummy[:, merge_batch_dim] != idx, :] + + X_dummy, batch_effects_dummy, Y_dummy = self.hbr.generate(X_dummy, + batch_effects_dummy, samples) + + if informative_prior: + self.hbr.adapt(np.concatenate((X_dummy, X)), + np.concatenate((Y_dummy, y)), + np.concatenate((batch_effects_dummy, batch_effects))) + else: + self.hbr.estimate(np.concatenate((X_dummy, X)), + np.concatenate((Y_dummy, y)), + np.concatenate((batch_effects_dummy, batch_effects))) + + return self + + def merge(self, nm, X_dummy_ranges=[[0.1, 0.9, 0.01]], merge_batch_dim=0, + samples=10): + + X_dummy1, batch_effects_dummy1 = self.hbr.create_dummy_inputs(X_dummy_ranges) + X_dummy2, batch_effects_dummy2 = nm.hbr.create_dummy_inputs(X_dummy_ranges) + + X_dummy1, batch_effects_dummy1, Y_dummy1 = self.hbr.generate(X_dummy1, + batch_effects_dummy1, samples) + X_dummy2, batch_effects_dummy2, Y_dummy2 = nm.hbr.generate(X_dummy2, + batch_effects_dummy2, samples) + + batch_effects_dummy2[:, merge_batch_dim] = batch_effects_dummy2[:, merge_batch_dim] + \ + np.max(batch_effects_dummy1[:, merge_batch_dim]) + 1 + + self.hbr.estimate(np.concatenate((X_dummy1, X_dummy2)), + np.concatenate((Y_dummy1, Y_dummy2)), + np.concatenate((batch_effects_dummy1, + batch_effects_dummy2))) + + return self + + def generate(self, X, batch_effects, samples=10): + + X, batch_effects, generated_samples = self.hbr.generate(X, batch_effects, + samples) + return X, batch_effects, generated_samples diff --git a/build/lib/pcntoolkit/normative_model/norm_np.py b/build/lib/pcntoolkit/normative_model/norm_np.py new file mode 100644 index 00000000..7b632522 --- /dev/null +++ b/build/lib/pcntoolkit/normative_model/norm_np.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Nov 22 14:41:07 2019 + +@author: seykia +""" + +from __future__ import print_function +from __future__ import division + +import os +import sys +import numpy as np +import torch +from torch import nn, optim +from torch.nn import functional as F +from sklearn.linear_model import LinearRegression +from sklearn.preprocessing import MinMaxScaler +import pickle + +try: # run as a package if installed + from pcntoolkit.normative_model.normbase import NormBase + from pcntoolkit.model.NPR import NPR, np_loss +except ImportError: + pass + + path = os.path.abspath(os.path.dirname(__file__)) + if path not in sys.path: + sys.path.append(path) + del path + + from model.NPR import NPR, np_loss + from norm_base import NormBase + +class struct(object): + pass + +class Encoder(nn.Module): + def __init__(self, x, y, args): + super(Encoder, self).__init__() + self.r_dim = args.r_dim + self.z_dim = args.z_dim + self.hidden_neuron_num = args.hidden_neuron_num + self.h_1 = nn.Linear(x.shape[1] + y.shape[1], self.hidden_neuron_num) + self.h_2 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) + self.h_3 = nn.Linear(self.hidden_neuron_num, self.r_dim) + + def forward(self, x, y): + x_y = torch.cat([x, y], dim=2) + x_y = F.relu(self.h_1(x_y)) + x_y = F.relu(self.h_2(x_y)) + x_y = F.relu(self.h_3(x_y)) + r = torch.mean(x_y, dim=1) + return r + + +class Decoder(nn.Module): + def __init__(self, x, y, args): + super(Decoder, self).__init__() + self.r_dim = args.r_dim + self.z_dim = args.z_dim + self.hidden_neuron_num = args.hidden_neuron_num + + self.g_1 = nn.Linear(self.z_dim, self.hidden_neuron_num) + self.g_2 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) + self.g_3 = nn.Linear(self.hidden_neuron_num, y.shape[1]) + + self.g_1_84 = nn.Linear(self.z_dim, self.hidden_neuron_num) + self.g_2_84 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) + self.g_3_84 = nn.Linear(self.hidden_neuron_num, y.shape[1]) + + def forward(self, z_sample): + z_hat = F.relu(self.g_1(z_sample)) + z_hat = F.relu(self.g_2(z_hat)) + y_hat = torch.sigmoid(self.g_3(z_hat)) + + z_hat_84 = F.relu(self.g_1(z_sample)) + z_hat_84 = F.relu(self.g_2_84(z_hat_84)) + y_hat_84 = torch.sigmoid(self.g_3_84(z_hat_84)) + + return y_hat, y_hat_84 + + + + +class NormNP(NormBase): + """ Classical GPR-based normative modelling approach + """ + + def __init__(self, X, y, configparam=None): + self.configparam = configparam + if configparam is not None: + with open(configparam, 'rb') as handle: + config = pickle.load(handle) + args = struct() + if 'batch_size' in config: + args.batch_size = config['batch_size'] + else: + args.batch_size = 10 + if 'epochs' in config: + args.epochs = config['epochs'] + else: + args.epochs = 100 + if 'device' in config: + args.device = config['device'] + else: + args.device = torch.device('cpu') + if 'm' in config: + args.m = config['m'] + else: + args.m = 200 + if 'hidden_neuron_num' in config: + args.hidden_neuron_num = config['hidden_neuron_num'] + else: + args.hidden_neuron_num = 10 + if 'r_dim' in config: + args.r_dim = config['r_dim'] + else: + args.r_dim = 5 + if 'z_dim' in config: + args.z_dim = config['z_dim'] + else: + args.z_dim = 3 + if 'nv' in config: + args.nv = config['nv'] + else: + args.nv = 0.01 + else: + args = struct() + args.batch_size = 10 + args.epochs = 100 + args.device = torch.device('cpu') + args.m = 200 + args.hidden_neuron_num = 10 + args.r_dim = 5 + args.z_dim = 3 + args.nv = 0.01 + + if y is not None: + if y.ndim == 1: + y = y.reshape(-1,1) + self.args = args + self.encoder = Encoder(X, y, args) + self.decoder = Decoder(X, y, args) + self.model = NPR(self.encoder, self.decoder, args) + + + @property + def n_params(self): + return 1 + + @property + def neg_log_lik(self): + return -1 + + def estimate(self, X, y): + if y.ndim == 1: + y = y.reshape(-1,1) + sample_num = X.shape[0] + batch_size = self.args.batch_size + factor_num = self.args.m + mini_batch_num = int(np.floor(sample_num/batch_size)) + device = self.args.device + + self.scaler = MinMaxScaler() + y = self.scaler.fit_transform(y) + + self.reg = [] + for i in range(factor_num): + self.reg.append(LinearRegression()) + idx = np.random.randint(0, sample_num, sample_num)#int(sample_num/10)) + self.reg[i].fit(X[idx,:],y[idx,:]) + + x_context = np.zeros([sample_num, factor_num, X.shape[1]]) + y_context = np.zeros([sample_num, factor_num, 1]) + + s = X.std(axis=0) + for j in range(factor_num): + x_context[:,j,:] = X + np.sqrt(self.args.nv) * s * np.random.randn(X.shape[0], X.shape[1]) + y_context[:,j,:] = self.reg[j].predict(x_context[:,j,:]) + + x_context = torch.tensor(x_context, device=device, dtype = torch.float) + y_context = torch.tensor(y_context, device=device, dtype = torch.float) + + x_all = torch.tensor(np.expand_dims(X,axis=1), device=device, dtype = torch.float) + y_all = torch.tensor(y.reshape(-1, 1, y.shape[1]), device=device, dtype = torch.float) + + self.model.train() + epochs = [int(self.args.epochs/4),int(self.args.epochs/2),int(self.args.epochs/5), + int(self.args.epochs-self.args.epochs/4-self.args.epochs/2-self.args.epochs/5)] + k = 1 + for e in range(len(epochs)): + optimizer = optim.Adam(self.model.parameters(), lr=10**(-e-2)) + for j in range(epochs[e]): + train_loss = 0 + for i in range(mini_batch_num): + optimizer.zero_grad() + idx = np.arange(i*batch_size,(i+1)*batch_size) + y_hat, y_hat_84, z_all, z_context, dummy, dummy = self.model(x_context[idx,:,:], y_context[idx,:,:], x_all[idx,:,:], y_all[idx,:,:]) + loss = np_loss(y_hat, y_hat_84, y_all[idx,0,:], z_all, z_context) + loss.backward() + train_loss += loss.item() + optimizer.step() + print('Epoch: %d, Loss:%f' %( k, train_loss)) + k += 1 + return self + + def predict(self, Xs, X=None, Y=None, theta=None): + sample_num = Xs.shape[0] + factor_num = self.args.m + x_context_test = np.zeros([sample_num, factor_num, Xs.shape[1]]) + y_context_test = np.zeros([sample_num, factor_num, 1]) + for j in range(factor_num): + x_context_test[:,j,:] = Xs + y_context_test[:,j,:] = self.reg[j].predict(x_context_test[:,j,:]) + x_context_test = torch.tensor(x_context_test, device=self.args.device, dtype = torch.float) + y_context_test = torch.tensor(y_context_test, device=self.args.device, dtype = torch.float) + self.model.eval() + with torch.no_grad(): + y_hat, y_hat_84, z_all, z_context, y_sigma, y_sigma_84 = self.model(x_context_test, y_context_test, n = 100) + + y_hat = self.scaler.inverse_transform(y_hat.cpu().numpy()) + y_hat_84 = self.scaler.inverse_transform(y_hat_84.cpu().numpy()) + y_sigma = y_sigma.cpu().numpy() * (self.scaler.data_max_ - self.scaler.data_min_) + y_sigma_84 = y_sigma_84.cpu().numpy() * (self.scaler.data_max_ - self.scaler.data_min_) + sigma_al = y_hat - y_hat_84 + return y_hat.squeeze(), (y_sigma**2 + sigma_al**2).squeeze() #, z_context[0].cpu().numpy(), z_context[1].cpu().numpy() + \ No newline at end of file diff --git a/build/lib/pcntoolkit/normative_model/norm_rfa.py b/build/lib/pcntoolkit/normative_model/norm_rfa.py new file mode 100644 index 00000000..f60e731a --- /dev/null +++ b/build/lib/pcntoolkit/normative_model/norm_rfa.py @@ -0,0 +1,72 @@ +from __future__ import print_function +from __future__ import division + +import os +import sys +import numpy as np + +try: # run as a package if installed + from pcntoolkit.normative_model.norm_base import NormBase + from pcntoolkit.model.rfa import GPRRFA +except ImportError: + pass + + path = os.path.abspath(os.path.dirname(__file__)) + if path not in sys.path: + sys.path.append(path) + del path + + from model.rfa import GPRRFA + from norm_base import NormBase + +class NormRFA(NormBase): + """ Classical GPR-based normative modelling approach + """ + + def __init__(self, X, y=None, theta=None, n_feat=None): + + if (X is not None): + if n_feat is None: + print("initialising RFA") + else: + print("initialising RFA with", n_feat, "random features") + self.gprrfa = GPRRFA(theta, X, n_feat=n_feat) + self._n_params = self.gprrfa.get_n_params(X) + else: + raise(ValueError, 'please specify covariates') + return + + if theta is None: + self.theta0 = np.zeros(self._n_params) + else: + if len(theta) == self._n_params: + self.theta0 = theta + else: + raise(ValueError, 'hyperparameter vector has incorrect size') + + self.theta = self.theta0 + + @property + def n_params(self): + + return self._n_params + + @property + def neg_log_lik(self): + return self.gprrfa.nlZ + + def estimate(self, X, y, theta=None): + if theta is None: + theta = self.theta0 + self.gprrfa = GPRRFA(theta, X, y) + self.theta = self.gprrfa.estimate(theta, X, y) + + return self + + def predict(self, Xs, X, y, theta=None): + if theta is None: + theta = self.theta + yhat, s2 = self.gprrfa.predict(theta, X, y, Xs) + + return yhat, s2 + \ No newline at end of file diff --git a/build/lib/pcntoolkit/normative_model/norm_utils.py b/build/lib/pcntoolkit/normative_model/norm_utils.py new file mode 100644 index 00000000..48e79980 --- /dev/null +++ b/build/lib/pcntoolkit/normative_model/norm_utils.py @@ -0,0 +1,28 @@ +try: # run as a package if installed + from pcntoolkit.normative_model.norm_blr import NormBLR + from pcntoolkit.normative_model.norm_gpr import NormGPR + from pcntoolkit.normative_model.norm_rfa import NormRFA + from pcntoolkit.normative_model.norm_hbr import NormHBR + from pcntoolkit.normative_model.norm_np import NormNP +except: + from norm_blr import NormBLR + from norm_gpr import NormGPR + from norm_rfa import NormRFA + from norm_hbr import NormHBR + from norm_np import NormNP + +def norm_init(X, y=None, theta=None, alg='gpr', **kwargs): + if alg == 'gpr': + nm = NormGPR(X=X, y=y, theta=theta, **kwargs) + elif alg =='blr': + nm = NormBLR(X=X, y=y, theta=theta, **kwargs) + elif alg == 'rfa': + nm = NormRFA(X=X, y=y, theta=theta, **kwargs) + elif alg == 'hbr': + nm = NormHBR(**kwargs) + elif alg == 'np': + nm = NormNP(X=X, y=y, **kwargs) + else: + raise(ValueError, "Algorithm " + alg + " not known.") + + return nm \ No newline at end of file diff --git a/build/lib/pcntoolkit/normative_parallel.py b/build/lib/pcntoolkit/normative_parallel.py new file mode 100644 index 00000000..8c549b67 --- /dev/null +++ b/build/lib/pcntoolkit/normative_parallel.py @@ -0,0 +1,1275 @@ +#!/.../anaconda/bin/python/ + +# ----------------------------------------------------------------------------- +# Run parallel normative modelling. +# All processing takes place in the processing directory (processing_dir) +# All inputs should be text files or binaries and space seperated +# +# It is possible to run these functions using... +# +# * k-fold cross-validation +# * estimating a training dataset then applying to a second test dataset +# +# First,the data is split for parallel processing. +# Second, the splits are submitted to the cluster. +# Third, the output is collected and combined. +# +# witten by (primarily) T Wolfers, (adaptated) SM Kia, H Huijsdens, L Parks, +# S Rutherford, AF Marquand +# ----------------------------------------------------------------------------- + +from __future__ import print_function +from __future__ import division + +import os +import sys +import glob +import shutil +import pickle +import fileinput +import time +import numpy as np +import pandas as pd +from subprocess import call, check_output + + +try: + import pcntoolkit as ptk + import pcntoolkit.dataio.fileio as fileio + from pcntoolkit import configs + from pcntoolkit.util.utils import yes_or_no + ptkpath = ptk.__path__[0] +except ImportError: + pass + ptkpath = os.path.abspath(os.path.dirname(__file__)) + if ptkpath not in sys.path: + sys.path.append(ptkpath) + import dataio.fileio as fileio + import configs + from util.utils import yes_or_no + + +PICKLE_PROTOCOL = configs.PICKLE_PROTOCOL + + +def execute_nm(processing_dir, + python_path, + job_name, + covfile_path, + respfile_path, + batch_size, + memory, + duration, + normative_path=None, + func='estimate', + interactive=False, + **kwargs): + + ''' Execute parallel normative models + This function is a mother function that executes all parallel normative + modelling routines. Different specifications are possible using the sub- + functions. + + Basic usage:: + + execute_nm(processing_dir, python_path, job_name, covfile_path, respfile_path, batch_size, memory, duration) + + :param processing_dir: Full path to the processing dir + :param python_path: Full path to the python distribution + :param normative_path: Full path to the normative.py. If None (default) then it will automatically retrieves the path from the installed packeage. + :param job_name: Name for the bash script that is the output of this function + :param covfile_path: Full path to a .txt file that contains all covariats (subjects x covariates) for the responsefile + :param respfile_path: Full path to a .txt that contains all features (subjects x features) + :param batch_size: Number of features in each batch + :param memory: Memory requirements written as string for example 4gb or 500mb + :param duation: The approximate duration of the job, a string with HH:MM:SS for example 01:01:01 + :param cv_folds: Number of cross validations + :param testcovfile_path: Full path to a .txt file that contains all covariates (subjects x covariates) for the test response file + :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 + + written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. + ''' + + if normative_path is None: + normative_path = ptkpath + '/normative.py' + + cv_folds = kwargs.get('cv_folds', None) + testcovfile_path = kwargs.get('testcovfile_path', None) + testrespfile_path= kwargs.get('testrespfile_path', None) + outputsuffix = kwargs.get('outputsuffix', 'estimate') + cluster_spec = kwargs.pop('cluster_spec', 'torque') + log_path = kwargs.get('log_path', None) + binary = kwargs.pop('binary', False) + + split_nm(processing_dir, + respfile_path, + batch_size, + binary, + **kwargs) + + batch_dir = glob.glob(processing_dir + 'batch_*') + # print(batch_dir) + number_of_batches = len(batch_dir) + # print(number_of_batches) + + if binary: + file_extentions = '.pkl' + else: + file_extentions = '.txt' + + kwargs.update({'batch_size':str(batch_size)}) + job_ids = [] + for n in range(1, number_of_batches+1): + kwargs.update({'job_id':str(n)}) + if testrespfile_path is not None: + if cv_folds is not None: + raise(ValueError, """If the response file is specified + cv_folds must be equal to None""") + else: + # specified train/test split + batch_processing_dir = processing_dir + 'batch_' + str(n) + '/' + batch_job_name = job_name + '_' + str(n) + '.sh' + batch_respfile_path = (batch_processing_dir + 'resp_batch_' + + str(n) + file_extentions) + batch_testrespfile_path = (batch_processing_dir + + 'testresp_batch_' + + str(n) + file_extentions) + batch_job_path = batch_processing_dir + batch_job_name + if cluster_spec == 'torque': + + # update the response file + kwargs.update({'testrespfile_path': \ + batch_testrespfile_path}) + bashwrap_nm(batch_processing_dir, + python_path, + normative_path, + batch_job_name, + covfile_path, + batch_respfile_path, + func=func, + **kwargs) + job_id = qsub_nm(job_path=batch_job_path, + log_path=log_path, + memory=memory, + duration=duration) + job_ids.append(job_id) + elif cluster_spec == 'sbatch': + # update the response file + kwargs.update({'testrespfile_path': \ + batch_testrespfile_path}) + sbatchwrap_nm(batch_processing_dir, + python_path, + normative_path, + batch_job_name, + covfile_path, + batch_respfile_path, + func=func, + memory=memory, + duration=duration, + **kwargs) + sbatch_nm(job_path=batch_job_path, + log_path=log_path) + elif cluster_spec == 'new': + # this part requires addition in different envioronment [ + sbatchwrap_nm(processing_dir=batch_processing_dir, + func=func, **kwargs) + sbatch_nm(processing_dir=batch_processing_dir) + # ] + if testrespfile_path is None: + if testcovfile_path is not None: + # forward model + batch_processing_dir = processing_dir + 'batch_' + str(n) + '/' + batch_job_name = job_name + '_' + str(n) + '.sh' + batch_respfile_path = (batch_processing_dir + 'resp_batch_' + + str(n) + file_extentions) + batch_job_path = batch_processing_dir + batch_job_name + if cluster_spec == 'torque': + bashwrap_nm(batch_processing_dir, + python_path, + normative_path, + batch_job_name, + covfile_path, + batch_respfile_path, + func=func, + **kwargs) + job_id = qsub_nm(job_path=batch_job_path, + log_path=log_path, + memory=memory, + duration=duration) + job_ids.append(job_id) + elif cluster_spec == 'sbatch': + sbatchwrap_nm(batch_processing_dir, + python_path, + normative_path, + batch_job_name, + covfile_path, + batch_respfile_path, + func=func, + memory=memory, + duration=duration, + **kwargs) + sbatch_nm(job_path=batch_job_path, + log_path=log_path) + elif cluster_spec == 'new': + # this part requires addition in different envioronment [ + bashwrap_nm(processing_dir=batch_processing_dir, func=func, + **kwargs) + qsub_nm(processing_dir=batch_processing_dir) + # ] + else: + # cross-validation + batch_processing_dir = (processing_dir + 'batch_' + + str(n) + '/') + batch_job_name = job_name + '_' + str(n) + '.sh' + batch_respfile_path = (batch_processing_dir + + 'resp_batch_' + str(n) + + file_extentions) + batch_job_path = batch_processing_dir + batch_job_name + if cluster_spec == 'torque': + bashwrap_nm(batch_processing_dir, + python_path, + normative_path, + batch_job_name, + covfile_path, + batch_respfile_path, + func=func, + **kwargs) + job_id = qsub_nm(job_path=batch_job_path, + log_path=log_path, + memory=memory, + duration=duration) + job_ids.append(job_id) + elif cluster_spec == 'sbatch': + sbatchwrap_nm(batch_processing_dir, + python_path, + normative_path, + batch_job_name, + covfile_path, + batch_respfile_path, + func=func, + memory=memory, + duration=duration, + **kwargs) + sbatch_nm(job_path=batch_job_path, + log_path=log_path) + elif cluster_spec == 'new': + # this part requires addition in different envioronment [ + bashwrap_nm(processing_dir=batch_processing_dir, func=func, + **kwargs) + qsub_nm(processing_dir=batch_processing_dir) + # ] + + if interactive: + + check_jobs(job_ids, delay=60) + + success = False + while (not success): + success = collect_nm(processing_dir, + job_name, + func=func, + collect=False, + binary=binary, + batch_size=batch_size, + outputsuffix=outputsuffix) + if success: + break + else: + if interactive == 'query': + response = yes_or_no('Rerun the failed jobs?') + if response: + rerun_nm(processing_dir, log_path=log_path, memory=memory, + duration=duration, binary=binary, + 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 interactive == 'query': + response = yes_or_no('Collect the results?') + if response: + success = collect_nm(processing_dir, + job_name, + func=func, + collect=True, + binary=binary, + batch_size=batch_size, + outputsuffix=outputsuffix) + else: + print('Collecting the results ...') + success = collect_nm(processing_dir, + job_name, + func=func, + collect=True, + binary=binary, + batch_size=batch_size, + outputsuffix=outputsuffix) + + +"""routines that are environment independent""" + +def split_nm(processing_dir, + respfile_path, + batch_size, + binary, + **kwargs): + + ''' This function prepares the input files for normative_parallel. + + Basic usage:: + + split_nm(processing_dir, respfile_path, batch_size, binary, testrespfile_path) + + :param processing_dir: Full path to the processing dir + :param respfile_path: Full path to the responsefile.txt (subjects x features) + :param batch_size: Number of features in each batch + :param testrespfile_path: Full path to the test responsefile.txt (subjects x features) + :param binary: If True binary file + + :outputs: The creation of a folder struture for batch-wise processing. + + witten by (primarily) T Wolfers (adapted) SM Kia, (adapted) S Rutherford. + ''' + + testrespfile_path = kwargs.pop('testrespfile_path', None) + + dummy, respfile_extension = os.path.splitext(respfile_path) + if (binary and respfile_extension != '.pkl'): + raise(ValueError, """If binary is True the file format for the + testrespfile file must be .pkl""") + elif (binary==False and respfile_extension != '.txt'): + raise(ValueError, """If binary is False the file format for the + testrespfile file must be .txt""") + + # splits response into batches + if testrespfile_path is None: + if (binary==False): + respfile = fileio.load_ascii(respfile_path) + else: + respfile = pd.read_pickle(respfile_path) + + respfile = pd.DataFrame(respfile) + + numsub = respfile.shape[1] + batch_vec = np.arange(0, + numsub, + batch_size) + batch_vec = np.append(batch_vec, + numsub) + + for n in range(0, (len(batch_vec) - 1)): + resp_batch = respfile.iloc[:, (batch_vec[n]): batch_vec[n + 1]] + os.chdir(processing_dir) + resp = str('resp_batch_' + str(n+1)) + batch = str('batch_' + str(n+1)) + if not os.path.exists(processing_dir + batch): + os.makedirs(processing_dir + batch) + os.makedirs(processing_dir + batch + '/Models/') + if (binary==False): + fileio.save_pd(resp_batch, + processing_dir + batch + '/' + + resp + '.txt') + else: + resp_batch.to_pickle(processing_dir + batch + '/' + + resp + '.pkl', protocol=PICKLE_PROTOCOL) + + # splits response and test responsefile into batches + else: + dummy, testrespfile_extension = os.path.splitext(testrespfile_path) + if (binary and testrespfile_extension != '.pkl'): + raise(ValueError, """If binary is True the file format for the + testrespfile file must be .pkl""") + elif(binary==False and testrespfile_extension != '.txt'): + raise(ValueError, """If binary is False the file format for the + testrespfile file must be .txt""") + + if (binary==False): + respfile = fileio.load_ascii(respfile_path) + testrespfile = fileio.load_ascii(testrespfile_path) + else: + respfile = pd.read_pickle(respfile_path) + testrespfile = pd.read_pickle(testrespfile_path) + + respfile = pd.DataFrame(respfile) + testrespfile = pd.DataFrame(testrespfile) + + numsub = respfile.shape[1] + batch_vec = np.arange(0, numsub, + batch_size) + batch_vec = np.append(batch_vec, + numsub) + for n in range(0, (len(batch_vec) - 1)): + resp_batch = respfile.iloc[:, (batch_vec[n]): batch_vec[n + 1]] + testresp_batch = testrespfile.iloc[:, (batch_vec[n]): batch_vec[n + + 1]] + os.chdir(processing_dir) + resp = str('resp_batch_' + str(n+1)) + testresp = str('testresp_batch_' + str(n+1)) + batch = str('batch_' + str(n+1)) + if not os.path.exists(processing_dir + batch): + os.makedirs(processing_dir + batch) + os.makedirs(processing_dir + batch + '/Models/') + if (binary==False): + fileio.save_pd(resp_batch, + processing_dir + batch + '/' + + resp + '.txt') + fileio.save_pd(testresp_batch, + processing_dir + batch + '/' + testresp + + '.txt') + else: + resp_batch.to_pickle(processing_dir + batch + '/' + + resp + '.pkl', protocol=PICKLE_PROTOCOL) + testresp_batch.to_pickle(processing_dir + batch + '/' + + testresp + '.pkl', + protocol=PICKLE_PROTOCOL) + + +def collect_nm(processing_dir, + job_name, + func='estimate', + collect=False, + binary=False, + batch_size=None, + outputsuffix='_estimate'): + + '''Function to checks and collects all batches. + + Basic usage:: + + collect_nm(processing_dir, job_name) + + + :param processing_dir: Full path to the processing directory + :param collect: If True data is checked for failed batches and collected; if False data is just checked + :param binary: Results in pkl format + + :outputs: Text files containing all results accross all batches the combined output (written to disk). + + :returns 0: if batches fail + :returns 1: if bathches complete successfully + + written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. + ''' + + if binary: + file_extentions = '.pkl' + else: + file_extentions = '.txt' + + # detect number of subjects, batches, hyperparameters and CV + batches = glob.glob(processing_dir + 'batch_*/') + + count = 0 + batch_fail = [] + + if (func!='fit' and func!='extend' and func!='merge' and func!='tune'): + 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 + + file_extentions) + else: + break + if binary is False: + file_example = fileio.load(file_example[0]) + else: + file_example = pd.read_pickle(file_example[0]) + numsubjects = file_example.shape[0] + batch_size = file_example.shape[1] + + # artificially creates files for batches that were not executed + batch_dirs = glob.glob(processing_dir + 'batch_*/') + batch_dirs = fileio.sort_nicely(batch_dirs) + for batch in batch_dirs: + filepath = glob.glob(batch + 'yhat' + outputsuffix + '*') + if filepath == []: + count = count+1 + batch1 = glob.glob(batch + '/' + job_name + '*.sh') + print(batch1) + batch_fail.append(batch1) + if collect is True: + pRho = np.ones(batch_size) + pRho = pRho.transpose() + pRho = pd.Series(pRho) + fileio.save(pRho, batch + 'pRho' + outputsuffix + + file_extentions) + + Rho = np.zeros(batch_size) + Rho = Rho.transpose() + Rho = pd.Series(Rho) + fileio.save(Rho, batch + 'Rho' + outputsuffix + + file_extentions) + + rmse = np.zeros(batch_size) + rmse = rmse.transpose() + rmse = pd.Series(rmse) + fileio.save(rmse, batch + 'RMSE' + outputsuffix + + file_extentions) + + smse = np.zeros(batch_size) + smse = smse.transpose() + smse = pd.Series(smse) + fileio.save(smse, batch + 'SMSE' + outputsuffix + + file_extentions) + + expv = np.zeros(batch_size) + expv = expv.transpose() + expv = pd.Series(expv) + fileio.save(expv, batch + 'EXPV' + outputsuffix + + file_extentions) + + msll = np.zeros(batch_size) + msll = msll.transpose() + msll = pd.Series(msll) + fileio.save(msll, batch + 'MSLL' + outputsuffix + + file_extentions) + + yhat = np.zeros([numsubjects, batch_size]) + yhat = pd.DataFrame(yhat) + fileio.save(yhat, batch + 'yhat' + outputsuffix + + file_extentions) + + ys2 = np.zeros([numsubjects, batch_size]) + ys2 = pd.DataFrame(ys2) + fileio.save(ys2, batch + 'ys2' + outputsuffix + + file_extentions) + + Z = np.zeros([numsubjects, batch_size]) + Z = pd.DataFrame(Z) + fileio.save(Z, batch + 'Z' + outputsuffix + + file_extentions) + + nll = np.zeros(batch_size) + nll = nll.transpose() + nll = pd.Series(nll) + fileio.save(nll, batch + 'NLL' + outputsuffix + + file_extentions) + + bic = np.zeros(batch_size) + bic = bic.transpose() + bic = pd.Series(bic) + fileio.save(bic, batch + 'BIC' + outputsuffix + + file_extentions) + + if not os.path.isdir(batch + 'Models'): + os.mkdir('Models') + + + else: # if more than 10% of yhat is nan then it is a failed batch + yhat = fileio.load(filepath[0]) + if np.count_nonzero(~np.isnan(yhat))/(np.prod(yhat.shape))<0.9: + count = count+1 + batch1 = glob.glob(batch + '/' + job_name + '*.sh') + print('More than 10% nans in '+ batch1[0]) + batch_fail.append(batch1) + + else: + batch_dirs = glob.glob(processing_dir + 'batch_*/') + batch_dirs = fileio.sort_nicely(batch_dirs) + for batch in batch_dirs: + filepath = glob.glob(batch + 'Models/' + 'NM_' + '*' + outputsuffix + + '*') + if len(filepath) < batch_size: + count = count+1 + batch1 = glob.glob(batch + '/' + job_name + '*.sh') + print(batch1) + batch_fail.append(batch1) + + # combines all output files across batches + if collect is True: + pRho_filenames = glob.glob(processing_dir + 'batch_*/' + 'pRho' + + outputsuffix + '*') + if pRho_filenames: + pRho_filenames = fileio.sort_nicely(pRho_filenames) + pRho_dfs = [] + for pRho_filename in pRho_filenames: + pRho_dfs.append(pd.DataFrame(fileio.load(pRho_filename))) + pRho_dfs = pd.concat(pRho_dfs, ignore_index=True, axis=0) + fileio.save(pRho_dfs, processing_dir + 'pRho' + outputsuffix + + file_extentions) + del pRho_dfs + + Rho_filenames = glob.glob(processing_dir + 'batch_*/' + 'Rho' + + outputsuffix + '*') + if Rho_filenames: + Rho_filenames = fileio.sort_nicely(Rho_filenames) + Rho_dfs = [] + for Rho_filename in Rho_filenames: + Rho_dfs.append(pd.DataFrame(fileio.load(Rho_filename))) + Rho_dfs = pd.concat(Rho_dfs, ignore_index=True, axis=0) + fileio.save(Rho_dfs, processing_dir + 'Rho' + outputsuffix + + file_extentions) + del Rho_dfs + + Z_filenames = glob.glob(processing_dir + 'batch_*/' + 'Z' + + outputsuffix + '*') + if Z_filenames: + Z_filenames = fileio.sort_nicely(Z_filenames) + Z_dfs = [] + for Z_filename in Z_filenames: + Z_dfs.append(pd.DataFrame(fileio.load(Z_filename))) + Z_dfs = pd.concat(Z_dfs, ignore_index=True, axis=1) + fileio.save(Z_dfs, processing_dir + 'Z' + outputsuffix + + file_extentions) + del Z_dfs + + yhat_filenames = glob.glob(processing_dir + 'batch_*/' + 'yhat' + + outputsuffix + '*') + if yhat_filenames: + yhat_filenames = fileio.sort_nicely(yhat_filenames) + yhat_dfs = [] + for yhat_filename in yhat_filenames: + yhat_dfs.append(pd.DataFrame(fileio.load(yhat_filename))) + yhat_dfs = pd.concat(yhat_dfs, ignore_index=True, axis=1) + fileio.save(yhat_dfs, processing_dir + 'yhat' + outputsuffix + + file_extentions) + del yhat_dfs + + ys2_filenames = glob.glob(processing_dir + 'batch_*/' + 'ys2' + + outputsuffix + '*') + if ys2_filenames: + ys2_filenames = fileio.sort_nicely(ys2_filenames) + ys2_dfs = [] + for ys2_filename in ys2_filenames: + ys2_dfs.append(pd.DataFrame(fileio.load(ys2_filename))) + ys2_dfs = pd.concat(ys2_dfs, ignore_index=True, axis=1) + fileio.save(ys2_dfs, processing_dir + 'ys2' + outputsuffix + + file_extentions) + del ys2_dfs + + rmse_filenames = glob.glob(processing_dir + 'batch_*/' + 'RMSE' + + outputsuffix + '*') + if rmse_filenames: + rmse_filenames = fileio.sort_nicely(rmse_filenames) + rmse_dfs = [] + for rmse_filename in rmse_filenames: + rmse_dfs.append(pd.DataFrame(fileio.load(rmse_filename))) + rmse_dfs = pd.concat(rmse_dfs, ignore_index=True, axis=0) + fileio.save(rmse_dfs, processing_dir + 'RMSE' + outputsuffix + + file_extentions) + del rmse_dfs + + smse_filenames = glob.glob(processing_dir + 'batch_*/' + 'SMSE' + + outputsuffix + '*') + if smse_filenames: + smse_filenames = fileio.sort_nicely(smse_filenames) + smse_dfs = [] + for smse_filename in smse_filenames: + smse_dfs.append(pd.DataFrame(fileio.load(smse_filename))) + smse_dfs = pd.concat(smse_dfs, ignore_index=True, axis=0) + fileio.save(smse_dfs, processing_dir + 'SMSE' + outputsuffix + + file_extentions) + del smse_dfs + + expv_filenames = glob.glob(processing_dir + 'batch_*/' + 'EXPV' + + outputsuffix + '*') + if expv_filenames: + expv_filenames = fileio.sort_nicely(expv_filenames) + expv_dfs = [] + for expv_filename in expv_filenames: + expv_dfs.append(pd.DataFrame(fileio.load(expv_filename))) + expv_dfs = pd.concat(expv_dfs, ignore_index=True, axis=0) + fileio.save(expv_dfs, processing_dir + 'EXPV' + outputsuffix + + file_extentions) + del expv_dfs + + msll_filenames = glob.glob(processing_dir + 'batch_*/' + 'MSLL' + + outputsuffix + '*') + if msll_filenames: + msll_filenames = fileio.sort_nicely(msll_filenames) + msll_dfs = [] + for msll_filename in msll_filenames: + msll_dfs.append(pd.DataFrame(fileio.load(msll_filename))) + msll_dfs = pd.concat(msll_dfs, ignore_index=True, axis=0) + fileio.save(msll_dfs, processing_dir + 'MSLL' + outputsuffix + + file_extentions) + del msll_dfs + + nll_filenames = glob.glob(processing_dir + 'batch_*/' + 'NLL' + + outputsuffix + '*') + if nll_filenames: + nll_filenames = fileio.sort_nicely(nll_filenames) + nll_dfs = [] + for nll_filename in nll_filenames: + nll_dfs.append(pd.DataFrame(fileio.load(nll_filename))) + nll_dfs = pd.concat(nll_dfs, ignore_index=True, axis=0) + fileio.save(nll_dfs, processing_dir + 'NLL' + outputsuffix + + file_extentions) + del nll_dfs + + bic_filenames = glob.glob(processing_dir + 'batch_*/' + 'BIC' + + outputsuffix + '*') + if bic_filenames: + bic_filenames = fileio.sort_nicely(bic_filenames) + bic_dfs = [] + for bic_filename in bic_filenames: + bic_dfs.append(pd.DataFrame(fileio.load(bic_filename))) + bic_dfs = pd.concat(bic_dfs, ignore_index=True, axis=0) + fileio.save(bic_dfs, processing_dir + 'BIC' + outputsuffix + + file_extentions) + del bic_dfs + + if (func!='predict' and func!='extend' and func!='merge' and func!='tune'): + if not os.path.isdir(processing_dir + 'Models') and \ + os.path.exists(os.path.join(batches[0], 'Models')): + os.mkdir(processing_dir + 'Models') + + meta_filenames = glob.glob(processing_dir + 'batch_*/Models/' + + 'meta_data.md') + mY = [] + sY = [] + X_scalers = [] + Y_scalers = [] + if meta_filenames: + meta_filenames = fileio.sort_nicely(meta_filenames) + with open(meta_filenames[0], 'rb') as file: + meta_data = pickle.load(file) + + for meta_filename in meta_filenames: + with open(meta_filename, 'rb') as file: + meta_data = pickle.load(file) + mY.append(meta_data['mean_resp']) + sY.append(meta_data['std_resp']) + if meta_data['inscaler'] in ['standardize', 'minmax', + 'robminmax']: + X_scalers.append(meta_data['scaler_cov']) + if meta_data['outscaler'] in ['standardize', 'minmax', + 'robminmax']: + Y_scalers.append(meta_data['scaler_resp']) + meta_data['mean_resp'] = np.squeeze(np.column_stack(mY)) + meta_data['std_resp'] = np.squeeze(np.column_stack(sY)) + meta_data['scaler_cov'] = X_scalers + meta_data['scaler_resp'] = Y_scalers + + with open(os.path.join(processing_dir, 'Models', + 'meta_data.md'), 'wb') as file: + pickle.dump(meta_data, file, protocol=PICKLE_PROTOCOL) + + batch_dirs = glob.glob(processing_dir + 'batch_*/') + if batch_dirs: + batch_dirs = fileio.sort_nicely(batch_dirs) + for b, batch_dir in enumerate(batch_dirs): + src_files = glob.glob(batch_dir + 'Models/NM*' + + outputsuffix + '.pkl') + if src_files: + src_files = fileio.sort_nicely(src_files) + for f, full_file_name in enumerate(src_files): + if os.path.isfile(full_file_name): + file_name = full_file_name.split('/')[-1] + n = file_name.split('_') + n[-2] = str(b * batch_size + f) + n = '_'.join(n) + shutil.copy(full_file_name, processing_dir + + 'Models/' + n) + elif func=='fit': + count = count+1 + batch1 = glob.glob(batch_dir + '/' + job_name + '*.sh') + print('Failed batch: ' + batch1[0]) + batch_fail.append(batch1) + + # list batches that were not executed + print('Number of batches that failed:' + str(count)) + batch_fail_df = pd.DataFrame(batch_fail) + if file_extentions == '.txt': + fileio.save_pd(batch_fail_df, processing_dir + 'failed_batches'+ + file_extentions) + else: + fileio.save(batch_fail_df, processing_dir + + 'failed_batches' + + file_extentions) + + if not batch_fail: + return True + else: + return False + +def delete_nm(processing_dir, + binary=False): + '''This function deletes all processing for normative modelling and just keeps the combined output. + + Basic usage:: + + collect_nm(processing_dir) + + :param processing_dir: Full path to the processing directory. + :param binary: Results in pkl format. + + written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. + ''' + + if binary: + file_extentions = '.pkl' + else: + file_extentions = '.txt' + for file in glob.glob(processing_dir + 'batch_*/'): + shutil.rmtree(file) + if os.path.exists(processing_dir + 'failed_batches' + file_extentions): + 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 + +def bashwrap_nm(processing_dir, + python_path, + normative_path, + job_name, + covfile_path, + respfile_path, + func='estimate', + **kwargs): + + ''' This function wraps normative modelling into a bash script to run it + on a torque cluster system. + + Basic usage:: + + bashwrap_nm(processing_dir, python_path, normative_path, job_name, covfile_path, respfile_path) + + :param processing_dir: Full path to the processing dir + :param python_path: Full path to the python distribution + :param normative_path: Full path to the normative.py + :param job_name: Name for the bash script that is the output of this function + :param covfile_path: Full path to a .txt file that contains all covariates (subjects x covariates) for the responsefile + :param respfile_path: Full path to a .txt that contains all features (subjects x features) + :param cv_folds: Number of cross validations + :param testcovfile_path: Full path to a .txt file that contains all covariates (subjects x covariates) for the testresponse file + :param testrespfile_path: Full path to a .txt file that contains all test features + :param alg: which algorithm to use + :param configparam: configuration parameters for this algorithm + + :outputs: A bash.sh file containing the commands for normative modelling saved to the processing directory (written to disk). + + written by (primarily) T Wolfers, (adapted) S Rutherford. + ''' + + # here we use pop not get to remove the arguments as they used + cv_folds = kwargs.pop('cv_folds',None) + testcovfile_path = kwargs.pop('testcovfile_path', None) + testrespfile_path = kwargs.pop('testrespfile_path', None) + alg = kwargs.pop('alg', None) + configparam = kwargs.pop('configparam', None) + # change to processing dir + os.chdir(processing_dir) + output_changedir = ['cd ' + processing_dir + '\n'] + + bash_lines = '#!/bin/bash\n' + bash_cores = 'export OMP_NUM_THREADS=1\n' + bash_environment = [bash_lines + bash_cores] + + # creates call of function for normative modelling + if (testrespfile_path is not None) and (testcovfile_path is not None): + job_call = [python_path + ' ' + normative_path + ' -c ' + + covfile_path + ' -t ' + testcovfile_path + ' -r ' + + testrespfile_path + ' -f ' + func] + elif (testrespfile_path is None) and (testcovfile_path is not None): + job_call = [python_path + ' ' + normative_path + ' -c ' + + covfile_path + ' -t ' + testcovfile_path + ' -f ' + func] + elif cv_folds is not None: + job_call = [python_path + ' ' + normative_path + ' -c ' + + covfile_path + ' -k ' + str(cv_folds) + ' -f ' + func] + elif func != 'estimate': + job_call = [python_path + ' ' + normative_path + ' -c ' + + covfile_path + ' -f ' + func] + else: + raise(ValueError, """For 'estimate' function either testcov or cvfold + must be specified.""") + + # add algorithm-specific parameters + if alg is not None: + job_call = [job_call[0] + ' -a ' + alg] + if configparam is not None: + job_call = [job_call[0] + ' -x ' + str(configparam)] + + # add standardization flag if it is false + # if not standardize: + # job_call = [job_call[0] + ' -s'] + + # add responses file + job_call = [job_call[0] + ' ' + respfile_path] + + # add in optional arguments. + for k in kwargs: + job_call = [job_call[0] + ' ' + k + '=' + kwargs[k]] + + # writes bash file into processing dir + with open(processing_dir+job_name, 'w') as bash_file: + bash_file.writelines(bash_environment + output_changedir + \ + job_call + ["\n"]) + + # changes permissoins for bash.sh file + os.chmod(processing_dir + job_name, 0o700) + + +def qsub_nm(job_path, + log_path, + memory, + duration): + + '''This function submits a job.sh scipt to the torque custer using the qsub command. + + Basic usage:: + + + qsub_nm(job_path, log_path, memory, duration) + + :param job_path: Full path to the job.sh file. + :param memory: Memory requirements written as string for example 4gb or 500mb. + :param duation: The approximate duration of the job, a string with HH:MM:SS for example 01:01:01. + + :outputs: Submission of the job to the (torque) cluster. + + written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. + ''' + + # created qsub command + if log_path is None: + qsub_call = ['echo ' + job_path + ' | qsub -N ' + job_path + ' -l ' + + 'procs=1' + ',mem=' + memory + ',walltime=' + duration] + else: + qsub_call = ['echo ' + job_path + ' | qsub -N ' + job_path + + ' -l ' + 'procs=1' + ',mem=' + memory + ',walltime=' + + duration + ' -o ' + log_path + ' -e ' + log_path] + + # submits job to cluster + #call(qsub_call, shell=True) + job_id = check_output(qsub_call, shell=True).decode(sys.stdout.encoding).replace("\n", "") + + return job_id + + +def rerun_nm(processing_dir, + log_path, + memory, + duration, + binary=False, + interactive=False): + '''This function reruns all failed batched in processing_dir after collect_nm has identified the failed batches. + Basic usage:: + + rerun_nm(processing_dir, log_path, memory, duration) + + :param processing_dir: Full path to the processing directory + :param memory: Memory requirements written as string for example 4gb or 500mb. + :param duration: The approximate duration of the job, a string with HH:MM:SS for example 01:01:01. + + written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. + ''' + + job_ids = [] + + + if binary: + file_extentions = '.pkl' + failed_batches = fileio.load(processing_dir + + 'failed_batches' + file_extentions) + shape = failed_batches.shape + for n in range(0, shape[0]): + jobpath = failed_batches[n, 0] + print(jobpath) + job_id = qsub_nm(job_path=jobpath, + log_path=log_path, + memory=memory, + duration=duration) + job_ids.append(job_id) + else: + file_extentions = '.txt' + failed_batches = fileio.load_pd(processing_dir + + 'failed_batches' + file_extentions) + shape = failed_batches.shape + for n in range(0, shape[0]): + jobpath = failed_batches.iloc[n, 0] + print(jobpath) + job_id = qsub_nm(job_path=jobpath, + log_path=log_path, + memory=memory, + duration=duration) + job_ids.append(job_id) + + if interactive: + check_jobs(job_ids, delay=60) + + +# COPY the rotines above here and aadapt those to your cluster +# bashwarp_nm; qsub_nm; rerun_nm + +def sbatchwrap_nm(processing_dir, + python_path, + normative_path, + job_name, + covfile_path, + respfile_path, + memory, + duration, + func='estimate', + **kwargs): + + '''This function wraps normative modelling into a bash script to run it + on a torque cluster system. + + Basic usage:: + + sbatchwrap_nm(processing_dir, python_path, normative_path, job_name, covfile_path, respfile_path, memory, duration) + + :param processing_dir: Full path to the processing dir + :param python_path: Full path to the python distribution + :param normative_path: Full path to the normative.py + :param job_name: Name for the bash script that is the output of this function + :param covfile_path: Full path to a .txt file that contains all covariates (subjects x covariates) for the responsefile + :param respfile_path: Full path to a .txt that contains all features (subjects x features) + :param cv_folds: Number of cross validations + :param testcovfile_path: Full path to a .txt file that contains all covariates (subjects x covariates) for the testresponse file + :param testrespfile_path: Full path to a .txt file that contains all test features + :param alg: which algorithm to use + :param configparam: configuration parameters for this algorithm + + :outputs: A bash.sh file containing the commands for normative modelling saved to the processing directory (written to disk). + + written by (primarily) T Wolfers, (adapted) S Rutherford + ''' + + # here we use pop not get to remove the arguments as they used + cv_folds = kwargs.pop('cv_folds',None) + testcovfile_path = kwargs.pop('testcovfile_path', None) + testrespfile_path = kwargs.pop('testrespfile_path', None) + alg = kwargs.pop('alg', None) + configparam = kwargs.pop('configparam', None) + + # change to processing dir + os.chdir(processing_dir) + output_changedir = ['cd ' + processing_dir + '\n'] + + sbatch_init='#!/bin/bash\n' + sbatch_jobname='#SBATCH --job-name=' + processing_dir + '\n' + sbatch_account='#SBATCH --account=p33_norment\n' + sbatch_nodes='#SBATCH --nodes=1\n' + sbatch_tasks='#SBATCH --ntasks=1\n' + sbatch_time='#SBATCH --time=' + str(duration) + '\n' + sbatch_memory='#SBATCH --mem-per-cpu=' + str(memory) + '\n' + sbatch_module='module purge\n' + sbatch_anaconda='module load anaconda3\n' + sbatch_exit='set -o errexit\n' + + #echo -n "This script is running on " + #hostname + + bash_environment = [sbatch_init + + sbatch_jobname + + sbatch_account + + sbatch_nodes + + sbatch_tasks + + sbatch_time + + sbatch_module + + sbatch_anaconda] + + # creates call of function for normative modelling + if (testrespfile_path is not None) and (testcovfile_path is not None): + job_call = [python_path + ' ' + normative_path + ' -c ' + + covfile_path + ' -t ' + testcovfile_path + ' -r ' + + testrespfile_path + ' -f ' + func] + elif (testrespfile_path is None) and (testcovfile_path is not None): + job_call = [python_path + ' ' + normative_path + ' -c ' + + covfile_path + ' -t ' + testcovfile_path + ' -f ' + func] + elif cv_folds is not None: + job_call = [python_path + ' ' + normative_path + ' -c ' + + covfile_path + ' -k ' + str(cv_folds) + ' -f ' + func] + elif func != 'estimate': + job_call = [python_path + ' ' + normative_path + ' -c ' + + covfile_path + ' -f ' + func] + else: + raise(ValueError, """For 'estimate' function either testcov or cvfold + must be specified.""") + + # add algorithm-specific parameters + if alg is not None: + job_call = [job_call[0] + ' -a ' + alg] + if configparam is not None: + job_call = [job_call[0] + ' -x ' + str(configparam)] + + # add standardization flag if it is false + # if not standardize: + # job_call = [job_call[0] + ' -s'] + + # add responses file + job_call = [job_call[0] + ' ' + respfile_path] + + # add in optional arguments. + for k in kwargs: + job_call = [job_call[0] + ' ' + k + '=' + kwargs[k]] + + # writes bash file into processing dir + with open(processing_dir+job_name, 'w') as bash_file: + bash_file.writelines(bash_environment + output_changedir + \ + job_call + ["\n"] + [sbatch_exit]) + + # changes permissoins for bash.sh file + os.chmod(processing_dir + job_name, 0o700) + +def sbatch_nm(job_path, + log_path): + + '''This function submits a job.sh scipt to the torque custer using the qsub + command. + + Basic usage:: + + sbatch_nm(job_path, log_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. + + written by (primarily) T Wolfers, (adapted) S Rutherford. + ''' + + # created qsub command + sbatch_call = ['sbatch ' + job_path] + + # submits job to cluster + call(sbatch_call, shell=True) + +def sbatchrerun_nm(processing_dir, + memory, + duration, + new_memory=False, + new_duration=False, + binary=False, + **kwargs): + + '''This function reruns all failed batched in processing_dir after collect_nm has identified he failed batches. + + Basic usage:: + + rerun_nm(processing_dir, memory, duration) + + :param processing_dir: Full path to the processing directory. + :param memory: Memory requirements written as string, for example 4gb or 500mb. + :param duration: The approximate duration of the job, a string with HH:MM:SS for example 01:01:01. + :param new_memory: If you want to change the memory you have to indicate it here. + :param new_duration: If you want to change the duration you have to indicate it here. + + :outputs: Re-runs failed batches. + + written by (primarily) T Wolfers, (adapted) S Rutherford. + ''' + log_path = kwargs.pop('log_path', None) + + if binary: + file_extentions = '.pkl' + failed_batches = fileio.load(processing_dir + 'failed_batches' + file_extentions) + shape = failed_batches.shape + for n in range(0, shape[0]): + jobpath = failed_batches[n, 0] + print(jobpath) + if new_duration != False: + 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) + + else: + file_extentions = '.txt' + failed_batches = fileio.load_pd(processing_dir + 'failed_batches' + file_extentions) + shape = failed_batches.shape + for n in range(0, shape[0]): + jobpath = failed_batches.iloc[n, 0] + print(jobpath) + if new_duration != False: + 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) + + +def retrieve_jobs(): + """ + A utility function to retrieve task status from the outputs of qstat. + + :return: a dictionary of jobs. + + """ + + output = check_output('qstat', shell=True).decode(sys.stdout.encoding) + output = output.split('\n') + jobs = dict() + 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 + + return jobs + + +def check_job_status(jobs): + """ + A utility function to count the tasks with different status. + + :param jobs: List of job ids. + :return: returns the number of taks athat are queued, running, completed etc + + """ + running_jobs = retrieve_jobs() + + r = 0 + c = 0 + q = 0 + u = 0 + 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 + continue + + print('Total Jobs:%d, Queued:%d, Running:%d, Completed:%d, Unknown:%d' + %(len(jobs), q, r, c, u)) + return q,r,c,u + + +def check_jobs(jobs, delay=60): + """ + A utility function for chacking the status of submitted jobs. + + :param jobs: list of job ids. + :param delay: the delay (in sec) between two consequative checks, defaults to 60. + + """ + + n = len(jobs) + + while(True): + q,r,c,u = check_job_status(jobs) + if c == n: + print('All jobs are completed!') + break + time.sleep(delay) + + diff --git a/build/lib/pcntoolkit/trendsurf.py b/build/lib/pcntoolkit/trendsurf.py new file mode 100644 index 00000000..e0f0d6e5 --- /dev/null +++ b/build/lib/pcntoolkit/trendsurf.py @@ -0,0 +1,253 @@ +#!/Users/andre/sfw/anaconda3/bin/python + +# ------------------------------------------------------------------------------ +# Usage: +# python trendsurf.py -m [maskfile] -b [basis] -c [covariates] +# +# Written by A. Marquand +# ------------------------------------------------------------------------------ + +from __future__ import print_function +from __future__ import division + +import os +import sys +import numpy as np +import nibabel as nib +import argparse + +try: # Run as a package if installed + from pcntoolkit.dataio import fileio + from pcntoolkit.model.bayesreg import BLR +except ImportError: + pass + path = os.path.abspath(os.path.dirname(__file__)) + if path not in sys.path: + sys.path.append(path) + del path + + from dataio import fileio + from model.bayesreg import BLR + + + +def load_data(datafile, maskfile=None): + """ load 4d nifti data """ + if datafile.endswith("nii.gz") or datafile.endswith("nii"): + # we load the data this way rather than fileio.load() because we need + # access to the volumetric representation (to know the # coordinates) + dat = fileio.load_nifti(datafile, vol=True) + dim = dat.shape + if len(dim) <= 3: + dim = dim + (1,) + else: + raise ValueError("No routine to handle non-nifti data") + + mask = fileio.create_mask(dat, mask=maskfile) + + dat = fileio.vol2vec(dat, mask) + maskid = np.where(mask.ravel())[0] + + # generate voxel coordinates + i, j, k = np.meshgrid(np.linspace(0, dim[0]-1, dim[0]), + np.linspace(0, dim[1]-1, dim[1]), + np.linspace(0, dim[2]-1, dim[2]), indexing='ij') + + # voxel-to-world mapping + img = nib.load(datafile) + world = np.vstack((i.ravel(), j.ravel(), k.ravel(), + np.ones(np.prod(i.shape), float))).T + world = np.dot(world, img.affine.T)[maskid, 0:3] + + return dat, world, mask + + +def create_basis(X, basis, mask): + """ Create a (polynomial) basis set """ + + # check whether we are using a polynomial basis set + if type(basis) is int or (type(basis) is str and len(basis) == 1): + dimpoly = int(basis) + dimx = X.shape[1] + print('Generating polynomial basis set of degree', dimpoly, '...') + Phi = np.zeros((X.shape[0], X.shape[1]*dimpoly)) + colid = np.arange(0, dimx) + for d in range(1, dimpoly+1): + Phi[:, colid] = X ** d + colid += dimx + else: # custom basis set + if type(basis) is str: + print('Loading custom basis set from', basis) + + # Phi_vol = fileio.load_data(basis) + # we load the data this way instead so we can apply the same mask + Phi_vol = fileio.load_nifti(basis, vol=True) + Phi = fileio.vol2vec(Phi_vol, mask) + print('Basis set consists of', Phi.shape[1], 'basis functions.') + # maskid = np.where(mask.ravel())[0] + else: + raise ValueError("I don't know what to do with basis:", basis) + + return Phi + + +def write_nii(data, filename, examplenii, mask): + """ Write output to nifti """ + + # load example image + ex_img = nib.load(examplenii) + dim = ex_img.shape[0:3] + nvol = int(data.shape[1]) + + # write data + array_data = np.zeros((np.prod(dim), nvol)) + array_data[mask.flatten(), :] = data + array_data = np.reshape(array_data, dim+(nvol,)) + array_img = nib.Nifti1Image(array_data, + ex_img.get_affine(), + ex_img.get_header()) + nib.save(array_img, filename) + + +def get_args(*args): + # parse arguments + parser = argparse.ArgumentParser(description="Trend surface model") + parser.add_argument("filename") + parser.add_argument("-b", help="basis set", dest="basis", default=3) + 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("-a", help="use ARD", action='store_true') + parser.add_argument("-o", help="output all measures", action='store_true') + args = parser.parse_args() + wdir = os.path.realpath(os.path.curdir) + filename = os.path.join(wdir, args.filename) + if args.maskfile is None: + maskfile = None + else: + maskfile = os.path.join(wdir, args.maskfile) + basis = args.basis + if args.covfile is not None: + raise(NotImplementedError, "Covariates not implemented yet.") + + return filename, maskfile, basis, args.a, args.o + + +def estimate(filename, maskfile, basis, ard=False, outputall=False, + saveoutput=True): + """ Estimate a trend surface model + + This will estimate a trend surface model, independently for each subject. + This is currently fit using a polynomial model of a specified degree. + The models are estimated on the basis of data stored on disk in ascii or + neuroimaging data formats (currently nifti only). Ascii data should be in + tab or space delimited format with the number of voxels in rows and the + number of subjects in columns. Neuroimaging data will be reshaped + into the appropriate format + + Basic usage:: + + estimate(filename, maskfile, basis) + + where the variables are defined below. Note that either the cfolds + parameter or (testcov, testresp) should be specified, but not both. + + :param filename: 4-d nifti file containing the images to be estimated + :param maskfile: nifti mask used to apply to the data + :param basis: model order for the interpolating polynomial + + All outputs are written to disk in the same format as the input. These are: + + :outputs: * yhat - predictive mean + * ys2 - predictive variance + * trendcoeff - coefficients from the trend surface model + * negloglik - Negative log marginal likelihood + * hyp - hyperparameters + * explainedvar - explained variance + * rmse - standardised mean squared error + """ + + # load data + print("Processing data in", filename) + Y, X, mask = load_data(filename, maskfile) + Y = np.round(10000*Y)/10000 # truncate precision to avoid numerical probs + if len(Y.shape) == 1: + Y = Y[:, np.newaxis] + N = Y.shape[1] + + # standardize responses and covariates + mY = np.mean(Y, axis=0) + sY = np.std(Y, axis=0) + Yz = (Y - mY) / sY + mX = np.mean(X, axis=0) + sX = np.std(X, axis=0) + Xz = (X - mX) / sX + + # create basis set and set starting hyperparamters + Phi = create_basis(Xz, basis, mask) + if ard is True: + hyp0 = np.zeros(Phi.shape[1]+1) + else: + hyp0 = np.zeros(2) + + # estimate the models for all subjects + if ard: + print('ARD is enabled') + yhat = np.zeros_like(Yz) + ys2 = np.zeros_like(Yz) + nlZ = np.zeros(N) + hyp = np.zeros((N, len(hyp0))) + rmse = np.zeros(N) + ev = np.zeros(N) + m = np.zeros((N, Phi.shape[1])) + bs2 = np.zeros((N, Phi.shape[1])) + for i in range(0, N): + print("Estimating model ", i+1, "of", N) + breg = BLR() + hyp[i, :] = breg.estimate(hyp0, Phi, Yz[:, i]) + m[i, :] = breg.m + nlZ[i] = breg.nlZ + + # compute extra measures (e.g. marginal variances)? + if outputall: + bs2[i] = np.sqrt(np.diag(np.linalg.inv(breg.A))) + + # compute predictions and errors + yhat[:, i], ys2[:, i] = breg.predict(hyp[i, :], Phi, Yz[:, i], Phi) + yhat[:, i] = yhat[:, i]*sY[i] + mY[i] + rmse[i] = np.sqrt(np.mean((Y[:, i] - yhat[:, i]) ** 2)) + ev[i] = 100*(1 - (np.var(Y[:, i] - yhat[:, i]) / np.var(Y[:, i]))) + + print("Variance explained =", ev[i], "% RMSE =", rmse[i]) + + print("Mean (std) variance explained =", ev.mean(), "(", ev.std(), ")") + print("Mean (std) RMSE =", rmse.mean(), "(", rmse.std(), ")") + + # Write output + if saveoutput: + print("Writing output ...") + np.savetxt("trendcoeff.txt", m, delimiter='\t', fmt='%5.8f') + np.savetxt("negloglik.txt", nlZ, delimiter='\t', fmt='%5.8f') + np.savetxt("hyp.txt", hyp, delimiter='\t', fmt='%5.8f') + np.savetxt("explainedvar.txt", ev, delimiter='\t', fmt='%5.8f') + np.savetxt("rmse.txt", rmse, delimiter='\t', fmt='%5.8f') + fileio.save_nifti(yhat, 'yhat.nii.gz', filename, mask) + fileio.save_nifti(ys2, 'ys2.nii.gz', filename, mask) + + if outputall: + np.savetxt("trendcoeffvar.txt", bs2, delimiter='\t', fmt='%5.8f') + else: + out = [yhat, ys2, nlZ, hyp, rmse, ev, m] + if outputall: + out.append(bs2) + return out + +def main(*args): + np.seterr(invalid='ignore') + + filename, maskfile, basis, ard, outputall = get_args(args) + estimate(filename, maskfile, basis, ard, outputall) + +# For running from the command line: +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/build/lib/pcntoolkit/util/__init__.py b/build/lib/pcntoolkit/util/__init__.py new file mode 100644 index 00000000..9f9161bf --- /dev/null +++ b/build/lib/pcntoolkit/util/__init__.py @@ -0,0 +1 @@ +from . import utils \ No newline at end of file diff --git a/build/lib/pcntoolkit/util/hbr_utils.py b/build/lib/pcntoolkit/util/hbr_utils.py new file mode 100644 index 00000000..db7a5ccd --- /dev/null +++ b/build/lib/pcntoolkit/util/hbr_utils.py @@ -0,0 +1,236 @@ +from __future__ import print_function +import os +import sys +import numpy as np +from scipy import stats +import scipy.special as spp +import pickle +import matplotlib.pyplot as plt +import pandas as pd +import pymc3 as pm +from pcntoolkit.model.SHASH import * +from pcntoolkit.model.hbr import bspline_transform + +""" +@author: augub +""" + +def MCMC_estimate(f, trace): + """Get an MCMC estimate of f given a trace""" + out = np.zeros_like(f(trace.point(0))) + n=0 + for p in trace.points(): + out += f(p) + n+=1 + return out/n + + +def get_MCMC_zscores(X, Y, Z, model): + """Get an MCMC estimate of the z-scores of Y""" + def f(sample): + return get_single_zscores(X, Y, Z, model,sample) + return MCMC_estimate(f, model.hbr.trace) + + +def get_single_zscores(X, Y, Z, model, sample): + """Get the z-scores of y, given clinical covariates and a model""" + likelihood = model.configs['likelihood'] + params = forward(X,Z,model,sample) + return z_score(Y, params, likelihood = likelihood) + + +def z_score(Y, params, likelihood = "Normal"): + """Get the z-scores of Y, given likelihood parameters""" + if likelihood.startswith('SHASH'): + mu = params['mu'] + sigma = params['sigma'] + epsilon = params['epsilon'] + delta = params['delta'] + if likelihood == "SHASHo": + 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) + 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) + SHASH = SHASH_c * true_sigma + true_mu + Z = np.sinh(np.arcsinh(SHASH) * delta - epsilon) + elif likelihood == 'Normal': + Z = (Y-params['mu'])/params['sigma'] + else: + exit("Unsupported likelihood") + return Z + + +def get_MCMC_quantiles(synthetic_X, z_scores, model, be): + """Get an MCMC estimate of the quantiles""" + """This does not use the get_single_quantiles function, for memory efficiency""" + resolution = synthetic_X.shape[0] + synthetic_X_transformed = model.hbr.transform_X(synthetic_X) + be = np.reshape(np.array(be),(1,-1)) + synthetic_Z = np.repeat(be, resolution, axis = 0) + z_scores = np.reshape(np.array(z_scores),(1,-1)) + zs = np.repeat(z_scores, resolution, axis=0) + def f(sample): + params = forward(synthetic_X_transformed,synthetic_Z, model,sample) + q = quantile(zs, params, likelihood = model.configs['likelihood']) + return q + out = MCMC_estimate(f, model.hbr.trace) + return out + + +def get_single_quantiles(synthetic_X, z_scores, model, be, sample): + """Get the quantiles within a given range of covariates, given a model""" + resolution = synthetic_X.shape[0] + synthetic_X_transformed = model.hbr.transform_X(synthetic_X) + be = np.reshape(np.array(be),(1,-1)) + synthetic_Z = np.repeat(be, resolution, axis = 0) + z_scores = np.reshape(np.array(z_scores),(1,-1)) + zs = np.repeat(z_scores, resolution, axis=0) + params = forward(synthetic_X_transformed,synthetic_Z, model,sample) + q = quantile(zs, params, likelihood = model.configs['likelihood']) + return q + + +def quantile(zs, params, likelihood = "Normal"): + """Get the zs'th quantiles given likelihood parameters""" + if likelihood.startswith('SHASH'): + mu = params['mu'] + sigma = params['sigma'] + epsilon = params['epsilon'] + delta = params['delta'] + if likelihood == "SHASHo": + quantiles = S_inv(zs,epsilon,delta)*sigma + mu + elif likelihood == "SHASHo2": + 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) + quantiles = SHASH_c *sigma + mu + elif likelihood == 'Normal': + quantiles = zs*params['sigma'] + params['mu'] + else: + exit("Unsupported likelihood") + return quantiles + + +def single_parameter_forward(X, Z, model, sample, p_name): + """Get a likelihood paramameter given covariates, batch-effects and model parameters""" + outs = np.zeros(X.shape[0])[:,None] + all_bes = np.unique(Z,axis=0) + for be in all_bes: + bet = tuple(be) + idx = (Z==be).all(1) + if model.configs[f"linear_{p_name}"]: + if model.configs[f'random_slope_{p_name}']: + slope_be = sample[f"slope_{p_name}"][bet] + else: + slope_be = sample[f"slope_{p_name}"] + if model.configs[f'random_intercept_{p_name}']: + intercept_be = sample[f"intercept_{p_name}"][bet] + else: + intercept_be = sample[f"intercept_{p_name}"] + + out = (X[np.squeeze(idx),:]@slope_be)[:,None] + intercept_be + outs[np.squeeze(idx),:] = out + else: + if model.configs[f'random_{p_name}']: + outs[np.squeeze(idx),:] = sample[p_name][bet] + else: + outs[np.squeeze(idx),:] = sample[p_name] + + return outs + + +def forward(X, Z, model, sample): + """Get all likelihood paramameters given covariates and batch-effects and model parameters""" + # TODO think if this is the correct spot for this + mapfuncs={'sigma': lambda x: np.log(1+np.exp(x)), 'delta':lambda x :np.log(1+np.exp(x)) + 0.3} + + likelihood = model.configs['likelihood'] + + if likelihood == 'Normal': + parameter_list = ['mu','sigma'] + elif likelihood in ['SHASHb','SHASHo','SHASHo2']: + parameter_list = ['mu','sigma','epsilon','delta'] + else: + exit("Unsupported likelihood") + + for i in parameter_list: + if not (i in mapfuncs.keys()): + mapfuncs[i] = lambda x: x + + output_dict = {p_name:np.zeros(X.shape) for p_name in parameter_list} + + for p_name in parameter_list: + output_dict[p_name] = mapfuncs[p_name](single_parameter_forward(X,Z,model,sample,p_name)) + + return output_dict + + +def Rhats(model, thin = 1, resolution = 100, varnames = None): + """Get Rhat as function of sampling iteration""" + trace = model.hbr.trace + + if varnames == None: + varnames = trace.varnames + chain_length = trace.get_values(varnames[0],chains=trace.chains[0], thin=thin).shape[0] + + interval_skip=chain_length//resolution + + rhat_dict = {} + + for varname in varnames: + testvar = np.stack(trace.get_values(varname,combine=False)) + vardim = testvar.reshape((testvar.shape[0], testvar.shape[1], -1)).shape[2] + rhats_var = np.zeros((resolution, vardim)) + + var = np.stack(trace.get_values(varname,combine=False)) + var = var.reshape((var.shape[0], var.shape[1], -1)) + for v in range(var.shape[2]): + for j in range(resolution): + rhats_var[j,v] = pm.rhat(var[:,:j*interval_skip,v]) + rhat_dict[varname] = rhats_var + return rhat_dict + + +def S_inv(x, e, d): + return np.sinh((np.arcsinh(x) + e) / d) + +def K(p, x): + return np.array(spp.kv(p, x)) + +def P(q): + """ + The P function as given in Jones et al. + :param q: + :return: + """ + frac = np.exp(1 / 4) / np.sqrt(8 * np.pi) + K1 = K((q + 1) / 2, 1 / 4) + K2 = K((q - 1) / 2, 1 / 4) + a = (K1 + K2) * frac + return a + +def m(epsilon, delta, r): + """ + The r'th uncentered moment. Given by Jones et al. + """ + frac1 = 1 / np.power(2, r) + acc = 0 + for i in range(r + 1): + combs = spp.comb(r, i) + flip = np.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 + + + diff --git a/build/lib/pcntoolkit/util/utils.py b/build/lib/pcntoolkit/util/utils.py new file mode 100644 index 00000000..24bb8dcc --- /dev/null +++ b/build/lib/pcntoolkit/util/utils.py @@ -0,0 +1,1507 @@ +from __future__ import print_function + +import os +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 +import matplotlib.pyplot as plt +import pandas as pd +import bspline +from bspline import splinelab +from sklearn.datasets import make_regression +import pymc3 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 +except ImportError: + pass + + path = os.path.abspath(os.path.dirname(__file__)) + rootpath = os.path.dirname(path) # parent directory + if rootpath not in sys.path: + sys.path.append(rootpath) + del path, rootpath + import configs + +PICKLE_PROTOCOL = configs.PICKLE_PROTOCOL + +# ----------------- +# Utility functions +# ----------------- +def create_poly_basis(X, dimpoly): + """ + Compute a polynomial basis expansion of the specified order + + """ + + if len(X.shape) == 1: + X = X[:, np.newaxis] + D = X.shape[1] + Phi = np.zeros((X.shape[0], D*dimpoly)) + colid = np.arange(0, D) + for d in range(1, dimpoly+1): + Phi[:, colid] = X ** d + colid += D + + return Phi + +def create_bspline_basis(xmin, xmax, p = 3, nknots = 5): + """ + Compute a Bspline basis set where: + + :param p: order of spline (3 = cubic) + :param nknots: number of knots (endpoints only counted once) + + """ + + knots = np.linspace(xmin, xmax, nknots) + k = splinelab.augknt(knots, p) # pad the knot vector + B = bspline.Bspline(k, p) + return B + +def create_design_matrix(X, intercept = True, basis = 'bspline', + basis_column = 0, site_ids=None, all_sites=None, + **kwargs): + """ + Prepare a design matrix from a set of covariates sutiable for + running Bayesian linar regression. This design matrix consists of + a set of user defined covariates, optoinal site intercepts + (fixed effects) and also optionally a nonlinear basis expansion over + one of the columns + + :param X: matrix of covariates + :param basis: type of basis expansion to use + :param basis_column: which colume to perform the expansion over? + :param site_ids: list of site ids (one per data point) + :param all_sites: list of unique site ids + :param p: order of spline (3 = cubic) + :param nknots: number of knots (endpoints only counted once) + + if site_ids is specified, this must have the same number of entries as + there are rows in X. If all_sites is specfied, these will be used to + create the site identifiers in place of site_ids. This accommocdates + the scenario where not all the sites used to create the model are + present in the test set (i.e. there will be some empty site columns). + + """ + + xmin = kwargs.pop('xmin', 0) + xmax = kwargs.pop('xmax', 100) + + N = X.shape[0] + + if type(X) is pd.DataFrame: + X = X.to_numpy() + + # add intercept column + if intercept: + Phi = np.concatenate((np.ones((N, 1)), X), axis=1) + else: + Phi = X + + # add dummy coded site columns + if all_sites is None: + if site_ids is not None: + all_sites = sorted(pd.unique(site_ids)) + + if site_ids is None: + if all_sites is None: + site_cols = None + else: + # site ids are not specified, but all_sites are + site_cols = np.zeros((N, len(all_sites))) + else: + # site ids are defined + # make sure the data are in pandas format + if type(site_ids) is not pd.Series: + site_ids = pd.Series(data=site_ids) + #site_ids = pd.Series(data=site_ids) + + # make sure all_sites is defined + if all_sites is None: + all_sites = sorted(pd.unique(site_ids)) + + # dummy code the sites + site_cols = np.zeros((N, len(all_sites))) + for i, s in enumerate(all_sites): + site_cols[:, i] = site_ids == s + + if site_cols.shape[0] != N: + raise ValueError('site cols must have the same number of rows as X') + + if site_cols is not None: + Phi = np.concatenate((Phi, site_cols), axis=1) + + # create Bspline basis set + if basis == 'bspline': + B = create_bspline_basis(xmin, xmax, **kwargs) + Phi = np.concatenate((Phi, np.array([B(i) for i in X[:,basis_column]])), axis=1) + elif basis == 'poly': + Phi = np.concatenate(Phi, create_poly_basis(X[:,basis_column], **kwargs)) + + return Phi + +def squared_dist(x, z=None): + """ + Compute sum((x-z) ** 2) for all vectors in a 2d array. + + """ + + # do some basic checks + if z is None: + z = x + if len(x.shape) == 1: + x = x[:, np.newaxis] + if len(z.shape) == 1: + z = z[:, np.newaxis] + + nx, dx = x.shape + nz, dz = z.shape + if dx != dz: + raise ValueError(""" + Cannot compute distance: vectors have different length""") + + # mean centre for numerical stability + m = np.mean(np.vstack((np.mean(x, axis=0), np.mean(z, axis=0))), axis=0) + x = x - m + z = z - m + + xx = np.tile(np.sum((x*x), axis=1)[:, np.newaxis], (1, nz)) + zz = np.tile(np.sum((z*z), axis=1), (nx, 1)) + + dist = (xx - 2*x.dot(z.T) + zz) + + return dist + + +def compute_pearsonr(A, B): + """ + Manually computes the Pearson correlation between two matrices. + + Basic usage:: + + compute_pearsonr(A, B) + + :param A: an N * M data array + :param cov: an N * M array + + :returns Rho: N dimensional vector of correlation coefficients + :returns ys2: N dimensional vector of p-values + + Notes:: + + This function is useful when M is large and only the diagonal entries + of the resulting correlation matrix are of interest. This function + does not compute the full correlation matrix as an intermediate step + + """ + + # N = A.shape[1] + N = A.shape[0] + + # first mean centre + Am = A - np.mean(A, axis=0) + Bm = B - np.mean(B, axis=0) + # then normalize + An = Am / np.sqrt(np.sum(Am**2, axis=0)) + Bn = Bm / np.sqrt(np.sum(Bm**2, axis=0)) + del(Am, Bm) + + Rho = np.sum(An * Bn, axis=0) + del(An, Bn) + + # Fisher r-to-z + Zr = (np.arctanh(Rho) - np.arctanh(0)) * np.sqrt(N - 3) + N = stats.norm() + pRho = 2*N.cdf(-np.abs(Zr)) + # pRho = 1-N.cdf(Zr) + + return Rho, pRho + +def explained_var(ytrue, ypred): + """ + Computes the explained variance of predicted values. + + Basic usage:: + + exp_var = explained_var(ytrue, ypred) + + where + + :ytrue: n*p matrix of true values where n is the number of samples + and p is the number of features. + :ypred: n*p matrix of predicted values where n is the number of samples + and p is the number of features. + + :returns exp_var: p dimentional vector of explained variances for each feature. + + """ + + exp_var = 1 - (ytrue - ypred).var(axis = 0) / ytrue.var(axis = 0) + + return exp_var + +def compute_MSLL(ytrue, ypred, ypred_var, train_mean = None, train_var = None): + """ + Computes the MSLL or MLL (not standardized) if 'train_mean' and 'train_var' are None. + + Basic usage:: + + MSLL = compute_MSLL(ytrue, ypred, ytrue_sig, noise_variance, train_mean, train_var) + + where + + :param ytrue : n*p matrix of true values where n is the number of samples + and p is the number of features. + :param ypred : n*p matrix of predicted values where n is the number of samples + and p is the number of features. + :param ypred_var : n*p matrix of summed noise variances and prediction variances where n is the number of samples + and p is the number of features. + + :param train_mean: p dimensional vector of mean values of the training data for each feature. + + :param train_var : p dimensional vector of covariances of the training data for each feature. + + :returns loss : p dimensional vector of MSLL or MLL for each feature. + + """ + + if train_mean is not None and train_var is not None: + + # make sure y_train_mean and y_train_sig have right dimensions (subjects x voxels): + Y_train_mean = np.repeat(train_mean, ytrue.shape[0], axis = 0) + Y_train_sig = np.repeat(train_var, ytrue.shape[0], axis = 0) + + # compute MSLL: + loss = np.mean(0.5 * np.log(2 * np.pi * ypred_var) + (ytrue - ypred)**2 / (2 * ypred_var) - + 0.5 * np.log(2 * np.pi * Y_train_sig) - (ytrue - Y_train_mean)**2 / (2 * Y_train_sig), axis = 0) + + else: + # compute MLL: + loss = np.mean(0.5 * np.log(2 * np.pi * ypred_var) + (ytrue - ypred)**2 / (2 * ypred_var), axis = 0) + + return loss + +def calibration_descriptives(x): + """ + Compute statistics useful to assess the calibration of normative models, + including skew and kurtosis of the distribution, plus their standard + deviation and standar errors (separately for each column in x) + + Basic usage:: + stats = calibration_descriptives(Z) + + where + + :param x : n*p matrix of statistics you wish to assess + :returns stats :[skew, sdskew, kurtosis, sdkurtosis, semean, sesd] + + """ + + n = np.shape(x)[0] + m1 = np.mean(x,axis=0) + m2 = sum((x-m1)**2) + m3 = sum((x-m1)**3) + m4 = sum((x-m1)**4) + 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) + sdkurtosis = np.sqrt( 4*(n**2-1) * sdskew**2 / ((n-3)*(n+5)) ) + semean = np.sqrt(np.var(x)/n) + sesd = s1/np.sqrt(2*(n-1)) + cd = [skew, sdskew, kurtosis, sdkurtosis, semean, sesd] + + return cd + +class WarpBase(with_metaclass(ABCMeta)): + """ + Base class for likelihood warping following: + Rios and Torab (2019) Compositionally-warped Gaussian processes + https://www.sciencedirect.com/science/article/pii/S0893608019301856 + + All Warps must define the following methods:: + + Warp.get_n_params() - return number of parameters + Warp.f() - warping function (Non-Gaussian field -> Gaussian) + Warp.invf() - inverse warp + Warp.df() - derivatives + Warp.warp_predictions() - compute predictive distribution + + """ + + def __init__(self): + self.n_params = np.nan + + def get_n_params(self): + """ Report the number of parameters required """ + + assert not np.isnan(self.n_params), \ + "Warp function not initialised" + + return self.n_params + + def warp_predictions(self, mu, s2, param, percentiles=[0.025, 0.975]): + """ + Compute the warped predictions from a gaussian predictive + distribution, specifed by a mean (mu) and variance (s2) + + :param mu: Gassian predictive mean + :param s2: Predictive variance + :param param: warping parameters + :param percentiles: Desired percentiles of the warped likelihood + + :returns: * median - median of the predictive distribution + * pred_interval - predictive interval(s) + + """ + + # Compute percentiles of a standard Gaussian + N = norm + Z = N.ppf(percentiles) + + # find the median (using mu = median) + median = self.invf(mu, param) + + # compute the predictive intervals (non-stationary) + pred_interval = np.zeros((len(mu), len(Z))) + for i, z in enumerate(Z): + pred_interval[:,i] = self.invf(mu + np.sqrt(s2)*z, param) + + return median, pred_interval + + @abstractmethod + def f(self, x, param): + """ Evaluate the warping function (mapping non-Gaussian respone + variables to Gaussian variables) + """ + + @abstractmethod + def invf(self, y, param): + """ Evaluate the warping function (mapping Gaussian latent variables + to non-Gaussian response variables) + """ + + @abstractmethod + def df(self, x, param): + """ Return the derivative of the warp, dw(x)/dx """ + +class WarpLog(WarpBase): + """ Affine warp + y = a + b*x + """ + + def __init__(self): + self.n_params = 0 + + def f(self, x, params=None): + + y = np.log(x) + + return y + + def invf(self, y, params=None): + + x = np.exp(y) + + return x + + def df(self, x, params): + + df = 1/x + + return df + +class WarpAffine(WarpBase): + """ Affine warp + y = a + b*x + """ + + def __init__(self): + self.n_params = 2 + + def _get_params(self, param): + if len(param) != self.n_params: + raise(ValueError, + 'number of parameters must be ' + str(self.n_params)) + return param[0], np.exp(param[1]) + + def f(self, x, params): + a, b = self._get_params(params) + + y = a + b*x + return y + + def invf(self, y, params): + a, b = self._get_params(params) + + x = (y - a) / b + + return x + + def df(self, x, params): + a, b = self._get_params(params) + + df = np.ones(x.shape)*b + return df + +class WarpBoxCox(WarpBase): + """ Box cox transform having a single parameter (lambda), i.e. + + y = (sign(x) * abs(x) ** lamda - 1) / lambda + + This follows the generalization in Bicken and Doksum (1981) JASA 76 + and allows x to assume negative values. + """ + + def __init__(self): + self.n_params = 1 + + def _get_params(self, param): + + return np.exp(param) + + def f(self, x, params): + lam = self._get_params(params) + + if lam == 0: + y = np.log(x) + else: + y = (np.sign(x) * np.abs(x) ** lam - 1) / lam + return y + + def invf(self, y, params): + lam = self._get_params(params) + + if lam == 0: + x = np.exp(y) + else: + x = np.sign(lam * y + 1) * np.abs(lam * y + 1) ** (1 / lam) + + return x + + def df(self, x, params): + lam = self._get_params(params) + + dx = np.abs(x) ** (lam - 1) + + return dx + +class WarpSinArcsinh(WarpBase): + """ Sin-hyperbolic arcsin warp having two parameters (a, b) and defined by + + y = sinh(b * arcsinh(x) - a) + + Using the parametrisation of Rios et al, Neural Networks 118 (2017) + where a controls skew and b controls kurtosis, such that: + + * a = 0 : symmetric + * a > 0 : positive skew + * a < 0 : negative skew + * b = 1 : mesokurtic + * b > 1 : leptokurtic + * b < 1 : platykurtic + + where b > 0. However, it is more convenentent to use an alternative + parameterisation, given in Jones and Pewsey 2019 JRSS Significance 16 + https://doi.org/10.1111/j.1740-9713.2019.01245.x + + where: + + y = sinh(b * arcsinh(x) + epsilon * b) + + and a = -epsilon*b + + see also Jones and Pewsey 2009 Biometrika, 96 (4) for more details + about the SHASH distribution + https://www.jstor.org/stable/27798865 + """ + + def __init__(self): + self.n_params = 2 + + def _get_params(self, param): + if len(param) != self.n_params: + raise(ValueError, + 'number of parameters must be ' + str(self.n_params)) + + epsilon = param[0] + b = np.exp(param[1]) + a = -epsilon*b + + return a, b + + def f(self, x, params): + a, b = self._get_params(params) + + y = np.sinh(b * np.arcsinh(x) - a) + return y + + def invf(self, y, params): + a, b = self._get_params(params) + + x = np.sinh((np.arcsinh(y)+a)/b) + + return x + + def df(self, x, params): + a, b = self._get_params(params) + + dx = (b *np.cosh(b * np.arcsinh(x) - a))/np.sqrt(1 + x ** 2) + + return dx + +class WarpCompose(WarpBase): + """ Composition of warps. These are passed in as an array and + intialised automatically. For example:: + + W = WarpCompose(('WarpBoxCox', 'WarpAffine')) + + where ell_i are lengthscale parameters and sf2 is the signal variance + """ + + def __init__(self, warpnames=None, debugwarp=False): + + if warpnames is None: + raise ValueError("A list of warp functions is required") + self.debugwarp = debugwarp + self.warps = [] + self.n_params = 0 + for wname in warpnames: + warp = eval(wname + '()') + self.n_params += warp.get_n_params() + self.warps.append(warp) + + def f(self, x, theta): + theta_offset = 0 + + if self.debugwarp: + print('begin composition') + for ci, warp in enumerate(self.warps): + n_params_c = warp.get_n_params() + theta_c = [theta[c] for c in + range(theta_offset, theta_offset + n_params_c)] + theta_offset += n_params_c + + if self.debugwarp: + print('f:', ci, theta_c, warp) + + if ci == 0: + fw = warp.f(x, theta_c) + else: + fw = warp.f(fw, theta_c) + return fw + + def invf(self, x, theta): + n_params = 0 + n_warps = 0 + if self.debugwarp: + print('begin composition') + + for ci, warp in enumerate(self.warps): + n_params += warp.get_n_params() + n_warps += 1 + theta_offset = n_params + for ci, warp in reversed(list(enumerate(self.warps))): + n_params_c = warp.get_n_params() + theta_offset -= n_params_c + theta_c = [theta[c] for c in + range(theta_offset, theta_offset + n_params_c)] + + if self.debugwarp: + print('invf:', theta_c, warp) + + if ci == n_warps-1: + finvw = warp.invf(x, theta_c) + else: + finvw = warp.invf(finvw, theta_c) + + return finvw + + def df(self, x, theta): + theta_offset = 0 + if self.debugwarp: + print('begin composition') + for ci, warp in enumerate(self.warps): + n_params_c = warp.get_n_params() + + theta_c = [theta[c] for c in + range(theta_offset, theta_offset + n_params_c)] + theta_offset += n_params_c + + if self.debugwarp: + print('df:', ci, theta_c, warp) + + if ci == 0: + dfw = warp.df(x, theta_c) + else: + dfw = warp.df(dfw, theta_c) + + return dfw + +# ----------------------- +# Functions for inference +# ----------------------- + +class CustomCV: + """ Custom cross-validation approach. This function does not do much, it + merely provides a wrapper designed to be compatible with + scikit-learn (e.g. sklearn.model_selection...) + + :param train: a list of indices of training splits (each itself a list) + :param test: a list of indices of test splits (each itself a list) + + :returns tr: Indices for training set + :returns te: Indices for test set + + """ + + def __init__(self, train, test, X=None, y=None): + self.train = train + self.test = test + self.n_splits = len(train) + if X is not None: + self.N = X.shape[0] + else: + self.N = None + + def split(self, X, y=None): + if self.N is None: + self.N = X.shape[0] + + for i in range(0, self.n_splits): + tr = self.train[i] + te = self.test[i] + yield tr, te + +def bashwrap(processing_dir, python_path, script_command, job_name, + bash_environment=None): + + """ This function wraps normative modelling into a bash script to run it + on a torque cluster system. + + :param processing_dir: Full path to the processing dir + :param python_path: Full path to the python distribution + :param script_command: python command to execute + :param job_name: Name for the bash script output by this function + :param covfile_path: Full path to covariates + :param respfile_path: Full path to response variables + :param cv_folds: Number of cross validations + :param testcovfile_path: Full path to test covariates + :param testrespfile_path: Full path to tes responses + :param bash_environment: A file containing enviornment specific commands + + :returns: A .sh file containing the commands for normative modelling + + written by Thomas Wolfers + """ + + # change to processing dir + os.chdir(processing_dir) + output_changedir = ['cd ' + processing_dir + '\n'] + + # sets bash environment if necessary + if bash_environment is not None: + bash_environment = [bash_environment] + print("""Your own environment requires in any case: + #!/bin/bash\n export and optionally OMP_NUM_THREADS=1\n""") + else: + bash_lines = '#!/bin/bash\n\n' + bash_cores = 'export OMP_NUM_THREADS=1\n' + bash_environment = [bash_lines + bash_cores] + + command = [python_path + ' ' + script_command + '\n'] + + # writes bash file into processing dir + bash_file_name = os.path.join(processing_dir, job_name + '.sh') + with open(bash_file_name, 'w') as bash_file: + bash_file.writelines(bash_environment + output_changedir + command) + + # changes permissoins for bash.sh file + os.chmod(bash_file_name, 0o700) + + return bash_file_name + +def qsub(job_path, memory, duration, logdir=None): + """This function submits a job.sh scipt to the torque custer using the qsub command. + + Basic usage:: + + qsub_nm(job_path, log_path, memory, duration) + + :param job_path: Full path to the job.sh file. + :param memory: Memory requirements written as string for example 4gb or 500mb. + :param duation: The approximate duration of the job, a string with HH:MM:SS for example 01:01:01. + + :outputs: Submission of the job to the (torque) cluster. + + written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. + """ + if logdir is None: + logdir = os.path.expanduser('~') + + # created qsub command + qsub_call = ['echo ' + job_path + ' | qsub -N ' + job_path + ' -l ' + + 'mem=' + memory + ',walltime=' + duration + + ' -e ' + logdir + ' -o ' + logdir] + + # submits job to cluster + call(qsub_call, shell=True) + +def extreme_value_prob_fit(NPM, perc): + n = NPM.shape[0] + t = NPM.shape[1] + n_perc = int(round(t * perc)) + m = np.zeros(n) + for i in range(n): + temp = np.abs(NPM[i, :]) + temp = np.sort(temp) + temp = temp[t - n_perc:] + temp = temp[0:int(np.floor(0.90*temp.shape[0]))] + m[i] = np.mean(temp) + params = genextreme.fit(m) + return params + +def extreme_value_prob(params, NPM, perc): + n = NPM.shape[0] + t = NPM.shape[1] + n_perc = int(round(t * perc)) + m = np.zeros(n) + for i in range(n): + temp = np.abs(NPM[i, :]) + temp = np.sort(temp) + temp = temp[t - n_perc:] + temp = temp[0:int(np.floor(0.90*temp.shape[0]))] + m[i] = np.mean(temp) + probs = genextreme.cdf(m,*params) + return probs + +def ravel_2D(a): + s = a.shape + return np.reshape(a,[s[0], np.prod(s[1:])]) + +def unravel_2D(a, s): + return np.reshape(a,s) + +def threshold_NPM(NPMs, fdr_thr=0.05, npm_thr=0.1): + """ Compute voxels with significant NPMs. """ + p_values = stats.norm.cdf(-np.abs(NPMs)) + results = np.zeros(NPMs.shape) + masks = np.full(NPMs.shape, False, dtype=bool) + for i in range(p_values.shape[0]): + masks[i,:] = FDR(p_values[i,:], fdr_thr) + results[i,] = NPMs[i,:] * masks[i,:].astype(np.int) + m = np.sum(masks,axis=0)/masks.shape[0] > npm_thr + #m = np.any(masks,axis=0) + return results, masks, m + +def FDR(p_values, alpha): + """ Compute the false discovery rate in all voxels for a subject. """ + dim = np.shape(p_values) + p_values = np.reshape(p_values,[np.prod(dim),]) + sorted_p_values = np.sort(p_values) + sorted_p_values_idx = np.argsort(p_values); + testNum = len(p_values) + thresh = ((np.array(range(testNum)) + 1)/np.float(testNum)) * alpha + h = sorted_p_values <= thresh + unsort = np.argsort(sorted_p_values_idx) + h = h[unsort] + h = np.reshape(h, dim) + return h + + +def calibration_error(Y,m,s,cal_levels): + ce = 0 + for cl in cal_levels: + z = np.abs(norm.ppf((1-cl)/2)) + ub = m + z * s + lb = m - z * s + ce = ce + np.abs(cl - np.sum(np.logical_and(Y>=lb,Y<=ub))/Y.shape[0]) + return ce + + +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 + + """ + + if isinstance(n_samples, int): + n_samples = [n_samples for i in range(n_grps)] + + X_train, Y_train, X_test, Y_test = [], [], [], [] + grp_id_train, grp_id_test = [], [] + coef = [] + for i in range(n_grps): + bias = np.random.randint(-10, high=10) + + if method == 'linear': + X_temp, Y_temp, coef_temp = make_regression(n_samples=n_samples[i]*2, + n_features=n_features, n_targets=1, + noise=10 * np.random.rand(), bias=bias, + n_informative=1, coef=True, + random_state=random_state) + elif method == 'non-linear': + X_temp = np.random.randint(-2,6,[2*n_samples[i], n_features]) \ + + np.random.randn(2*n_samples[i], n_features) + Y_temp = X_temp[:,0] * 20 * np.random.rand() + np.random.randint(10,100) \ + * np.sin(2 * np.random.rand() + 2 * np.pi /5 * X_temp[:,0]) + coef_temp = 0 + elif method == 'combined': + X_temp = np.random.randint(-2,6,[2*n_samples[i], n_features]) \ + + np.random.randn(2*n_samples[i], n_features) + Y_temp = (X_temp[:,0]**3) * np.random.uniform(0, 0.5) \ + + X_temp[:,0] * 20 * np.random.rand() \ + + np.random.randint(10, 100) + 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])) + X_train = np.vstack(X_train) + X_test = np.vstack(X_test) + Y_train = np.concatenate(Y_train) + Y_test = np.concatenate(Y_test) + grp_id_train = np.expand_dims(np.concatenate(grp_id_train), axis=1) + grp_id_test = np.expand_dims(np.concatenate(grp_id_test), axis=1) + + for i in range(n_features): + plt.figure() + for j in range(n_grps): + plt.scatter(X_train[grp_id_train[:,0]==j,i], + Y_train[grp_id_train[:,0]==j,], label='Group ' + str(j)) + plt.xlabel('X' + str(i)) + plt.ylabel('Y') + plt.legend() + + if working_dir is not None: + if not os.path.isdir(working_dir): + os.mkdir(working_dir) + with open(os.path.join(working_dir ,'trbefile.pkl'), 'wb') as file: + pickle.dump(pd.DataFrame(grp_id_train),file, protocol=PICKLE_PROTOCOL) + with open(os.path.join(working_dir ,'tsbefile.pkl'), 'wb') as file: + pickle.dump(pd.DataFrame(grp_id_test),file, protocol=PICKLE_PROTOCOL) + with open(os.path.join(working_dir ,'X_train.pkl'), 'wb') as file: + pickle.dump(pd.DataFrame(X_train),file, protocol=PICKLE_PROTOCOL) + with open(os.path.join(working_dir ,'X_test.pkl'), 'wb') as file: + pickle.dump(pd.DataFrame(X_test),file, protocol=PICKLE_PROTOCOL) + with open(os.path.join(working_dir ,'Y_train.pkl'), 'wb') as file: + pickle.dump(pd.DataFrame(Y_train),file, protocol=PICKLE_PROTOCOL) + with open(os.path.join(working_dir ,'Y_test.pkl'), 'wb') as file: + pickle.dump(pd.DataFrame(Y_test),file, protocol=PICKLE_PROTOCOL) + + return X_train, Y_train, grp_id_train, X_test, Y_test, grp_id_test, coef + + +def divergence_plot(nm, ylim=None): + + if nm.hbr.configs['n_chains'] > 1 and nm.hbr.model_type != 'nn': + a = pm.summary(nm.hbr.trace).round(2) + plt.figure() + plt.hist(a['r_hat'],10) + plt.title('Gelman-Rubin diagnostic for divergence') + + divergent = nm.hbr.trace['diverging'] + + tracedf = pm.trace_to_dataframe(nm.hbr.trace) + + _, ax = plt.subplots(2, 1, figsize=(15, 4), sharex=True, sharey=True) + ax[0].plot(tracedf.values[divergent == 0].T, color='k', alpha=.05) + ax[0].set_title('No Divergences', fontsize=10) + ax[1].plot(tracedf.values[divergent == 1].T, color='C2', lw=.5, alpha=.5) + ax[1].set_title('Divergences', fontsize=10) + plt.ylim(ylim) + plt.xticks(range(tracedf.shape[1]), list(tracedf.columns)) + plt.xticks(rotation=90, fontsize=7) + plt.tight_layout() + plt.show() + + +def load_freesurfer_measure(measure, data_path, subjects_list): + + """This is a utility function to load different Freesurfer measures in a pandas Dataframe. + + Inputs + + :param measure: a string that defines the type of Freesurfer measure we want to load. \ + The options include: + + * 'NumVert': Number of Vertices in each cortical area based on Destrieux atlas. + * 'SurfArea: Surface area for each cortical area based on Destrieux atlas. + * 'GrayVol': Gary matter volume in each cortical area based on Destrieux atlas. + * 'ThickAvg': Average Cortical thinckness in each cortical area based on Destrieux atlas. + * 'ThickStd': STD of Cortical thinckness in each cortical area based on Destrieux atlas. + * 'MeanCurv': Integrated Rectified Mean Curvature in each cortical area based on Destrieux atlas. + * 'GausCurv': Integrated Rectified Gaussian Curvature in each cortical area based on Destrieux atlas. + * 'FoldInd': Folding Index in each cortical area based on Destrieux atlas. + * 'CurvInd': Intrinsic Curvature Index in each cortical area based on Destrieux atlas. + * 'brain': Brain Segmentation Statistics from aseg.stats file. + * 'subcortical_volumes': Subcortical areas volume. + + :param data_path: a string that specifies the path to the main Freesurfer folder. + :param subjects_list: A Pythin list containing the list of subject names to load the data for. \ + The subject names should match the folder name for each subject's Freesurfer data folder. + + Outputs: + - df: A pandas datafrmae containing the subject names as Index and target Freesurfer measures. + - missing_subs: A Python list of subject names that miss the target Freesurefr measures. + + """ + + df = pd.DataFrame() + missing_subs = [] + + if measure in ['NumVert', 'SurfArea', 'GrayVol', 'ThickAvg', + 'ThickStd', 'MeanCurv', 'GausCurv', 'FoldInd', 'CurvInd']: + l = ['NumVert', 'SurfArea', 'GrayVol', 'ThickAvg', + 'ThickStd', 'MeanCurv', 'GausCurv', 'FoldInd', 'CurvInd'] + col = l.index(measure) + 1 + for i, sub in enumerate(subjects_list): + try: + data = dict() + + a = pd.read_csv(data_path + sub + '/stats/lh.aparc.a2009s.stats', + delimiter='\s+', comment='#', header=None) + temp = dict(zip(a[0], a[col])) + for key in list(temp.keys()): + temp['L_'+key] = temp.pop(key) + data.update(temp) + + a = pd.read_csv(data_path + sub + '/stats/rh.aparc.a2009s.stats', + delimiter='\s+', comment='#', header=None) + temp = dict(zip(a[0], a[col])) + for key in list(temp.keys()): + temp['R_'+key] = temp.pop(key) + data.update(temp) + + df_temp = pd.DataFrame(data,index=[sub]) + df = pd.concat([df, df_temp]) + print('%d / %d: %s is done!' %(i, len(subjects_list), sub)) + except: + missing_subs.append(sub) + print('%d / %d: %s is missing!' %(i, len(subjects_list), sub)) + continue + + elif measure == 'brain': + for i, sub in enumerate(subjects_list): + try: + data = dict() + s = StringIO() + with open(data_path + sub + '/stats/aseg.stats') as f: + for line in f: + if line.startswith('# Measure'): + s.write(line) + s.seek(0) # "rewind" to the beginning of the StringIO object + a = pd.read_csv(s, header=None) # with further parameters? + data_brain = dict(zip(a[1], a[3])) + data.update(data_brain) + df_temp = pd.DataFrame(data,index=[sub]) + df = pd.concat([df, df_temp]) + print('%d / %d: %s is done!' %(i, len(subjects_list), sub)) + except: + missing_subs.append(sub) + print('%d / %d: %s is missing!' %(i, len(subjects_list), sub)) + continue + + elif measure == 'subcortical_volumes': + for i, sub in enumerate(subjects_list): + try: + data = dict() + s = StringIO() + with open(data_path + sub + '/stats/aseg.stats') as f: + for line in f: + if line.startswith('# Measure'): + s.write(line) + s.seek(0) # "rewind" to the beginning of the StringIO object + a = pd.read_csv(s, header=None) # with further parameters? + a = dict(zip(a[1], a[3])) + if ' eTIV' in a.keys(): + tiv = a[' eTIV'] + else: + tiv = a[' ICV'] + a = pd.read_csv(data_path + sub + '/stats/aseg.stats', delimiter='\s+', comment='#', header=None) + data_vol = dict(zip(a[4]+'_mm3', a[3])) + for key in data_vol.keys(): + data_vol[key] = data_vol[key]/tiv + data.update(data_vol) + data = pd.DataFrame(data,index=[sub]) + df = pd.concat([df, data]) + print('%d / %d: %s is done!' %(i, len(subjects_list), sub)) + except: + missing_subs.append(sub) + print('%d / %d: %s is missing!' %(i, len(subjects_list), sub)) + continue + + return df, missing_subs + + +class scaler: + + def __init__(self, scaler_type='standardize', tail=0.01): + + self.scaler_type = scaler_type + self.tail = tail + + if self.scaler_type not in ['standardize', 'minmax', 'robminmax']: + raise ValueError("Undifined scaler type!") + + + def fit(self, X): + + if self.scaler_type == 'standardize': + + self.m = np.mean(X, axis=0) + self.s = np.std(X, axis=0) + + elif self.scaler_type == 'minmax': + self.min = np.min(X, axis=0) + self.max = np.max(X, axis=0) + + elif self.scaler_type == 'robminmax': + self.min = np.zeros([X.shape[1],]) + self.max = np.zeros([X.shape[1],]) + for i in range(X.shape[1]): + self.min[i] = np.median(np.sort(X[:,i])[0:int(np.round(X.shape[0] * self.tail))]) + self.max[i] = np.median(np.sort(X[:,i])[-int(np.round(X.shape[0] * self.tail)):]) + + + def transform(self, X, adjust_outliers=False): + + if self.scaler_type == 'standardize': + + X = (X - self.m) / self.s + + elif self.scaler_type in ['minmax', 'robminmax']: + + X = (X - self.min) / (self.max - self.min) + + if adjust_outliers: + + X[X < 0] = 0 + X[X > 1] = 1 + + return X + + def inverse_transform(self, X, index=None): + + if self.scaler_type == 'standardize': + if index is None: + X = X * self.s + self.m + else: + X = X * self.s[index] + self.m[index] + + elif self.scaler_type in ['minmax', 'robminmax']: + if index is None: + X = X * (self.max - self.min) + self.min + else: + X = X * (self.max[index] - self.min[index]) + self.min[index] + return X + + def fit_transform(self, X, adjust_outliers=False): + + if self.scaler_type == 'standardize': + + self.m = np.mean(X, axis=0) + self.s = np.std(X, axis=0) + X = (X - self.m) / self.s + + elif self.scaler_type == 'minmax': + + self.min = np.min(X, axis=0) + self.max = np.max(X, axis=0) + X = (X - self.min) / (self.max - self.min) + + elif self.scaler_type == 'robminmax': + + self.min = np.zeros([X.shape[1],]) + self.max = np.zeros([X.shape[1],]) + + for i in range(X.shape[1]): + self.min[i] = np.median(np.sort(X[:,i])[0:int(np.round(X.shape[0] * self.tail))]) + self.max[i] = np.median(np.sort(X[:,i])[-int(np.round(X.shape[0] * self.tail)):]) + + X = (X - self.min) / (self.max - self.min) + + if adjust_outliers: + X[X < 0] = 0 + X[X > 1] = 1 + + return X + + + +def retrieve_freesurfer_eulernum(freesurfer_dir, subjects=None, save_path=None): + + """ + This function receives the freesurfer directory (including processed data + for several subjects) and retrieves the Euler number from the log files. If + the log file does not exist, this function uses 'mris_euler_number' to recompute + the Euler numbers (ENs). The function returns the ENs in a dataframe and + the list of missing subjects (that for which computing EN is failed). If + 'save_path' is specified then the results will be saved in a pickle file. + + Basic usage:: + + ENs, missing_subjects = retrieve_freesurfer_eulernum(freesurfer_dir) + + where the arguments are defined below. + + :param freesurfer_dir: absolute path to the Freesurfer directory. + :param subjects: List of subject that we want to retrieve the ENs for. + If it is 'None' (the default), the list of the subjects will be automatically + retreived from existing directories in the 'freesurfer_dir' (i.e. the ENs + for all subjects will be retrieved). + :param save_path: The path to save the results. If 'None' (default) the + results are not saves on the disk. + + + :outputs: * ENs - A dataframe of retrieved ENs. + * missing_subjects - The list of missing subjects. + + Developed by S.M. Kia + + """ + + if subjects is None: + subjects = [temp for temp in os.listdir(freesurfer_dir) + if os.path.isdir(os.path.join(freesurfer_dir ,temp))] + + df = pd.DataFrame(index=subjects, columns=['lh_en','rh_en','avg_en']) + missing_subjects = [] + + for s, sub in enumerate(subjects): + sub_dir = os.path.join(freesurfer_dir, sub) + log_file = os.path.join(sub_dir, 'scripts', 'recon-all.log') + + if os.path.exists(sub_dir): + if os.path.exists(log_file): + with open(log_file) as f: + for line in f: + # find the part that refers to the EC + if re.search('orig.nofix lheno', line): + eno_line = line + f.close() + eno_l = eno_line.split()[3][0:-1] # remove the trailing comma + eno_r = eno_line.split()[6] + euler = (float(eno_l) + float(eno_r)) / 2 + + df.at[sub, 'lh_en'] = eno_l + df.at[sub, 'rh_en'] = eno_r + df.at[sub, 'avg_en'] = euler + + print('%d: Subject %s is successfully processed. EN = %f' + %(s, sub, df.at[sub, 'avg_en'])) + else: + print('%d: Subject %s is missing log file, running QC ...' %(s, sub)) + try: + bashCommand = 'mris_euler_number '+ freesurfer_dir + sub +'/surf/lh.orig.nofix>' + 'temp_l.txt 2>&1' + res = subprocess.run(bashCommand, stdout=subprocess.PIPE, shell=True) + file = open('temp_l.txt', mode = 'r', encoding = 'utf-8-sig') + lines = file.readlines() + file.close() + words = [] + for line in lines: + line = line.strip() + words.append([item.strip() for item in line.split(' ')]) + eno_l = np.float32(words[0][12]) + + bashCommand = 'mris_euler_number '+ freesurfer_dir + sub +'/surf/rh.orig.nofix>' + 'temp_r.txt 2>&1' + res = subprocess.run(bashCommand, stdout=subprocess.PIPE, shell=True) + file = open('temp_r.txt', mode = 'r', encoding = 'utf-8-sig') + lines = file.readlines() + file.close() + words = [] + for line in lines: + line = line.strip() + words.append([item.strip() for item in line.split(' ')]) + eno_r = np.float32(words[0][12]) + + df.at[sub, 'lh_en'] = eno_l + df.at[sub, 'rh_en'] = eno_r + df.at[sub, 'avg_en'] = (eno_r + eno_l) / 2 + + print('%d: Subject %s is successfully processed. EN = %f' + %(s, sub, df.at[sub, 'avg_en'])) + + except: + e = sys.exc_info()[0] + missing_subjects.append(sub) + print('%d: QC is failed for subject %s: %s.' %(s, sub, e)) + + else: + missing_subjects.append(sub) + print('%d: Subject %s is missing.' %(s, sub)) + df = df.dropna() + + if save_path is not None: + with open(save_path, 'wb') as file: + pickle.dump({'ENs':df}, file) + + return df, missing_subjects + +def get_package_versions(): + + import platform + versions = dict() + versions['Python'] = platform.python_version() + + try: + import theano + versions['Theano'] = theano.__version__ + except: + versions['Theano'] = '' + + try: + import pymc3 + versions['PyMC3'] = pymc3.__version__ + except: + versions['PyMC3'] = '' + + try: + import pcntoolkit + versions['PCNtoolkit'] = pcntoolkit.__version__ + except: + versions['PCNtoolkit'] = '' + + return versions + + +def z_to_abnormal_p(Z): + """ + + This function receives a matrix of z-scores (deviations) and transfer them + to corresponding abnormal probabilities. For more information see Sec. 2.5 + in https://www.biorxiv.org/content/10.1101/2021.05.28.446120v1.full.pdf. + + :param Z: n by p matrix of z-scores (deviations in normative modeling) where + n is the number of subjects and p is the number of features. + :type Z: numpy.array + + :return: a matrix of same size as Z, with probability of each sample being + an abnormal sample. + :rtype: numpy.array + + """ + + abn_p = 1- norm.sf(np.abs(Z))*2 + + return abn_p + + +def anomaly_detection_auc(abn_p, labels, n_permutation=None): + """ + This is a utility function for computing region-wise AUC scores for anomaly + detection using normative model. If n_permutations is not None (e.g. 1000), + it also computes permuation p_values for each region. + + :param abn_p: n by p matrix of with probability of each sample being + an abnormal sample. This matrix can be computed using 'z_to_abnormal_p' + function. + :type abn_p: numpy.array + :param labels: a vactor of binary labels for n subjects, 0 for healthy and + 1 for patients. + :type labels: numpy.array + :param n_permutation: If not none the permutation significance test with + n_permutation repetitions is performed for each feature. defaults to None. + :type n_permutation: numpy.int + :return: p by 1 matrix of AUCs and p_values for permutation test for each + feature (i.e. brain region). + :rtype: numpy.array + + """ + + n, p = abn_p.shape + aucs = np.zeros([p]) + p_values = np.zeros([p]) + + for i in range(p): + aucs[i] = roc_auc_score(labels, abn_p[:,i]) + + if n_permutation is not None: + + auc_perm = np.zeros([n_permutation]) + for j in range(n_permutation): + rand_idx = np.random.permutation(len(labels)) + rand_labels = labels[rand_idx] + auc_perm[j] = roc_auc_score(rand_labels, abn_p[:,i]) + + p_values[i] = (np.sum(auc_perm > aucs[i]) + 1) / (n_permutation + 1) + print('Feature %d of %d is done: p_value=%f' %(i,n_permutation,p_values[i])) + + return aucs, p_values + + +def cartesian_product(arrays): + + """ + This is a utility function for creating dummy data (covariates). It computes the cartesian product of N 1D arrays. + + Example: + a = cartesian_product(np.arange(0,5), np.arange(6,10)) + + :param arrays: a list of N input 1D numpy arrays with size d1,d2,dN. + :return: A d1...dN by N matrix of cartesian product of N arrays. + + """ + + la = len(arrays) + dtype = np.result_type(arrays[0]) + arr = np.empty([len(a) for a in arrays] + [la], dtype=dtype) + for i, a in enumerate(np.ix_(arrays)): + arr[...,i] = a + + return arr.reshape(-1, la) + + +def yes_or_no(question): + + """ + Utility function for getting yes/no action from the user. + + :param question: String for user query. + + :return: Boolean of True for 'yes' and False for 'no'. + + + """ + + while "the answer is invalid": + reply = str(input(question+' (y/n): ')).lower().strip() + if reply[:1] == 'y': + return True + if reply[:1] == 'n': + return False + + + +#====== This is stuff used for the SHASH distributions, but using numpy (not pymc or theano) === + +def K(p, x): + return np.array(spp.kv(p, x)) + +def P(q): + """ + The P function as given in Jones et al. + :param q: + :return: + + """ + frac = np.exp(1 / 4) / np.sqrt(8 * np.pi) + K1 = K((q + 1) / 2, 1 / 4) + K2 = K((q - 1) / 2, 1 / 4) + a = (K1 + K2) * frac + return a + +def m(epsilon, delta, r): + """ + The r'th uncentered moment. Given by Jones et al. + """ + frac1 = 1 / np.power(2, r) + acc = 0 + for i in range(r + 1): + combs = spp.comb(r, i) + flip = np.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 + +#====== end stufff for SHASH + +# Design matrix function + +def z_score(y, mean, std, skew=None, kurtosis=None, likelihood = "Normal"): + + """ + Computes Z-score of some data given parameters and a likelihood type string. + if likelihood == "Normal", parameters 'skew' and 'kurtosis' are ignored + :param y: + :param mean: + :param std: + :param skew: + :param kurtosis: + :param likelihood: + :return: + """ + if likelihood == "SHASHo": + SHASH = (y-mean)/std + Z = np.sinh(np.arcsinh(SHASH)*kurtosis - skew) + elif likelihood == "SHASHo2": + std_d = std/kurtosis + SHASH = (y-mean)/std_d + Z = np.sinh(np.arcsinh(SHASH)*kurtosis - skew) + elif likelihood == "SHASHb": + true_mean = m(skew, kurtosis, 1) + true_std = np.sqrt((m(skew, kurtosis, 2) - true_mean ** 2)) + SHASH_c = ((y-mean)/std) + SHASH = SHASH_c * true_std + true_mean + Z = np.sinh(np.arcsinh(SHASH) * kurtosis - skew) + else: + Z = (y-mean)/std + return Z + + +def expand_all(*args): + def expand(a): + if len(a.shape) == 1: + return np.expand_dims(a, axis=1) + else: + return a + return [expand(x) for x in args] diff --git a/dist/pcntoolkit-0.26-py3.8.egg b/dist/pcntoolkit-0.26-py3.8.egg new file mode 100644 index 0000000000000000000000000000000000000000..156c7318e72ae4d2e55a132ae4d0e7a6c0b986ab GIT binary patch literal 201350 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 z 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 literal 0 HcmV?d00001 diff --git a/dist/pcntoolkit-0.26-py3.9.egg b/dist/pcntoolkit-0.26-py3.9.egg new file mode 100644 index 0000000000000000000000000000000000000000..6983ff9c71dbc30e5f707729f9a08f3a9f5462ba GIT binary patch literal 201529 zcmZ^~b8s(F(=Hm@wr$%^ez9%aJGO1xww)c@wv(M?$JX8Nxu?#3zgu1F4fOG+rf`z%bmg29#&4+++`-IDXj0|6ugA zt#+o2JbtB4=?b<28pqV1^^|-B;oVuETc@dU*Q4ja&OS!)f1xD?1AoFkmG!@p_3>A^ zte`B9$C=tiQ+^ z%A(>b46YuovB?Ya!AxjjH=daL*lCgWN*f?|UDS?~^6HzL1Q>T0W<%t4HY{C^?#3u93*zFX z)L&xBJPbrsnjVA;BA;N3VbWYPm_6+Gaj25`Ht*RmDFK@%!GDELf92{Q(PXS;tMx)B zjf24+xA7AR1B#ynZ%EjDKr9LofA8*9lMAqMWatbQd*5Hy^tt<)!~eRA{k)L&&nj5W z_P2WZ1#3O&KwBgi?~c{@>d64zBcGR*v*8M&@S!L+d}gRfzCv zIqKij_5b95XgQlXxmh`z{V!Yi(+dbeOmLwG9|F-Fwy=?8albAc6GxS@+;DC$z!??p z^W*=hRBA&Uh*76AdtEIA-a$OmWL62W(9>aKSZlAUa|QNb?%W{wcEl_c;x<5LYreRE zKNgT7HZi-Od&AAI^3438@G|9Uwb>M#7JRUTdc$$;OM~|aXR}V=#CPzjRuO)W4mq7Y zLHfm5&W97+Af-lBmEixA#9Qf&b(`IRBR= z69;>9D+`x@hE&v-8R;1o6(*;b6lQ2us4D0t#^+d1mDT507S*I?6(*(u?aEa2(o3yC zk&e{V)K1)(r4(r;WmLy!)UP0o0PCippw6Je#MH{d)WFW5#K1&g%ydy>>iH59+R(%0 z$*XCx28lW#1q%nlwbuZmA0&SZe?o8_a8!o>pSQFCnXoWwAfW6`pnsA44{z-qob8NU zt=!H2`FqW84fEKw?1l^W>9u zI=9xoVwQMwjo&Z?*$YJc5iZ5>igja3Pscc^rd+zlr@z-UgwCr&erTO({+WZX-`MQa zoOPFk4tTz|XrpTi4LEao7JNA5p-j!zRsJ z1JEVGPa$eq2@bYuKXLG8aa z044BAbxl8IJW{rFQns1m!((<~t(St1lyQpAH*=;2bF~1{Dpju%c=NIxEOO54WMgln zY1Dx_c;D)ml7e|zf~NbUU;a=%{n<4hm02V^a8xpVq}I5gMkZLKgvJz z(Q8~&y;ak~ILV;Vq7=ER!8<-~&e%DLE&X_ZMyO8{&moGw>BM4TFgTo}YP zwut8$IWT4}^pwn{RqxpsaqDNfOR#Kk`NxlbF~?9cc#{iGb;c2(wuC4RnA(Sw-0tfe z&Iyu~c${ZOL`|BIE$s)$ovzwqCj@!S zJB8oaU~Yu#V^c%eF#QeT*;TU`XoledJEIsHVHo5HB|f?DKP7w%8reQ_N*>N`7KPZD zW4k5BIgi2DC=N7s$DR*x40XivCsWLW7C=B3xwmxzc{J3>8r%9X*1v?++kr+BK?ER> z@LN!Z4fAt1j4KnBia)~rxP@KePud;Nqti*zftHC|^=aMppqM8hU`77=kbnmDP#3w@ z{v6w@dp~Fy#72M>kw#A%3aP}W&@NCPw z`YA}`P)CO|SAt+Hc(!hew;}kv^_uQ2a7mJb2ms0j3G8Y9om!bNiU>KnT+$g%AXUQE zrFVV`k-OsJQ*x{Oy}KD;=cc#OH7OD?bV&vX>};5m;%rvauiks9Yl2~CBtb{m@UpBL zmIS&7QcEk7sV;9_i9kYBG&8P}ZHfj#>zzuigvjH%YVdDl;N7@%K}}s%Ig@d04mwRf zKed|RfkH$luOrxQyK~()N8?7@pOw--JLe;p)3Va8r zknWV~M+zS)6TOO_jdUMzaEMCEGX%JN@-iErepPes_4<8$RlkUK*O7unkc7s5O9h?{ z;5Ga*V?GlJ$oRBNPAh0gmO7$EvO+nAP{ooQ3^XHoV9R-fuw)FES&d?smxs(_Lm6f! z2{IlEkw7jQvsNz-4LmYB@ysjb+xdTT@2!PSp_#hffF!!M_yMqj09g$O#Lw?WMC+P%1U<0p`&m*o(aXY)Qs{KCA z|4GvJRQfI2Vq2k9chyvzSXwZ@B1gBX1(W2?;%JKcbZjxt_=5iJ(Hxd`d!7$^%%}E! zHNt(r6A&9osG|Yod5y@nRJAC1Hr)2;@nbOLTkcoN_3zi-`jwbQLptL@`s^Qj0FjZ1 z{fm7{=}Ec0A!h|Z4Q;ksk@#10Z<_KtbtuB2fmNuUzf%nSbEvUNM4j@F#CbL!@5(P) zw}*?V7Md0Q1d^N2Ssf>=L+-dg8~a~oz|}kZU)hNDw>OYaFDvE01dj~Zjv&rt!U^CY zu1Z^Oup|>}nHRR=(+4&;uYcPw(k+b^AWoY{0?C779a(4(-KQvX3)`ht<3)+VD+S)6 zXO&QP7}j~X^R^9r#0VFUUgff>2SzjIAL{+{yC+gm% z^E!0LwXDG~4pULYf!6dwGp}zs2=?uIFg(GvoL;Ehl#wDXq7c`Z_oF$PtLFu>NeE!N zTqw&)~-e?-ZM;pPsY7nIhpPqb*~ItUO@w6rwS3T9AY zuI)Id5n>4T^)o%;?@0~*|(O(m&=}T|U)n zay$m}Om>9Q<|6(9@y)0|$_D7)L;WkK+>(p*a~W?fXwuG2K`QxTl0P|Q1j>!?jo0m! zGGSpv)u?9Fu%Bl_da)1+ap~XYakK>QHPLb#L=Vponk~l4a=QJXnH&`TNT<5A1>KK7 zR^xmes*w9r)xi3%kf?Gjnz&+^gx8pcw2>uzw4{0rxTxq8hce@etO5*$?~P>1Vb-+1 zPkh9{Z{t2a`hJ*sxN+m5puDAb!?Qphl>rRVr&Fzd-R=h2ZEej*kW10an^tN1PW_C? zB1&~PdAI`eDc!|?ke`JS2f{~z=Bx0?VD@bBLBNz_hp=3Uv@g>xMU9K2=nN^_U~Jl>9t30nGq!*X7527kDLaH z9Nb{3xA*S+6|vF?=h8Q9v>`7zx&7F4xZ}n`C+&DrBF(_AG*|}nNJV(5uO^?h6%K5O zI)O7oMO|*}^n}2r&h$w(0%{5*)ju};I@%!ra;vd+Jp%`NL)ZP>POFwxATjnYCB1CHD456gpG3VaTFf!t z<((QRuP3(cR0imBe6*au>+bmw76J)XM@dBh&7>Ho$U{1SIC`b?c>$7=FZAG()F3Hg zGE^(oEiw#xCyti;Fm_tGmCs%2=);r#LXq?#ufq+g+CjVUXv&i3`XKd_yUpzU<; z#}Zw5TXJkSbjmIQdSq10(TL26#ax}eHFA#z4BizQDvmiC&Tg{+RFgtyXvxbs_PM1v z;Hpdr6fU%Z$h$Me+`6z<1EEbfb(T|0nT6%jV+1fp3{}kq2^wrM5_@fN0-J!aTs_p# zt}0BM-s6ZE>RY6{wFoooqZR31zr- zNq}u(TQo@`wOtaZ%L&gC1%J!<%qTmC%jT~!p{>aoyw7~O=5r0IPFe+_nII~HNc$E) zpO;Mz5{73@9-L~-^=O|MMeD2*<(b8DQZ?~bMpy6|T83){?iwWCl2M3O(CUcQd{a>{ zY@L*^d?8YmgdG&+#&VYNE3rbpXb!ks`mFMB4piub}$WRy5P@h`)Q2<^pY_o*Hdpth}h5l0n3 z=!9`fEl{e2Mkw$36WEB`GOy2YLIlH$%%_p)Oc8%En^F!vxs0?xWt}(Gyj3U}n)7Y) zyviiO6?2WDoXct6S^&U@kk$~QFg*Jmpf6y)ml;9lu5A&Ho&SurMB7pka;Bzbn(6*y zbr6^&di1<)-Y_r@?l>(lz>l7U*f8bD@{{@%LKh;BUAj5t2=4jOL(xG?&4G^LfW1^; zq)GQkMD@v~elGg#Si((C1B1K{KN5S=P6Roum#)C3 z)l?RYzv!=vh@uQuEP5Mr4MzoEv*;RaG^}*2H4y5Ufa=gw9q|J?P#D(+I1f0M^JbBX zet_|SI4H!9W_aN~h^wpHP$THPUMxu8g};4~4!}nSrO3K<)U)qEM!&QOdB8C`o%a0q zP@86$!laUHk)El|Mg%7Cg{_p-te7fWgoIr)QP3SEEI;%eBrDXvjq6CM_ zFhqf#)Dv1+SZGV4Ev7QRolsSV6FwSY_eyX}jqqUdprvCYL+ZyCE->M;FSyyGWeDtF z1!m*i{j?|nykEg_rFrk8fNrgX)!$wjb_&=?XY^D-yZkZ`G6}W5^vbJAW;rP==^b^Un z`=~W9t%YN7%E}9LM9$r!K!4HP0*m0}YP4A(p2`&XI+QZdA?qdiEa>gOx9-T*Rll9A zC_64#Kyclwvaz2Z03{SGE);;62-sO)o|$;idASZ%c)6=gUD$B8(G&sGfIB4_Qca3C z@U<%_*O-jnpMRCDI-sJVX15zj9Ayf&{nim|)2W082R__;cRQdhFe48X_SBMDB~E@M ze8zIO9O$+j*s951~P4(2tBW#?48wEFP(VIN8o862c>Cutm2(0Pz&`eB*)k#Qfctg&ggz z1Gvrz!c?4iiJqBgDPlT*Jn!3a`}wrxT0F0_z6fe0LmCc`M+;N=BAX`l`)b>liXP+% zj-DDetO@a&_w_~tNqTXa7$0n7B*aI&b-go2-$?oiZng}yG<8)wPMUHC{3+niAc`jJ zctH+8eF54}TW=ATEdx|D2(TyC*5E8Rie2VK?vA2)TO=iKgOZ@}YU{Sk&gG4x*Yz3$ z<%w_=TOzIuQ8m1*fQ9Q^HK(W-Nb+}4J@wDVLg$O-`k3UIx4cCsw3E9Hgy%DUx>6OY z?@~?%wp0iQFNdcWZ|x-V2l~%=qKu#dsZWYOwwI!a2sEV3t70`OW&io{v^f5_I5G^5 zrUP$-k#+CCbI?OW<>~6Z$Ydbz3B8)dqM$+^nX%$`U#34?j%G>|_` zFar8z=zP-7fecmWOW19$KD%^e+amhF3v`BOefI4Po2fGPmMUT)R*L9ht<>0X{RSF& zY+Z{5k2;=3=lswwX0c8|{KrkB((w!|YVRGvbm-Ff)*_R!(-a(=GW0}O5+cHoK*J`6 zkApD^^@?Od=mleS<9SI+8?%c=`r59kX)ueKYH>`NrM-U~wU8Nfs@GGMM%gtsfN-M# z%gU}Tlc-bFBXunT5!UGA=?58}>5{okZU!3zg=_~e!q2|mY{nnpf321?dWyKqTC!?; zk!NBO@qCx(7hN*9tyP!v>+uvgO4Lzbf2x zO;82%m=H8%q&=KlK{jXgoRW$J;m`oI%BC8@Fw=qxixk2hzFg0Z(6R)mM2xff{Ji!l z`&uf-&q#>|sUDgiJOsKz0j>S+qGu+A(KN`s`J{LQs(`V-U@?{vW?}Rkz23=*SD4)YvCqzhjOpA z4e-bK^lKlRrYuNt>9f;zw9VSaMO_;R;aAGl3g&;yTEr15E;55-SLemHMA|t;Hxk^?1t{+^5yOJN@3CA67ci7S1fR#p=^; znIZ~(nFpt)O*Mobb*OH6o=&3htXe@wJ=sm?Y_@2&7|sON2^xSQ&hgn_P8g$^fzu5$T3P3?6)p zWjx|$6{J*uOP8-Wue;o`Z|r`;Z}fj&|9m(cVj;2=JRwq^#mXr9XZMH`#w$aDR>!K} zx=F_B7=iR@J^gkid5Ziias|ehPtF)7m$|rXkfxQWx(@??atQEjyY*|D2-&6|h&JX;-csodM>fJwvG(8Jg*#{l_4t;l_8 zopJdgnjrm;cH(PA_$=iiuj^Fy;5_dr95T!SN6eBKCAJZ1N;`L(8S(gZr==G@!2;@n zX}@5XF-E0`Pw{I~f#(~ep>LQ-xar8VI7xsOh zE2}PdTfjqS!(ggcw>Ixlj=cOAi&t~j`PN9NWh)u`gDi7mn}boV z#vg&K_`nznr^B(o0~-w0fEcN*h`%FSm9RnM_!cwSCqPKiT5 z-tb<%UULomYg+7Xhnd!o;4Or&t48V9-4E{*NaFKqpeyg@9r*U?DnqjXARI(AWwK(^ z_Mae=-bC+AAg8ngNFX@I`1K;9E#Q93{f^b~IKz6{Au|-Mrb?I^Bt*3S(1na6PJusL z3OBvAY0f3(AdeA^Jr*8}pv}04GViWI0*?lqQvYVnNGu=`)^)^AD!VHYUucZ6&n+V_S6GnRL z%m0>X+%yPR;^2$uJ0 z*3`C5EizKtxa5V>!t%;2BMY%eZumyVLy+3_OYN{T34Ob()!HfPp-RYS%no$|3w4j> zsG{XGXul%9*x0yV3(+!4kGk17HuU5D7_v&;>I1ArTB$n3Bu;Tj2BL3M zA4$uQ3>Iai7Ejj|42(5jMKp)W)w*8zHPG$H1i5J)FTk~NhmfO|wVwRXqb^lJY}5B*tg+uZGAy+d%ZPalg`wMuk6xC z>J;2)sz5?I@f=t>7x4=$K`^W#0q?rAF1G;oUed$zGoE92?ZUMu9#e5%OZ897w!$~W zsH;^RDHMU{X>Ln2n$&rMMj>=`K3Mc!ewI_*9ToDl^)0&qr#Pj)Sc~C1`zNg+|El7 zQahpacN$E4&w)T7+GIwEvt;PV@=ctzG+jdH0C0g>qTe5iPwvqEkX1<@){tka3uZb zIClF`n~nwDWo}JUM~R0Z*bTDkL`q?YsHOUETmm6?2ETFnn4CtEUEE{bi)BAYcGyu@8Ty(PT1 z_=Orh+C%m5x{xfS(K$&(nx5p}+SM{!sm*X;g4jMB)qVV)O4l;as+L7p6i6yC%3Y~}- z^F& z3!Km=7RFbxNDw-ym+$^X!vV?FXHXA`?;BHzh)So1Di@Ecd)Sch!#mZ^jro(q9y4}u z4A+jBl3~zPrcAX8g*|TGlx@PX7x0{;;rfa5X@c(p_0jL|O}O>cPwu?5TOE%&wD9#V z6ceSBKS+7f=RP~eY&%B{UxNZH{Puinb?J* z|Bs*>nV_Y^?hAz}w+oDjPGth48~1B@NY-a{Tdu^@z0EQCTjH>)(q zsqY4@+VQgLlqAw49G?1UqkubBYS+JJO`0WtbfSS%+FVMMb$OYlT(eBC|LWWD2UabN zDsDL%CjO1%Sly(SmfRT)c9w#d*i&Vi56jLO=)yAjW~LojvrUvf%^2-Ed!p=Bpg=?U_4{qOfw%8l z^_M)4D!69G%ZPlBe|jV2@AueVci%se#tY-o`bTwZK#=ES`W=l5R~~2Mbp)e=N|x;x zNhuaTFh7=j&|6#*3cu5U*TgGk*4J>ND z%L^PG-lDX|M)Yr2PDl3eo38v(InN5#C|)0)9Y7_$7?c8E{u+ziLJ_x7PAb7PrvJ*oQ6J>Ks5VhMy2cdpiK@)MBEfNUfl=5E3S0tsHxPgoXY#vYvhPeTHt@>NHFiW1cPyO-t_3Q$#c6V!x_dm%e!L;TYSqi}cpwH1UR1@(jhFPXHoCPi^6lxP;B2)oPmv3g47HE?ED65Nh3Xmq zP_dcix@h|+>PUEunXA$;>YE=mRVL&+nW;A>d;fiP|>Emj;RECJ9e0Zuu*so z(bf;wt(+eO7X^k^BI2Y&XKC)65t7Z2&2_;qQMdo%Pv~Ihy%X25E}D6|L*c(Q8{ zJ#}G7!`1b2y3`+C(34YgI4|M$J-5#+@xWqwj|t!Xqw_*uj63H*Ah3pv=rFScoRnCs z1>e6D&)$^9<_TGlD9jO4ZOq!QI#F8ZB9{Gt4_%gP#m|2)J9JU{J^qcX3`*Na&fSnd zxS?uOH6Wd4z|GwN7_=(z`aBkL-gGU4lsTLI3mxQtM zu%r~8oFfcOkF7rlTF5jOH{EsC&~w5~m%HV1v1#d-BFjS=7}vL0>kOi&nN7KtM1V>2 z_>u3hPtx3JyPP;-9C(&Wgg=wgwo1YO+z1-CWn9C;P5Y)~PpnWgXmKxkK1}TKD9Mkm zbm~O|!nETeTXN8pY^$4{9=%!>S9!rMNLJLZVi^Nd4-{en zzjwZ_eQZ+coraoMft&YbW&^krQ+Bn{V8X~NShN9x(QFLES|M81*Fl`)^UP;k(j^LG zzc9Xu$7E69``~fKaMOCJ!!zRu8Lq3FVriRN?dZ~c-_?A$qR(f!!NohK_*V+obqq>2 zqV+yYvyR7WqR0>;K8AgWu{P>1NbWo(!sj}(Vf{+%>awO8I&HM#nCIAhh+|Fr&br3r zb4g=I8>SWAW%6ipgJiEpz}o>y`wJW$B=o{q{I!I5Jx_MedxM@+J5ZZzx@vZgU_~<+ zvU`_|41#$je_X>9BMQae3EKN(bWhd#@t&@Hjp2r;;7-ZB9#Vx}WXzfV)L&qwpUE!A z6WsU5c+AbO|7-Phj^nFvIX_TE=G#~=>AouEds-%M61;Y411X*@g$y2Nmq)slhPPJg4kq2unRz=ejAL{Qdd-Ue-Cfx{ z34s~T25gRiW1E!G#BUYmR&KI3F2+an-NHkiRetT&LY$8dI$U;s!rgdCry43sAZRfK z{OYf~VlQHjd}|v{FveGbgX#9&L;O`RGeyQG_5~^e(z=A+k_@k?e;EGmn2Y+d|QDgI~ z%0eAk3gTT{zAoOBJr_^%JKtUSjZ?eo(ZIU&&!cSuJqhc$me}iq1_xfr2WCGh#m1PZ7g<0WTbxfW}1$H%M6+5hphjS{5q~S}hab z9Eb;-?MF(AhsyzgFOMa=1GOCjUaBuffFfZS!isGE7V*z-u=lkauuuDP7Z1B)%ouB- zvd@)RGs6|HuhMH;(`M+V^KT&d-0JM+YL_>g3fXtrUzi9H$Ih&pd)7XpzIB(?b3T2{ z*)PZP%15Qi*4#p3uVB@24c01HeA5D#HI>YoWEj=9qyg2!YFW-d!A1C%dL{lxsvvhKsVE_3^4>&8*JfaVzN>RcKiM{+#5?=}9E5ZM0k>C1 zX2T=e?mqcMZ$?q`ZGzw@|7s-m*xM{{$?s;o_wJYJh*2CQ8Sz) z)#kaciVU)(@dYG$UtMCuF=l|n8)=NZm?FsXa=0kXVM;dsDuA%Tg**-GG2Fx)`QYXYNfecb)T3LT8d_()L_n6$?<-01r?0MZun03@lH`c;AC$% z7u*^#Jd`y(Gb@UWOB4c%Mk#3$j}#G`<5Ir3w%GeO()4w~aol4G48clGN>wR!zVgP~ zzwD$>&HKod)QHi-N*F=DD$+$ImWwcJA(5D+PI!E`%g`qI;IkuUgQtg!6l~H#v6s!LJh4Pl#9=S8U*`$t)QHQ1 zS_vLb8K%@Or~^btP?j4iD2fBe-G*K6-20#JS64uOSJdcqrb4);18T#++!eCB4-1Th z3AOfTcelu*qxSHTEnoQ<*6Te6JalUu=^%quZBIjbh(Smg=>*B4p9qNHQ2I#tlBnT9 zP7&ujwR33U#c2D|K%b8r$;b&+FDsFd>e018qM641OJ7WZmGM7M=6NIc%IzzE`4BSb7Bq8dZ&E}v-h2%5yvKB)dS$}K29i#}P7kODf1>;OGSOsl+o zf51b?l?cN^pyRa&lSGhtxP59Uoea4-A^Yu2T>N2yEnm<{I`7zT$-Ajkuq5z`N=K73 zJqAr9l(M-JN{^rgBO~1g+4%rn>wM#+49s*4tq~B}4D#cmaf4&LSb|$Db-5@}-#z+M zGXzOv%Vcj1vgm_YAXJH0@fu2GgHqiML|~dnb}ZdG;SbXF8*I#JgYAfzjRqtZJ-9?n z7A)hTsGxKPhQo+!kz+SxHEQB`MMhBYtGr0)d#z9yj~)&ZxcspK6ggGNI^EENh!dPp zBsT!o*{x&BW6o5e@{#i47#+An@Dd^DP5^!Y2 zf!IBxtavorOWls5CW`lqGi(BiH1i+BUP}-vdA67xET89besx12+FB+o{Zp0V042C> z(4&;{7oZ^k3y7B@p8KQ${|PN-v>(zMCwa)`I6u|*f(cFs-M}vJKVG2<+Hoje`upMp z?M=9t)nT&=7lRfZ)F!9AtlP0t>5_glmw3L~iH0WX#I%B@PeqG;a_+?-g|`TMN_2wC z&)VX6ofMAw{I9VH*^DuboVVF|Yk92?H^f&7#x#P2``)=et5niuMBQJxE~Ulr7Kz!( zAk}}&A@lJW^xmaBg>FF7Tq>1-5&6{xvqVdx91u? z3`KBUSXv_!Z^z-apc>(Dm;j#}rq5&f=%i*b!(9E$Ey7R`BX%Y4uD*ff3ii6wHEMh~ zO(Diq?Z#`8`epV8;LLJ+vo&QzNdV=#Aa9H%u4roeJ7}&H2JdliDuKu&u1-KIt|ZZF zY6WPIuLicllDpkK79L6SHnSRRN(cwkT;aAFS?n3hxLwNhv0e>eO zRNw40V5Ew)DlF#N%0xr)CqnemC#I!e_2dc4J?jQ=52U`=JgyBlBnLY7faO@i&dPqJ zy`xs;#of*g_95}AW&C=BpypLI+McpZcKDsVN;Up+r07fh9l2Aznm0O`9F3&1x&x~= zGKc#<3jL!l*U>Go+S4o`_0W)OzAC9Q559G=YhS!n)&Mcmf+%kDWRD08VbQG3JYTCF zoCROmws!@|+p#liwWR67uZ?bI!}s#ksexIP{sv^8s&;j%*`60g5BskE($m|eZ8H;W zk_<7yuP2*RxkAvbW@MQlTbs18tfzGU2*Fw)BX;#G7RKD!hP{+CwPxcqPZ4If@WxKb zR4cB=@M*XpPiP6%g6?=dzN<|pT*QIys*XxCJEX`nHZx`M{&s!6!vFH<4*WCRi%jtB zrJs4co#Mw{JH&aaiqbuIQzj!9q)JWHbqZ2Z4Meo`(gt?{e?JHE`JZr8Wt)$wlb z%fuzGYjKTH63FyBt3;c)TiZZ~8&`F`QELlh?Kw^#t&=tbxNkP-nxSC|yWRU9^;5+1 zTjq2_&}bvtSj19Ql>{4ySyQghWv2UqDrk4u?534@{#AgqU`P^ZO1;# zE!*#$Efz->N1<6MS@mo@L2_Fy+@~{brQk3@5q_cf50s;@qCO{BOt!5xj@z7%7;Wk8G`X``@?K`VdVz7>U=_}tQ4vbA5!$i@$oQT<3bI_k2bgF zlxCAVkLvvLlw<;%)u_E|t^0KX6S8?FPxSSIFzaXApS(1`F00O*7Mic^)&f5ThSdld zM}VJl?F?RUq?%2~wW7neH;lk7crmPBs{Vn6Q3vp-%*dOS{3Ho>wEF#GXC>e7D(|7$ zfbOTIgjTDm_sM(Jj4#;JmE?o=sY?bg`+_%bXI6ueBXU>z0s{&$V8l7R54grto;~_U z%e@zIfOteG zmrY9^HrTwwOV^Xf56NH3ze^dEnaKBN$hY@e5jG9p8|!AMl#8vFNap zkTw|@#ig5;om5N-m6_${OuL3gdM-Bw_>Zn*nrk16A6j>*f`I^#$<;sAbQsrL0H< z8SpSss@$uVztjwCx(x@Ha7OQ6Z0%c~ydAyjm+PZ6$9m)!Z$wf9Cml z-BiKo(QsaL$C(!yX3OSCUw4G;R^qk<;;bmxsQ1_{o;nFJP_Eqw9Oys)d3;}M&b|b7 z_T-N@SdyGRc!O6Qhf=JSf*Wal8GVCkqWtp9VcS(XrR+77o-gjJF16bfHZlG11&b#l zA26Boh88#0(i5KQj8#9Z0f~B1oAEhI?}teis<+C;?Y_hL{5R2na|DmDyygkt<0Lgi zW(doF_9>44J!LeW2~wzQ^y0HJZq48vHoN7CADoW1lOWK!pZ4X8gp1R30(U6ie1POT zzG1W(;1o`C6u?tIoZwjttP;bH1b7jUeG zM8+5x5IQ|Y9HH1$Osv}v1NMdjrHRpt2coJLFDW_@bbf*)f(-^KTEStw;u|Ltlt`$~ z4>mCN+H;~W=@dH@E@b g1>4pKF%<6*LEwb_Qxsy(=n(LO{DRe@5)Tu;7cN0-gL znZcR=)ADd3Y{ziO_IzlVXi(_RAo%$?wNdHq`E+-BG)Fk+{B*tNC3vYaw|XO}pW_$2 zcCaj0hJ;A;!DyfUt99kN_HZ(J7avo!dSo;gm4F`xb&(0aqo3&&CuJb!&o)kxyQh|J zIp!C5hr1zI6f&yS^4=lH^mE#jXGU?86WZSd-?7fq*(@ zfq;nqZ+)1fk+YGl?Y}X_|2<2`m9+Oo%j9GyEn_;8=c}9R?xxY4DNWveG;N#X8^h0l zE+>L9hEj=CVt2i>>(KvqG#Ch4fNm>uEGspN8fnLp4JQ@?$MVYm*_kD?2QG<7Hkr!9 z6Pc2cbIT`_g7M|~d1FviAmGL~u|X~Yyc*r0k!)sFn83iLO`fz*dt~AS?B1tQyHPGJ zOczSZ$kTBIe5)oXp_$9*EosZ3nCr+W&s>LtPUS3tM>*rrROL7*(oDOC#C=d}*1@GM zLL-1}25Xsf+}Hy#DIyXcgXA^LLU^{g*LUEK!-5$tALK9^U8tOnHUegFqe$vEpFHXy z3>^lRG|}9~${YExXFU z)W^tp*%0R#owE`nJ#nmw@ri^PQL0fhG+|xBt#I*xG9nuVsFjQism#J%>>dovORjJn zlfYMcU38K&>>wY_(aa#JXgrTd@K93>D%9Wm-L(aqE!J zFRrconOMz6NZ{Fc*nGy6V^?!dOy&;j*(Be>Vtj#c2F?G+NgSyz`Yk$5t_&B^{lli zH7}#2ba8_E$9g$D{o0Q2&_n59G<`hc-GSx(zH#LnyL=emOGg5DGQ9dTKaf zqER~C`&(-U9cKpz#<{j7eu#JpGz!nGx;mj4BrU&_A1>j@=q2cdOk?#oiOxzN*hB_F zIgGo~TeD0e%5y={>$GUkteNB#WFW;R=zH?)gDq2mF~WzpjRWqjqjuuG<16|>-_(Dd zu1yPcNX0;zagamuzR3Pw;Wzvr66jSQs2P}*j5$eCt?ST0xkbEGL^g8aag^HyYSi;) zzQ`~bL(_-aSS`8fi{psTo2~Zx@6RWV2ZufXhl6a5Js)^C)eMSM#`#rb117(zA(FEP zQ$3}06BW{@ap3?gy^m*6FJ{TG6@icocBgsRRHsI(Z!hHI180Uq|JBC2Hykt{dTtt) z#i~|BbKSxu^P9zzIMj~^+X7c+!sR~E{BU6+F}mipL78P{K7T(qRMT{0ww>aCXAA+p zMM>XhkdY*yBha-<09BoLZ)JJ=z`_pwKZke)9*mo=oN&< zY(20T51A5*(f)X`CZRjK>LtAkt}n?J=Adeo3PO+|5Sutolvky_vcV>jvX^FbI4dE8 zb)UGYWVhi@D3@z~u)6@Vo7TXYA>PYz+I5Epfa#$h1{X8}kUcRB&`Qqf4K)h*ZYhRL zn{yl5VZ&I)!sd^Hj8o%}D-fz~DIf+4##N?cVLV}{14!Q3TUd~Tq@;oO>$#A*IlRCw z4$#BRGuBP5zU4P~U-L(OL%N`blOma-IXhE|HP?rJbp;rt0c~PkyH}~Q8Yb&v%cIaz z;V9ZfrpN<@1PTCo8$&HVg04dZ?PRGkNQCj&`Xn&LMO+o(7v84pF}e?e;)U%fyB&^8 z7{FjVR%;pvS%6AldCXt)L8Y@~!&8DgPsUv^D!w%WMZ3V~0vfVqvv>*?lhdJ;}ghTS;AeXj#R=-~#K;+O}=mz1y~J+qP}nwr$(C zZQI!0cYh~2Irrq{Chy1kxss7e)?9Pcs8Llj<<-V}XY1Q&*aA~uiLk$h7dX!kcCadA zS)@~v2DR0;+~<#ms7S#<~6)6W=`q=??#q5c@`f&{)RYDdyLeyJ>V(ABz?pU7|ZyC!p z#@HCVU5s5`_yS32;QP@qZ?Bn7RdQh*#!1Rd=b8cWF0BRz7>Q%emKu<0%1WcQRtjP7`%vc7GQ(``nWXTGvGgWI2 z^q|_a5HITzHVaCX9-WDaEzPP%P1&t)`0!et&sTk0v{|z5FL^xli4{GKDtafJ6o~lpUZO$iuG}OQRvB>-(<*Tjuv=0z=@pxKe<>AJ|4&_f9}q9% z6d8zZ(aie16#0ICw3Cn)A6-*oXk>64)_BE~0 z>?@+!)m*e~T@)ej?ywKpS8dst`;!`%w-0|-uWPD;4F@o-c3uSyCOt*mWX4=%cKO#tvhnN zT%HVeIWpOyr=l!&xz?eioB7x8;~H?Sj@L$9Jn1&`lt%*7Amd`coQhVyvRUA}{=Iez z&ooekEkSc^MZS=T`Ly#$QUmjI=wD-r@v}WCH9ifblP6<|@8#krGg}Gn4a7Jrv3cNS!8P9-O&sn5&0dNi_f+m<9GRi=e zwCy~uj~jAzvYT~)D1fkCN7rJW_GY^H&ZK1yZL^xMz?*S5$9Wrk+h`$R%GFg--y)+y zRd}z3)F8`Y{|>=c*q(73tGUUVtf2v~+Sa%x5rm^zy5x-CR}BA3;g}JwuwMNsFGZs%x_zU&)8D7!EvbbA`U&I3d9Xg3 z*UhR5pWqGp;X32BwcRYklr`aY73aIEk^2i9+h=^)W>KMy%js>s3WZxPYF2Mginh*i zY6Uf$;t(B{B(eECtfSqShPJos4W^!7OVnpJZgi!BOSqF7{L_pok9rk}Xhy{XNH z+gp)6W1MQ<7W53b{PHBl=81_+wDF#EM?RvzYxOGK6_GYx{VkVO9$t9XOzvA~OP6>i zoA#gPTTxxC-K(wBy8*?MbgDf8sBPPm-jd-bvd0M31U+Bu`t4EBmjJ^H9ORaML_hwm z?VDwj5NcxubzOJ%EpQ$0tn?dSlv!-yNbbtjYg$tK<{yx8mKxxJ;?f60^+a|M@WdPP z4{-2Z9{1j*Z3LR5o=bFGA&{K7EKnPS=1*E*k%TpcQP)_L*Ped3XjZK&3*2%GoJp=8 z)D!KfkFq3E$#12YlcbrDhI;tI%%xy3dX!3BshgvX?F`ZB8*2{OTn zqK)*rq+(UY)S#!xGWqi&P9M*8QAcv|52py=bM@yAveIVeqWbs z&!$G6X13ChXGsvS<>0x-=ps3((OI=w83j0cOclRUuQP$xT4(Wq#K_;mgAEhA_Q#6m zvL;W>u_hg=VCBk;RfF(K&59|R{xzmwU+B0(@$bc_Gte*3!LCJv5INv%bG!K(Sr`YM zA4U{bGu`aY!=@d!GfIf+bPAY{zBFa-uL+`t%fJe7^(TxQX3SBo;w0ecc>N(f^QWex@ClGjh6?sZhX5M`2bru{WH1!+7^cC>cs$VE|EBB40COK7>%1<^_5_SG$16aufU0-1h zzkOuC(4_5WDm!K>Q)hvA)5oath}oM4oxCT2I@@G?pqqP{lU7X{ENH+AS%N5KR-!=g z1;OC4ySb5UD~N~LX{sI^GfKlvD?RBT%NnF|-0?zkjIA*8K75q(q({M}o_efkugEu% zH52SF;0-P4offHA-RhVD0H}u&nvP9QH4JVET&@xdd3mm`F1(<}&$prz7 zkPiipI?~9YWiggP=0o>5(g0kU^Q%fh|77Q0-18f$3BY$2>RgBJj2^&TTb$dsHvJU$ z=!P~6b!0xPLGA3~VPUcE9gbCP^oC>C0cH!a@oX_d_vcpbg{bpo`k z@D1}>%+j^b>R^r#uFUo+R>cY-IR)w3oL>8@z?26qa8;iv)jvXDs!(IfXW@Ro2Kh(Z zfqvC4Z$QH>9C^?R6+tW|SetpXaC2Lwil{Ux8VbZj9Dwn3{ux2SGJ0@TOXxVt-x_f= z1EXenpc=6OL57hELcM@=Gmr^Zr3SuHQ@(2BuW375hW4-3!OTM&hG{ zWbaLytlTG9X`y64&&_&eyXV`P(Q#}G_Q!cRqd&5#~1j$kQed$M45i zb@lfbkkj51+o2lk;~xBj>!7mAq}& z6up1iQ@DG?Q01ynxo>KBD^xm`1F_IkbgZMUK@(H-2; zO~O5jRBUn=JoeC%eZmP8M44D&>Zn#&-vW&yM1-xk1 zXP3X)7Tc}F{)2U9kh{{5cQCBM6)C=llT-&yHshw(_b}J8kXA8;-@vzyP3PzOWZN=W zu^N10*r17?N=*}>EnIsYV3nPBl$=JCMm*C4I+{!KrlDe`g)ks~0f(4Ed!Yivqj+(O zxpGhxg6gcm=e!mGD*^pZTfBZQJ?*+RUayiFWX^0!0)-H4fduv;Padn`o#Ee@e$;!X z)e(^c+SZVc8*Aw^sXGzh|6=+?Z^hiIbv2HA(1TD}y|~cSgus&x)6%R34{3>*uli9D zDHrzqjGD{3@encVLsBMUkEd>HY3S6MHJ{BJKc3CXvSN|ep3Mq5Wzkfg%^A3;-a4>I zNm3R=MGEjA@e8^!0p-!81|YM=`F93GoxrW|I6f(M?+8r~ZJ-xc`(s{ry}1(+P9^<$F{ zoD8n$L^`Q1i4JCzoF43yNVGC)#Ph@=g(8u{RVWdaC@ue0JJAA}$RN4MA+I8dX7TtP z*p^PDvxaXG#9x^%iDXw99)IBu;r1Eze{U+-<|^R!Diq>~a}sU|M#j`C72*i1L^Os| zkxG)^k|>bc_iTL24)2gkrk$iwJg?r6DQq^p0U(_q9k3e3@W%;vc!;{Zi$*vSC=_mr zH!QeFCI^*MDWEBo2wx26kW_-yvSsaoc6A9~G=3wcTvN#}9xV%Ha~4xt*bY$H;Au!^ zicnf46&DkU(<-aCbB<70Boq!43s3MEQ&^x8*0I^UK`N4_lrD$XbGP(H`I6&BE zRY_*e@fn(7uH<8fjD`}kNiw0L)yft$EGUPR7MG!5rL-o^W2}1$dGwTaLB~){6mQLx zy{S~?Oup@D)!eV(CF z1#!L6PjRi8MUR>BgB}1zPHwsZT}Y0gce?LyA$crA6>sUD#v)Ua1UVVQ2_DI;X#}G;3;3)#G{V__ zWy4_`UxZBFY*f_X6W8(%UQBUI*L@|xWj-b?{^+pX$auf8Rc`ka^P*+{Cc#^LjaI6Y zJC%#MvuOLVVXa8Ku{v`RJ?xaH5M|x@S1eQT_)C9;a_sChUhBkq*5UasO?LiH3`g>U zqOj%csJP6PtJ_V!;vn*XG++PNLru3rMQjF-Ti@S?ChW%(q+B?5R#C>k&05^`;+st-Te?1%Wqh% z{f~ypQb8GNM3Q16j67bX67(;KY5~qJXGEE%BAM_f7K(Q8bG3Kgji6irPqyCon1W*m zc6Yu1=-{;V8_3-f%hjzHfp_N)_$St$-9%4gq#exB|EtiUrvh8hxE9MHIJ8{vc<~)X765tf_9r+mbD)kyOdVYhlq@0_T zvxcbw-z1N7BiVKy66^AcFQ=qs=M_t(Hr> z*0JTdaEj$Ls>|hz*W29?yiE>m$ z(G2t}fFm#F3=;ER5gS(lqvQ;(nBSYCzr0$75k5l+64pt!pktyHthsX89g_=o^ zQNRszgQTKYLb@K!=2rY%lp;VJ!bORe+ zk&p?cG6AszbG9~=lK{|>j|M#ThMml#VJf&nHt7ZcaLtWi7yyf$yk?1Ap5Dg+!I7jD%|m( zz}5^XyqPOk=G0X0&B4u)#sO8b_H(8MZC$oKwBD(9x0x)ZwTr5nlV~(8#u1Nwk5IK2 ze>smAYo|l^yuK`koxEpM(&sdX3)E=az(y?MSPBKWI4ldn5=uWU!m1LgryMm!zJyf1 zLphB*RO?OYC#b!%7O@3#cX6^s;@`k!FM@3z7eFSO9aH--+{(WeX7|C^7T*PrqOS!Q zCXy)=#0U><^1l@F2KSbGeeaAZ|A2CNz4|;}Z-YFfii2$3TjN3}6xL9}ucV^sa+D9v ztLouVT)C$Ij2#rfscH0}E#(w`)^$DWn3~ttIRlGyI%!Z?#SDxC7yr8{lCfBkW@*nT zp9r@gY$}T&we)OylSdbnsuHXA2wG_?UQLUz1e>vzpO=`n36Km@a+UUZ)(B@7Zo+{v z4sw)h{wx)&`#O09c;5dP^9ydGj9~G&o2(BE>mhZyj6h4CB)o*7USXdk-}m3FFX?|+ z-@Rf^A@+e&gh7!CEAHeO#zkuIxX$*oblZfbL(PmU(Q3UJ0odA1WDB=I%#R$NV*HE* z+QabT8T;(T_9WBf+S^4*$K?9)IQ5-^iud#gQuXa-HEZKh^v*1`T|3H?MnHX}2Lzhe zg7wIVapG~V1IS|k^Efihy?=8tcm<0>TtnPowW;q$ijt>Hx*aEtCHs}SH?pG4?MYIn$=>w&;Ik1yTpde9i?H## z_VRkEj$JO>yQ1>VYX_H`qE5kOmHHE~wjH1|MX)g!dB~o|Czo3u93)Qq5TgM{Tm;OL zlHx{J$N)GI&dQAgOVH5~w^aX^qg$l-Sxr!eN<)iwPHmI7FsC~YHTSrv0Y5`Ni*Z)}Iq`|8B~|JrS7aqL)CQsplNKwB0SUy55ght_xZ56tg=ZHGPYi)NJp~ zsjyo<`K$9)CbEIvXKGN)&lKOfjt?)U#&Rv{ih|G2Z_c{hyI$+w8^?v(s$;IY7(3h5 z)~_9|o>1MsgScB(Ynx7RdmP^SkbkCM9Y}fF5puP|dePJkwlsVmNOuPk&Z>lbC0q({ zluq$2-ltDwm3R5#3UPv|NqoK!2P@dF_5c##e`+cqumunnd8M%fWH-M**PKu+<0GKt zfq>|&5i;zRCsM%=Cuhi%EI$m}z)VNLbiho9pAhP##i3CY;aIu3AlO0%2%J0)mde!y z6dvifP&P>G9?2p3SquA(2QvVdD*9@VPuteDfj(CSCf!pH?52rE!dH9YkY1x<&{$>% zu+<5Q75O=Mk~jvUryF3UFrPnyXfZ9epl>22r*KA5BW|OYFLyS|7rcz4z7WbZ3%Zah zLvpi^PiS&s*22wxicno%}8GhT| z>tye;29FZ=kljno0|wRuNYPKIY5lXnBHK|42epe_0#eOTq)n%_>Yc(GJ4Q&1%T@ah-p zWX9&sw8<%2Nc(`mo8EDYiuUuyFIADI6m)jDWTjn~i9db>QtzMv5Gj`de|N#uUQ!gu z7Ip|f?#GCcVqmE@0nL2KtZX)_El+#<_CKyB!mmb&2j8r+b(L(NqU7x8O?^D))y%uP z8GG&7nYjrC%O=G&Kli#FFJ={%ATuR#mw#mG?ipc=af1 zNN6_FZTcN9*AIJMNPi&7gylZVP$5-mv#31`QhTyhu_>=rO*28&th`hw(G5CI$XV@! zv3pi(bDy@{t*L}v=`m|Fr<}cHuq4KgKKag+I_p8|G@Cnqyg-MEBN_W>ZSERhAr!!< z6+3)pr}rn+=xo)F2BHm(d!K16S>D-KswY?mopX0$G#+kTnXigUZKA%Yt-ij+M%Wd& z>+zwZazWDty?y7!OpggJ2Z*P0X-L}%M>@ZLx)?}6-8X+HuRq!I4f z@B?ph{vjLGSuovo`BzNKZ{!G-aZi5L1)TJR=*OvWq^vxh^C=Wf^ z?%Hv{={gULOAcC>Leo?ML4&`@t!^NPyrd^mi7xj1sk@rHt7kD$pywx3oA^u?@D`wR zTo>vtg5eU!_yyqzj>Kjf0<~S}xjyE=3O5##OwkrJQEPHjb$M+}VZs0mRrd@Ozqm4w zeq0DV*}2?Px+_bs<>8f)$xMW)#L+US#yXsS#f{Sdox5l6JiMn*E(ap5P$CsxDafJ@ zg7OL6-2cy{IuhRq7t=OnVtHFg@qM+`ec}(3q_RuGM0nLSMY_bvE&ax#UcYp9CmXHs z1=!!}KfAid-0f6eR?&p{s@Zm-!@youTfe~nTMbfBrAVU(0|3xR^#3tbc6R*d?d{~^ zX!=hCQu7~4{wU%H&ni@)xBZe(Dt*W$Wf@v)cG37P2 zSkF%$tydx7Nt6rLFq3|x~OhCQ&%gBibiiu z1#zO@wj%Y5%S~M2S)l)YuZ}S^P$~G3-sw%8Rai!mUOUAd*2S!}LB$F-a6qYKY9gJ< zTqo6_$W2_}vph3YG|R5#kvA~BhTKSONBPj; zrSKJPP{{Ren3Ln3$f`rs(CnI^8cRd|i5yWhalAq~P^EegEF)`@<{-x!dd8D(pj9z) zA*-&eajdi?`DhLYaL~&T1LH!=U}RlG1vT1M__u8QynhTzZII7ApXXPv#nQh(Y7dam z8B?v&2B(8s8Cj1TWp{cgQx^b;_LCcV{^#LX`9K-ov?R2@Z``nIow{hE1##6HTZa+} zU2+n-d`Q`>r-H1(4eO6By`(8Bh|WCG#GwtJ9WFtBbZ}U<8a&li!BK4WETHPj1+jDq zk)}kAx4a0-m>W7iPmNK7YDyr-1d*KmSwLqGIq9Ji(JfDjV#mzpCHJ6HfVWj$xII1B_S7%-Tjfv>5FHV_np~&mT-oOmvu%j+#w?c8tGRW85SI%q z2EBd<*}?Oom;IH1ZVXca9}}C*a2;er#^eeVU5an`UAG6a1EUu*j0LJBKj}Iw5ENiYGtzgYl(uscgfGO; zB6)Hj6H23}a+aZ5hT;^_YyDtFXp@w@d8@)kkH47%9)G=5)$po7g)$1~HC~5|)J&}^ zV%;bZSGj3&Okt17ntDL_K|f5@Q%5_Faap#bSwm1RKT$snTVB1>kqS)NrE9yd=#rmj z8M!MwBQ?xpnWk1rsf3pzQK?0y?3a-5rnz0o+$0vd$c+U$e$v1MIUvt1B9l^c0cB0~ zY7I5t3jy`yymHLmgVNV=2yFbfT&`n^@vUo_9l=3iA^o@e?Cd2KLJRr) zO@^=Yuh^mBg---(g}&JVUo6B2W8oh3-%MxMk633WVeb-0u)h0FjVjZFs0nJ&D}+U` zgi>pib|P7vZu-mhD;)wjxAq&y7(zh$G^s$WRWhvKZPuP&omo3A~xS!Z@>Lr z$H~ljL_|Hi(Iwkf$KxXUzx*kE-A}=`Fv#2by+Un@(O})y zQL8JS>#MuK?{cDQhzsKH`OR)Bg8U;#E34efPvUAmSU0Yx-46Ol82sJN=6h4ffo@G5 zdxvR=^aGW{P%>pkuIxo(Yo#7y67kA)jEAtW$aq#>5DiK)oS5X#yf$syF1SQZ zghOqRjIeP+nzqW%REE62x`JF1;xG3B^$e~Gk_32zYTLc?-srIcVjJA>uJi-ow!qBa zQaw;N;J{dW1)$F0%lecVHUKT(#f@Y)6$kh@6JYE+IF?o!WG)ek?V2W@B?TILjIGIo z1F>93sU6z$JSeZ-2Qm0dpx{7vCs-GPODB@0ESyGpW-F^$RKdgG#Qr|% z<}6ANsS7+eF&l=t>Vr{KrK{tlE!*6}3$USdER3fTpO*6{ItKp!x0q>{A9QGxI zC92mN`GYFpA6(?c^YB@<9(abIqIE9jQJ1p~17k#b>g<1ivX4#Np@lx%Upl>E*ou~g z!N_kl@AtJ9K|A>+DTUiXt!%U7TvzH69da>Dx3h+E-K(tZASYS(K^e2grtoG)Eb zNZauU!dD^V(-2B?)@i@A{9}Cq@rbr$v#6`}qO|pd0JC2(#j-^eM@N6p(GfHE5zx;6 zQ1=JQ4TJHB%j$IgM|U=#3t7VXb`a(q?p|eA-m~ycM^OcH zM_C2E=o1xGiXOKvu!-nH3+C)Ixw?3rSm!@^Ku53OV#Wcb`Mq27%)6XPB!7+3x z7{S0qEoTK8$2g1lZTaKTwtQhS#<6Lym4|#ntaX2>MDFa7x@FgqCCb^Sg>b^>7*~b@ zMjZ3j<^2M79fyyl>WlHoPM@=c zQQ&3V%7#d!04&rzC45b(1G<#XFXDJKIYg&K zu@838yg_FF$Wr4Mygk9!LIk#5rgW4u`^Li02L<{&712dXDVrd7zas~9-(|Mw;2L2I zoYDlCl3oi@2Zo+_IbcLgohO{;PwIXC272wpxQkao{k}y%z!~((5|;NMVc%Id{DuGf zf=ds23LN48L!43vA;EBa{T-c{#sJnb-lsHMyw;xckhg$^%QWa*{bRzyBVBAI z1_TSTc$W{92lMX!iTgejONyWf-xmaz)qk{fgsup&)*wu}eUMm3W+?bhDVSS}5B*l5c28x3&j}t=C^YqA=Wo_S@xrTAk^ZgEbZP4+T13UZq z5`qnA{k6y!awxX;R-DdK+a}nX5;|wD%u~m}+s4n@>#wZ7JvM+s=ac|%ztp#h8#FK{ z0v+bx8+7KB41$`J&1*$T2K1THlfSDf>72k%F8x{&Vnr!di>WHadX z`swR0>eda_)3u$U-S;*1J=UYIpI{JX9QXgpwU@mu@QQeIz~7PkjsrgzK*|rZLSw6Js)aV@j4E9GLs-0$?fUH|6 zT_iY)SuVj2WL>ea&y*cD#;N+tOV>Jj!hzEq6*4c%MHo8LVLQ{Pt(t%8Y_`Gg%+bU4 zKmFQ&upy1VLZADG$<@Dy_WzB|e@FNHZ)l{BPfiR00x7L@$=QG|#$e3-5yNr)+tb0t ziy4F&iy1^2OBq0!%>Srm{QnH_v5*k;O-K=RurzTsC0dh0%UAHtlx|x;I#PN}Iyx$P z2jOly{7+2K|HkIV$I2ah!)3i20EQm{WN%++IlfR>u&{irw^`ewhu((9 zD{Z$L)#!^^dqI!;*wsk zuUY2lBdfLIs@NvG-6pftrjosj=WIJ;z(2V8YJK*qi9&n3W6tT0=WY9`Gf2|TPBg%8 z@5`u9Ws}z|}nUuvl>t)7Ft|I)3%yLs!Pl>L|h6iMB;>)(FUPYXv;>)+GUq#-bsgJ%> zJ+t_OvyZt`J99UOBGv{DZH%HO*dkQP;HkQWwNp29Ema-GtQhP_ZhO@(XBpMI+0|82 zR~SWhBm7;3m!-vGZgb4bgiXpXnD3qD>oH#Rtrj_?s((;^dTjoA#oVyhC>|vb^o7D4 z$PFUojmpS^EVb+2r?o{Yt{KPbn?Z|n@`=W8Sjr+}>Dcco3hON-X;eWL>_;NI8Q#?h->CdV-W(^ zNNfW~xyL5&&?#GU7u9dK500j1MQi8tB*>0)m z^&DcWm!}8UK;Rna><$0HIPx$@c(orX!b-ocQrnYv2VA`4>0Yc}*{DEOb?O=MoUz#D z_ITPZIuXO?HL0t~*{tN$EmGJk_b9XPq1Lt8(%tSnfk%)&aCb1+3A3LCw?}=>j`Z`O zEyB9bS$=0PRy|q9a&ENVegNL8x9n3uo>eq&T%2c%lzCsphNfRu%Cmr~Ub*Rzb z{X>06w>-=LPFubZ!~w%ZX@oslc98t!gz=?y^G}bj>o7LLsf~-WK~qulQ_dOT3&m(X zZl!^sHG}yBS9yr9YxE?MbWn|Qb(+q2)=+omqNLF0ywl{>nHFZk2CnH7!y=wtFscYV z7jG$l9{GL~LnQzV&ozl6-p4c=DUI986(5uqOlu5-h@w7U(LH6qnRKxlQ+VoL!#7Hk8ud&kcnX`_V!Ia2rU`OYvm z2p(d1VcsD7u>fH&6Ao-~QFMgEbf6$ApH4*`i~CGh>%|qq%S#I_944DvX%WoSPGdm( za%C#Xv1$<6>)-28pDWRzrhjAd#LwWMgq#JF^cL270VUS0iJom|oOIu+ z3x+H$t|DextKV~Ux6xXHYn`q1kWmX`9SXCqp@509R}u{?e*)Qu zTu*~Czay|1^M(#kj@R$JoTL7G3a-)Y7ULbIvtDsI%_?=AHyMidkoB{(TsVf<_=K+w zsG^91X2s8wD0*HWi;s>d`Y%i>Ii7dTp_dq1b|xR>a_1bw3CpA=thbmK(V2rJck3Af zpSkWnMY~OO4m~l$7}A*6{-vsf-WKqk_(jP$UPU zsx$q0ud0gJ5WVoBz?BtPcBC=grk5TpaV*Wnxgku1b!hv}3N!Q01KI0f#+2UqlZR_~)z^t!$0*lCpA@0U_{Z?MKRl^N>}Ojr!v zAb(kL*!Ug&PjHp|lMP_jNe&TanRJTV3>rl|)l$R|m?%!TOEA(8ExI_pVctpN$<^il z#E{;W=eEL;#2oe;Sf6`g-9yj+L6ES)4X>4l?gxPfBh}QP_tCO=X+&xHwJ@h8Zsb>V zA|fYv;jYh2ej+=54u?5@7NC)8=w5^7Dbe^O&OFx$;q{M-;MrN#T@uba=co(k4)K{U zl#uJ8apJ{5j(;o5AHjLqq;D)VlZG5cu#ytwDhT(rtiab~$PdZQ4}lk%hjsUX50!wC zhX$Sp@}A1*t$LujH?m;2W+)Y7B#sujfj<;*`+aRlSx@TSf)8tj+TI3e25s8)?ae_5 z0+}G-F9as(0VGkfXMSna6`ug5+{S(m-7o)RXH|XI8cIn-`cT4h3>)TgMQr~K_s{Ln zMH%L%La2V0h!Mnm4GWQY-W1OBD&&Y(?}Jret;UQL*m|S=Z;i~>QA=f3!~R~16$`u4 zAn=uFjc!}z4wM!(>Y6E9a)o;BjYhwTtwYvId9IaCkLp7?54S)O5}w`W4#mw1=v0OE z=8Kk;a#-LI+I0=-73strFhcg03sI8+>vbDx6dM*^zcQercaH`j1WCF;#+35t3qT~4 z`~dEFubswrhmK0maFn?U_{mOj_B8Re&Wk0mbbRr>`5f9 zEP2uCshxI}O5KE~t_kodP`K?-rE*nF>CsMI)uAB-cnsKCx+XNZC8JjtSsV$5DSEm7 ztB`$Wd-S}Cb)vBmp8jB7KtFXGUi~|6 z3V;>dGW4jOTaxWz3Voi=C9B#4Q5FDdOaY)NsZ z>%b`hGZ0z;ru- z=&!dArm*VY45_)#&lWAM5uohxH7c#vyyI6>qpepKvnjdlc2lRVJBsGUfJ_K1}8I+mII?mqCfjx8w zL`GF2b)2svcZ^?5?%XjZfkYAYl;N}h zM)7`2%MX}*K?BB&Gi8th33f6V0ld95(?Bl8Jr2 z1s!psa$t_oK$09_sUgle&7c+u{%FcSfO7~DTb1HNnDGw#-8LrNEv)uA@5hwfBzI|zE6Fbv*5PAfy532Ct zCKebYcY|)B$|7YVnJ?oQEtw>E1m+sgXg zrXR}~d_aRLw*d&AY}hWhhsvx0oSa-*T$9!$*n(tI6ME=LZ?e`cCSbZ=T_7YSI>P>S z+No4v*ynr1w>%=BNXQ+=`w>o-ue7l7cpv&oGZvw$1az9ArILG`M6bo1z}_ttaYEw> zxOJ9{&%X5DjstC6(9S~UT~L;4;^+YmIfq~)u#w~xx%uvihZ3oQ?vaONQbOCtmD`t6uANH4ART0@Fiy&zj%$^o#VTzJix8%y-Li)-Z=fx_h>L7^uKj1oq9LJ%Bjir^0&z66u6BDgddj{xKmQZ7nJ)D5@&s zm_e&y{K6Qzq&yhm6dsZn$U+K|c~6Y2fYt8|(O!Wvy91iIH7!v5iEuUqRo_#=pi04@ zM~2+#4SA&>nBd~%`pL)(6b;|Qy$Z7IN%gH^7CH{m0`%6&qMU#h#P)Q26%rI20i;@Ch3m>FT+%);`HJYd)hs0}uz8s8I017s2r@VUv!E;N zASSV%Nd$wq$Q%T9A* z%jXoZCQJfyd{8_l`DJ3m#PdoZq|I;T>7lQ91+R@aIO6!m$o`m+xsdiyhWD1N9qQ~39eSH(x+``=yEc>3T?5u^=8W297C8BnQzjUZ-x+JlA*y6p^3diY$qD5c zp(x6+hw*DEuK7()xW%H0I`tx-0x8alaTaVVk)t@U^OFB->v@D8@OrX(St_V%%#=;e zo_3A8&R&z(o>!Y{&NR_4Kys1L*YNf9H2r|iDHkiw49;j=Z*g2NU4dR@Gjqr)bI|5M z_U0nb3SK|9IJtH7&HPzz`_l<*Dy1~ZcS+8>Sw8GQI&hEdOM#*#otm_Ms3%qQO||B{ zx0qx_ee+ahfn|Di0VMx2u7_Cs>;Qae1b#^5-|QXtMDYyc_15?tF06L-l&$84w!%0F zPe)=h!dj)EVLr%QC&-x&clDU97r8k%%QDT#!+?ri!elGlmhQmks21mZQh5VBE!~m> zvon3;JilQ?;PEgwGXLuIr*~5{Q({gle_K=@T|ngAH=$_aVmMEDM9!8;UE}C4lH|(S zTlRMZm%-%19wOx^WL#ykkK%=WrmY__42`WLjieFef?zwwcD<6tsH<04?~Ad-nFF_xVYWBTOCkt z5CY~8N?os*Fi6E9%HwfhJ#Wr{5!|@A+K|r(+7bp(Z=}-Ndf66ekgwRV=^sjz1&!kM z)r(%;-!H-+99lgR#1L=#1b`7k7H-JiR|V?}+#-~fLfcjo;&dZ&l>^J?Po%w|AE2|i z7BVGysj|#oqpYp&c%C9HRlBzTCvIgMR{JSZt7fH|Kj~s!X4n7Iif|i(n)yug$#^i# zF}3-bUQPjMpIhqAH>5@B~JVT0WXbSR1j0$h-j! z->P-MknJC-RmWhAotf1K$$?{9jW3wauPK6afr9ZsTzaL7I$8}mu~m!gW^h?`<04(b zJ^y%R!Ok)47teBB_y};t*!ml|8b_0@)Xr?ut3SgPiPHU)Lo?rWS3co_Nk-iSLRGAb3iMVJ*F>J2;- z;(b8eIa;Agrn8xTvYb>X=+)*$Bfm2v(x6VztJ|hC8`ayv7|pz6*wh10hrJFw5as&p z3zfdO0fn1#z_3s*q3v&W{gs}r$hGK+4A~~=rL4u#_1fveOKsuBJ0fq0Tm+(00#;J? zoB6#9oS1By`~QcrbBGcw$hK(Owr$(CU1{64ZQHh4Y1_7KR+>M1^4IEF$BLM~VcZ+% z-E+2JBfKTWN$xd`OCJ+zsxw=GKD$gTI+Y-1qecGo1n3BXK%{#{S{-Y?3i>9tOtahI z2&8}&9MWshNk-7L&G<8r)L4Lk-9De)24Ht(JNT8?{e?Gj>D|$nj1MkBjwktETVpo_ z;?|96|CxF0Eo;78D^SzAWmBv>0;55eze`KaJK(`3m^zWHb2udoMi5o@L2I>#mxE1$ zX@S&DBzQ!=R=4S3Ps)(;J3zv!$W%n2Hp2;HTSz}j2LFV2R5Wfs*=bL}$Dn)|HVl$~ z!1@ETP2&-^nm$yCYX-p!@~qlL?KS!M;y7(-ar^OG&`IiF&(EDb=`O_<9d@qY4o)Cx z_20tD6COk@N=xE-@_5G3lkyE~`ew)V<*I6%ou~IOnOW)LB&@zq*F{Ux^97XyS~`!7 zj-#b+jqWDl1pqdRD~?7*>F(TD?%MNnk|XoSxw+mgit5z^7#@vDTpF)eQmhgS$dJ=k zERO$J3c~hf@0uwB>FBjp%(53ICx2P09?ic?j*(lkq};r6z#{LYBD7D~nBD&DxFXPf ziDn4rkzJsX1shu%R*nPOS}3lR3}cwRGm!}!7*$5~!9C693s!46y1166yU)cm-L}`Q z$AiL7#m$Yv^8j2=;(8w%wNyksVC$W6?JkX$ad@lo)tpEtI~cf<=RW-+Rk?%rf(l~> z3<9!LN{6JkjhGa}ky)hL)K+Zv0)g>jND(g`EH@NR^Y_wANi|skHY^uRIaGB@%(7^f z)>VQ7AkX!AYI_KVXu&=02@{oDdaW)QomYY#ND}ZUm-N)4Gg>T z$3NaPtF2gC=HgqGz;>6HR&4ovo*jw(zv6trlhb?%WkWll9;4AL1K63;1iybjyODhc z4STE)@O0ZJYvl~}Or~tlAnBuX6;O)(-r&4&x?caD7K1xKSe^bI6(UD z?DB#`>skFDDc;y_)mrwIxrfPF#WZRcIX0c|)3M}WI1RZB^N!1^D&ym$T`7I>bWr;p zN1w$W4cgW0Ww5K_lm{w2AYVt+c>nb?Ky|nbWB|{S=$llzLHX(xY}7cOYM&y&!N(-6 zmJ2Wh$RJks*3X_aI^K^Rbh=bM&o>{notLW*T>@;C`|gEtm>5l8$3w99D9N}cTa=`5 zlVBSbs&7IVKAamGJ)abs)lagSo(7n5RjG$k_e6^9%CT*cU$sarrL%!DVI7x9R102r83vx?MrVXA(-kUp{I>PQA-NjZMO zIAHTZ0EZg>Wb*RB2(y;LmVn)7(yuV=ew>v z=_~4)VYZF2Brmdy0f20L@_!u_nPydwG_|YAI`UUyt?{ zTSH5CuHN<6>c$cq!Yy0g3C+r_X8g}KanZhvy~l;E{S7JE1a#wfuVccOY+6_;z}&<| zv0{3Oz)Qf!%R_eXg$H0J-T~YKc(B3>QXyN!h9H(M&RpbC2d~W@@q$;x0#}$6oT85n zavK+-pWa_u#0*ZrhbbcXFfQ3jMoR(rRgSP2h=f)LQdG-3)1zZHLJi zO5Wg|vvD6N3WSgAT=1MaF|G`9h(E;{zS+6l@xF^NK51cga(DOnp9RFgI8ndd7+yOu z=29wp%Yt!cc5n2hNz#Nq(R1HE%@|+FF+P=HzA=G#=J~eYW)0?YAK=|M=YBZB=I?3a zcIZZ}qX)m$oOilAlox7m?)+jXf53pbpnYJzb({rS26B}bJ(#7A{l zLy_)Hl!Ogh{7L_P@N@2u* zJ4t96znh4rmd&AnVGB_~C+|n5#g;V~tnDAx&~SPYXm{7g#l-qlcL?Ktw57!W)QIiA zC_4T5{-6Q3Z7U)_ z_k5bK3cp}`YCwMqdj%&_ly?s{KMzQ;pU`K{ALZ+C_C1ev9}4mw`Qq*#3Lr`@o;n{} zCN#R3KGsll6!o3xzi1<~jw=ZNZ-pZpSXe+%95M1#pG|K!V9}+J74ndqj4VB5251gF z7Z8*Y&Ib=E%+;$yAcDmkLAEt#PUwv#Yj-YS$5$0c6W>Ub0~$W5Xa;@HLp!>z(w^%q z0C`a5vYr|Gm90Ov=pW2CfKp*xj=!q2uPDrc`wDynrN7&LELa)( zbZ2@#^6lNt5?&^7L9{=r8Tt5qJ_7xrH-x5E@cR6l*-mW>!4Jb<)FIkv^o~{@`qJ_r zTLzcpt|}eBm}exR`%?C47{*9ae|C-#QyK38t->9cI{~bF0p=QFKs?OPL4R>(z-P=? z8q>g)b39=yu}7r}VjwwfyT1{&EOi4O5lst0AA^kdvEZEbxQJhhn21w2Cy&AzzL-%@ zrGgdskX}*tK47X}EbSZBeN^~5{9^V4$XBfzeN&u_!m;vzJKf*7fGb~u4lp0-W~o zlrYgoYEwJE>AX8YsmP$|oxZNma)qqm%_I_9%4@=TO6!Ae(JMGhKEdU~EY*0a{6r)E z(iui^g`8PRBdSl6BA8Kee@X+Ul1lG}CqlO!h>dzF8v@(OH%mu+q$bb)0&Ch9OIF#? z3CX&T`T&WeSyAf|N0~cw3uP`5W#u;8h`eBM-R!l$w72Z3uV$f+R(Y^;cU3Fh)0h?^ ziNw|+>RRVdG{jcFDdk_1m2=fLqTriz!i+4j;rP0o49KV{XSd!49;jKzSz&|Tx@Fzt zs)O}f7U8u>_>N1jI3>hlUmFKtc0&U+1~5b*4k?|!850oo)+~7VaiZ#H$&sSg!S@NCN18_SgBZ-Szes|K9>gr8OhUbEaTDP6OVawd= zn^eFZzkc3%jb*%QHYwPX{zs^2fCzf_;GnRMzE^!`eqyx5J<{=VW=_VzdByEcai2HbbgxRDU>WK!RP31 z_>3*p%B3Ar35Eu=d9OU4CO0}_S$OIVIJ;n)MeJ;sd_^QQpvh z!k)6eH=y5@C8YR>>^_U-jX$KHe*ggWrFejTX0+HEsa|sFI}XDeFX#tFb{U=P{zpw1 z64QnsaAWwchSs#z1(~8SjfqaDRj>+CZ*|)QLqdkzj-(Qnq3t&4R8>-tc z^(vnk%oJ9UL-3aT#fWBA4bJLGn#-?o4Km^}aY7ln#~QJ+7hb?1zmzm zOwFDIb>yk5!X6ryw9@w|!(NnpGWR&c;uJgL_lm0i?zsm>*52?>eVBp&F3*;h5~qA_6fCMHjTaRipFi%v$}CFpGsJt&!?p1lAeN+xud7=LJG>H1Okc z;UWwwYn+gc?KQk$o(Mr4=ykUQuL_|#^U7FaYVTko1`$=b{8~A>;425yR%oq8bPXWKX3N)b5zhdn697o(Wmezc-`t%{4p8u6*iEo(hs0f{DKe2DZeR( z(T5=+OT6T1L>iJ5trH9-YXO5F0{cGSS-9HWW=<-7?R|$4U2l>?B|thjaaywzQ0V)5 zJhM&rK8|`c^ALx(iM<$ht3HKV7r2gbk;`nWI-R@(=x?!B z#m(KQ8IWD<@F;ewIjBZNLv;vraCr)9>H3h8fdAH@uf{EI^&`(MF4n`#hg+!MGVkMg ze+k!c_GJwE5oGs2qpuZTnY|q42Bu5A*EI=l7)qE0F7tMlMKIrEwqLPH67Na2uvtuprIkwf}{8Ng5q3e^S>2(NfV8Mq`3fW{T8TFhg|wnN z_++gMEQzL`^FW1-M7x|>I3BCf(z}V^I94+lb{am0uW&J4U3eHR%3}Aj5i8A zAB$pUvyU*@^O2E;zF8O57N<*<_sc+hz+JPrv2cEHc}5 z=eXC`*+z4$=X|&!DcRcy%^jyVP_Tf#lv+Irn;yZs*uskW!a%qY#1fUo>KLW- z=yha`aNulFrsWfSwE=Z&9u~Y?m;Wu*-PB`O7=~ZbKPK59jlyIj4~q0q##Qq3w}V>z zmXZn>sl6I7=m+8?eifLL=@Sd=2Q%m=P*4kVPL=ciCsMIO^WBA10ax-A9EI0StS2O# zkxuHUtnGI(V<>szIgQBD^s4jWzV7+TSF_QXbg_-H&!;xl1Gjeh52bjXQ<5l__oe~O*d5tALmKrU+_Mxr3w`r9gO zA%^n8>>H6<(-a9j$T5wSHC2+zC>EE>Op*i;1^&|U4KNCsY&@t|BL z9-UM+R_xWVbQkkYdcBQx7vbxLK%P^2TA1{n>EZV!BxX(lIQ-$#hN*>5SSZJrIx-@S zjriPO9Bd5Pi~S<#!HC8ML}U(DKD`Z4 z9Su}X(#dDqtU(45)L_F7$}fs$_R1foE4i^;olPhjnUBPTtj49^Ds~HFL?RlWL3w~m z>?QQo)jV-GN*a>nB9m`ljsy`5r#kySsJGfPI&T7{x;rA2|xiQS@ z+MdPOF&5&0ld^lkU0B`pBOtS<0hNN5*R60luA!GnT1|%(o(nVUU7}sa0t6^uS0|dg zYe{5H_AEvRtKh*301_jr)H()HI?4i|kY|UE!u7>a@*eO}(5=}^q|un?rXUGykydX6 z(VwQe&yGwcBVn&bG*`9m5|^RfQZWrWwgY>FYi$}5pJCQ<0$_llADBpSN+7|qb=$6kqnGtp^I^2d5jNz9CO}t zgai%?@D9!)qKMY)Dv8pU9*Jhdv5&77x4;7;mJq#!%0$YsFBHrE+Rn0rvW~j~llEx# zA7GqsJpF71W`}z)jtkk|`2E*;KPwGySvRg5Z%Ef&mSj=VsF<+F10N21%pUgVHT&zP z-Ce8S)$8U@ncJ~lcc~G7eCP0PP5nk~K76ykfYz?Qac}?FHz9N5pW7-UW8&stf_<}J zG^Xaa38M_4O_rZumU^Q|=HqmFm+ z8N7Eexlk1k@};0pO{4{1j)PUWAIPIsdl1~Tk+6f$jhBo45(kcd&IP++dW+BZRvUc2 zvYqg~rJ$dgUH-WwGqTXHtRC~)4_1_W<4TOVGP*ozpBsR>Gf6fGqpn)8JAym%JQ`%~ zb!}ar600?@w*qf?9lmLOQIgj6{;SiZ@Wu$IDYtveWCkF@^AMhAiD?u3w;?!L{hytR&YYBtMd0r^-u&q= zcc;agoX4;uk3+pDRr>Uvq5{gT4{RpjI7lz3XTb=c7zC`HAy;=X-+aA=qTywB<} z`rM3m=QV@+&zyZ_VQykYx>f~Wm9E+5#>r?VOO3vqlL2jQ{i1ZF0NKqD*x8leYHzl- zR&})6E&j23^-LdSYlJ)SnDx?C?>G)Z?{1Hy?aOp2wjVFtqW3d~+s$cS%{=;uE*k=S z0&Kd*Yc!qAw&lw7!lS!C175q>+*J>Du)LB7M0k1G5cYIl1AZ>&fraePRG=F~iCD*c zJiiO|l85@-hczV%M!7Td3wG8~+da!=FnUsge)a5+Q?p~;yKN=@;r5QAmBIEL)abwc zYFpG3v3f;MU|t({fwAY`RaI$6bg(Vb!#sU%LOfkD-RR*w*xKmA>|UoHH^{)wA_eX( zVWY1B+vqx&`$kuudoO0#RS}+Ptr@#49HcfWq-HiVyl=8W)s=K(o7K1fo9xOq+016m z9~DjjOXi;@5@bxvE9Vos`6k-9F`%Jdo<#j4-C z;{WuKyfG0u^*aX6T%((?y&S;W-#T18=ce_zFgsd$lx0+#XV!NVftN6B)}=Q`x+T?n$nDSN<+nsifBlfdA>RcIMN9URh=34aj&% zazkp<-%&QRs~j~sOj6)Nd0yy*RJPlcY-dpb>j2o$5yu~K-xcYIB{=lKQ2VD zTlw}n59rC|>H*Vw69h|ye|>{Tohh}77Th0QYZCxO}HQnjki0AZDC8Z!EcF1lW^oo`yl-qQuLcf?6Omy*lK ziit?+Uf?W4R=GOeAdTYPNUBB8v_zynrP6Ili%-D5{Vexl5or!#yD#MVet-Icgj&rr zFCthxiYG}!%vz!WMY_e=VcSsz!e?Zg(}G>oZC@nuV_$&4Jt*DRFkg{OG>>Hz0P)bX zV@@m`JL-6P1*OTO=_mNsJKJ8(x0vqJ=u48h+EOG_1kL;&zR_bgIA zB{`M~pzCd;N_i=M5LWs~ zosdpGqKvCJqT6l)fQ0H;Bo(;6l96|lmzQ^&m8oN7Nlw~(+3fv#dEdU3TexlK+DbX$;Gx1ik~9yiyxpRwR{ zFRSj-nS*+cXER1`DeRNf8g!MUUMJoNH#e(R^pe4Dn$*;2ZdxJr(#CEjZ-$(o(MWnu zW;4-Sq1#;CX!bt_Wcy9`Z`!ElEt_6fYo-R$tyl5V&AOdv%%=q*Er-=mySCb3CHhiv zHZ=Sq1dP)Gd;tH?F(@d6z$-T)~9bmE|>- zvCmxD5!#Kb$jXvAD!BJ``}eM8k?6Pc7s~05h5~7 z#*V9J*PPv9*8wNMw8X>0TW%*ve9q>xSW80WVa1Kbnkjp!Ubbb^L96$YaVl<6R-sjU z`sb*;-hwM@WPY*3fDsQh?>$r)R_>7PF^x?o4D%Cm^Kfbqu<*ci@?jcx(Ce>(doDD` zxl7rN5`jFwVIh!k+C}ZQ^_z?xP|GP750;_0>?DA#3O|dx${C?G-}YKL#XJS~17{Pa zvnDOU;f*Z|AH*VdDZWXY!x)XvwPLarXE&D&&oTq8X@`RJs!o}~jmML(m3&`P=&4cj zD{;NM9v6Z6J-tL$@R2|%WaS)*>^#eKmjsezUn2{6{U@k;032YoJ0M%MD?t1_x)eH- z*kt|lSG)t4AUXXn3>TzZrtBOhhxd?$Twkqx13U|S4X9hH?y0}_14^fu{cM^~9aO*d z%%)>0$R)8%Neik3I2~29L@n5}qb@h%(tB%m!BY4e8mL|y{qNB!cZhQSiW2~CVHAW5 z{_Akiz8vK1gl_{g3egFhjf3V=+&ah-2B9?u{c}8AcRhcBTI50@JgVD3-(oD zg+3@-dbVYNK{?p2)%w(Zx4uhMOPfK(=2lJ6HluuZdy9@DHk}$iv#$JmlA z0|&uQkm#)P$#VO;NFKxy6@2ZrJp;#?O22znE$|_y=^*=`X5{636dODAi zE9R<*{8QQ_W-Lk-cNh~`;48efl^|?0>r1-yM#$K!3fZP_nDl2SF=`$Wfqa!JU*5J3 z;OZT*BysrbvcmwXvmO(uUks;bQ#r{iiaJN$h#^=c!JnFqy}JG(vShnLHIY_rm!R=c zTKp}g5{9>q+lNEuY?NSlf1uPaD7TdGwf?}xMmJ2gZLm9T!{!MrSOR51EaX-2Bhi%i zQCTvQ2os_6K<$ca7X1}xd*^voUS@s6@vqvRAeoklOk1B_FHyxnl`V+U znc;B4%G(hU3BmOO4YLF#J4Irt;TnDC?Z=mZ;vMS>2B2q9;bqIia_Wvr;yc!Ig)6 z_%G-*ext2LN7==B8`D8ev4;~m6PgMCJl6S-!UJ)gIOG`!K}V!cdyHGPv;k%uWh%~^_?BZ zrkmGVD_-U=lQMMl$C_)+e;gD31VUG=(Qn*s&?L|#!b*B&M^Pn?s2_Q)gWk;fXRpWFvbC+_g5Kq_$(n= z*oj!!kpsc-CFzf6{kHKSQ=qGWz~}1`RtFR4XdLvQl7Q6*gVY&Ttj%IiW=>Xo^2j#o z8}PLYitGg>?)*R?J01#g@_vN9s-ZD&gudtK+z3nPj_5bjyir$NcyMl~!b0pHy|uGx zn@0y6-63l(Q0U10TNpUt&xrRG;5SMncH$bqPl)TKgVyMgXaBfI(jTAxd?j+QGBN`( zE5G)>sokrCgO9LJoXfk`7F63khvxnTxb&>XzY?T;K2pXsBR%W&Fb%0)=^aN)R(|t- zwu>k9`0);Aw=#O=j#luTgn;S>@d<&f-w5TQtL4p?{SA;li2MVvSqaoEMqX<0XzSBf z$LFDYyDuxnTWxEDWJPW)Z=o0Z?D%W%@_Nq`dw-VUSk~Yi=HNc83r&*`zvFy(+if=c zY6;?=`Tx&VopTf5cYY6{c@Nt$&gyRQf^B_8)3s~< zC#Xw4L7)8W?N#8@;xpZgJ@cbyJ{WR$5wXyNr+eE)Z|VnX`65^HQv8fMHjL1LO4=R<>Zr zCy*ZR_&VS%-Hn!0q^3(UW7^_wK#!}FqFdVi<8AYsDp-P(CkQL>Ih7y%HLn^S-@{>- zZSes%Ru#%_7->P7i|l?6T%R82e-N|9#m|w#1ux$pc+Rk<1J`|(?2Om{hwe}62K!(Z z1UTGfJ$9t-RaWW->Q9(~=AI9=-VjHEA7QIqi`@slpBoh8d*V;gtrQ|%4=nI0;1*vY zXSnY93|G(jXVlTUc9q@gd}|z#Wb!b$t*+Wqy9Jqr$0(?Oi-cPgDX z9VVGqZgBpBDte^!RBu{el&%)&Q}>YCh#XjDa<{+ku(z{m?Ak#GF-Wxymflvr#;>88 zRm-kaBGNb=s-hktl=QLa$hsj1DKvEE-s}@OdNf4_GI(dF{!sgtaWv`38EBI*J3Pty z(1VmC36Q~>iIJUrC&_PN5_Pq1%78i#-z)*-99uxm-M!z`pI={L>D?^VCq0NX+HJOa zW$x`^3y({-2k|rT41PeZ=BqdJ0RPmWX1F32Xof13*AE!@yVAhaRvFdo;oImcuxn4Z zJ9pHT__>U?){J-s7`mr1SuYJCrj`#%y4c^;1{viKK#uWp1kR8KWqHUO5$u26nzY!f zg414py5F5~Kb)_}Vf`g#BZQ5<16`@dMms=9_kjZs_dWZoIE&TSpy>uC;-TBk>FVhO zz1~+fF5E#^jVo0rX?a(j-C_pq9tFcTaXGDB(6p{gJUn*mxh6Rdt&H8OR(wrFZ&BM) zOj1w+ERMEx9q6aAVO|k&cm%EA>%_Qp|3a@tMUB+7r)d+0o*0Z^QFm)PRpY%M-K~6j z9x!{OFar_cZLhYiv*@ZNrP*m#?{2Fjcm$JC#_fC|6P)G*T@uU@Js=B=z=!VClOLaB z$f8hM$8i$s!j&ALNR{HHjOl~-lmr|Blu{aRQI%&`SX=<3%ZHubC$QUax-G9l-NNsXRWQI#DAYXs&+M1%cg(QhRlfn9Q%Z zU`R~mSs}!+7#A2zgY|q6kyNPksip+t_kqEAu?EgYXd%#_NSZFY%zQwZpa8s=P|hsW z{#k!E%M8r(WihexT8u)eFC+7dCbEN;Z6KaV0C zA`xXeVoDZmB+130Oi1j4-~||F!545;NCi|P6XhhOk9Sad*F=--1zlK`GWH)N`ZNP@ zQ~6Y$t4x5;&mrbEj-~PLCKx>~u8x;1fhkIp#8eJ9hOQSc;DvfDn!^LN#bkmcra?@h z`H~lGhrR~-G?S1V7vM#g{&10{1JF&N&@(|Yc>|Q_0sGNna6GAsrtvGGhzsvavl38L zh-6TLyqhF4!JI7?G1QnL9Q)VB`u$T!YOe|f$a%D+7A1*ixY6_jjeYKGf~>bTCp^OjkI;;3(58IO6<7bO{zNVn=}~gp3L;3k8JSj0e=91BXy=K zy3db%a$taQNQ6i|6e#<>ld+tg6>AveK`tl*51dl0PnYgT87;NiAisJ@JS~;H;&QR# zqBD8!SwtFGJ#!B*>=3Fr!2SHn#OAPxWRP-e6!37Dm#<)S8gZ`K6=!QRdXEVnCwxH) z?@AKH%^LnQ{c41s4Ss+tb|rL-7f8xCbP<@a#&J+%=&FwpwE9BGv#My^U=ki9*trz8 z_8=>bvSZcH9@|pXT_bDlxFLa03Z}A%+DY$+{uIxBi!#qT;b+LMa}z}mRPTrR?VHTz z+TA83d()R1v?2Dg@ z$fty0g83DhFP{(Z4`0wrGBNOB91fJRnL%G7MV3VT1O4Api!oy;jRF|}K$~Yi}EC?cea3k4$#E(82vX;m_n|`Mk+_rKM3z{;8B_>CQ(>wZzrLtEV3K zX_!;KL}9pHm|HL6h$~#u`U4i1n)LP3+Jp|eln#sS-(dm*!7MVH1_{6-^O!=0NJ9k- zP)!i^*6BC`a~+TCYtGZ2my_x6vl9@1@x&KC+|5b{mq5 ztBy49rr9t)O>*!)PIC0#F_yk9v*%j09H4*J-E)=Ra{%ArYJUyRmDM+nu87krEq*R* zm}I>gxEi=FVtcr0J?^os6VHvoDy6bwCgEl z3turzHt~gXPT`q=uE6(*uajWw2km7U3tOWfQ8f}_n-U{91>+hNS;Kh8HT=fTSSVjH z{>{1$NH4Oc_X062&6s}ff5FHy1YZ-B;GB7!l)69bT$2`UNvZo6i9SWk(&ea7;elY( zFtHp7{~aY-W0EYrY)P%A>o`g6)FP%}2NF-JQ|kepB8uOY;j)@nbCOu_Z14qE6cJ4s8;Tx;0Yxmil+^RDN-6Wx-<7F^T>?M)H|3+*q+Mf+ zyO#hxBeY5XUV(d!iADMQx#T^32NoT49Q6H<9)lHt?Wi zjIIi{upxjS*i^fl2bh)BEl_nCRfq*57v-ArwTdVyCysjcUJS!8;tAPnQWIsrOBJVJ zM>v6*1d6gT;prvW82E6U_gDBJ7;No1zB=2=9S#LNEJfpQ#E|R2jyR|oSn>lg0$Z8r z#(CH5KJP;W_7zliLNU-GR<|gi$!hZ~svlV@MJbuF1rUv~0x0QdNS$c(V^|1|wJb75@^)BOdXBp38m|zNXZM!#^Ke5mKEmBWgrNHY z1e9_Bm?rVMO@uzMtsC1{H*sMa4`gYE**pNO2uI#N{QpTrH=(APGHGDh(eHUh-`Ql% zsHkg}NkNlUd(!KVdMWch7p2e;3@LFF{zQW4o=pn8Og9>xSr-1`;fQ z>S~oJ4F3bZat6CUMn&&1;L@lr_c6PQ+S`C;l~edf>Noy8*`)-3#8D}+F5%i`-M%C^a$F)fWQGU@x24_!uvaxeUv!4!#ghQN&OCjp zZnIujNbjGNOSidR!*2N>gPiBGt8paiW#BZ_>q+#hRjQI$j1MJXMPN}fv!CIZ9iws3 zG8576Z7OFsqpF`;GMnQgp*?mqG^$ljD4V`ZHEiF#2hJ@99Om--me9?{LRYJd#^)w% z3(mOMEymJI>Ob}c+?wmpN1ke;&SbSRsDJl!wa(K2HJ0Q%4ET(j+~+#Z`YC>0{h=t~ zz1?7dmi~i-&^9A<9{!E{q%NJaIal^oq6(jwopfK*Qx01Q0leB{heZoL8G(A_LG?HL ziU1~rd7jZHkmAA{hv|;@>`D|<>acTOUccj17r26Qx1AeNMDdscvfB$Q+pe+(y`kHM0{C6%uYgt(td+3bRfZ}c++A)7CBi@LZf&q{aN|@%kXT6ZHpq_J3s&3t~ zR;pf|D$pK~=7?bn+wMYD?J5(ti@MHMDje;O)O}*>+26)Ix@h@UQ!me^+s=#6) zi?{QhJZf6;LGw>qJ|Z}&&}%C88nz|JkM~-Yr!bOpGY!W_Ce|v%W(uL)mIu*eW1VRJ zBrMAo;bKL*6vU9$GMrky6?YmE?M|jWiWQAU|Hg1-fveWrd>`KzohtFGBXQc z%R{+};mS3Zm7GQpUS*fP@f~~Y%+D+{-;iY&eGSi2GnZw<8Hu%DUx){nZp<7{V|>Z%PU^Wl8CW5i(p@_Q7fO$p}@hV>3dT%-43vJ_)YL(c&A0{8r*Aa~M)hT!!z|^7DQNmnc~v zdYy|=-Xm)%2G*3)Cz>}knNiCpH&W@AHZROtO4ntebMip$mGuJb^cjFrsdmceG@ycK z)Ch-zlhp`GV9LDjY0km*#MIA_R~%C{jHH;6AFYqo z!G?6M|L#l)m8%(pQe`#@=9&oDPw%K?uu4hgw|@IHffGQ>7;T`ljIA=4=1*25n#jBb z{>=v*(ayJhf(}V02d05Gq$nu@fFCfWA9`{gKCXBUBG5filx?YaF9RG^Kmp@%2tPKC zt|nhf7nF62&`#grE`)exM0PAl|F%ic!n=`L6i?OIAoX$Rx|sxj#|FKlAWpB&^miNoh+2Ou>@q|4 zsRkXt*iK}hkMsR@)9Xve0T_O+s3Xs>4{!Oc#OsNo{bHzOlEs& zWrq2>Q3;2_M^rhPj#V_Lv%WIS&~jKKmnXW z8<~Q{R)^qgkCBf?6cl=c@z<5x zx$nKExzk&U&Hqwx`3mraBMfMsjGoZBWa&E@Z_P37wdeZga8QU?tK`(UtQj$#^66Nz zuy6qb$^o%TeVkIcInkv8CDnxE@sixDQrSs6UBzu0F$De1@%MnyxfG#DvEtWWpyB}; zQg2bXuM56{%CWh;5DDGTXAtxXpA^ZNk&lLW?)9~`zB<#(^YUIIul+W(E}u;gmx$0m zJG6i~ZZRvG%_rd$v%KtLMvz-PG;m0mX(Z|p3;D?>BTkM2SleOZi{_tWt%YZUd1hBa zu?OQe^}#lw&9pP_7TFw&8WLfWHZwZU2{eO=Vg2nE4~#u-0g9Tj8)%7$od!ID{Oz9u zoC>QhW9a&1;KE;@AUx!H8u7%oUxug!6Q05D9;Ll#@+}N zA@~BgcJquMg7M$Rp@OHV@0#4rG1PM{&iOb-g+$#w0mth2az;n8j2fPPm~g;AOoDX4 zXhIAgg6myCQw12LDT02O1x4Bc7@(;>I0j+EaQ#pSQwXEqqYvihzfS6kI7Jt@9>NF$ z{E3D)W&_Pk6AEdXt{=)6svinyy2{4TP1I5@Sc^EeOD;AR_9osTCh7LAIKeN)H^mS2 z7IvH97yS^I0RJye@H+wbf`M+N0`5>H%psaE-N7Go8_-lp24M=MUq&CMUj|@G9bLwt z-4FW1jpxYK4*}&b80NZowNKoLuO7#WIKU6Wr>nu_qae&{*zg#WLbk%%P~YXS7@DpA9)Jvu0|NDDZGa4$#!l|b!$x~2E9sI*P@+C2>7&z+o{^P%d)PNnu*$xe2zs@GFq~HPHlQ z*gbH*bL;~SBytkZ>foP)xsq|e97=)#M)}{g2)~ZRhWEja#Ed46Hz*Z;<0}1@PC|q& z_JY{u%*x~467*1%2021k$Qfdkr;m^*@D@fzNs1+6c0=XM{;ffU+q3+Q#`q2ICccO%1=VPrGL-@Rp|BzIcf?f}Z$;mn0X4l8NpYOqtvF z6E>CX&r62K!H!0aCRQ?plH^8`1ZL_51ca{ZTlQ_YXc8MlRe?rMcDh<0& zJO~FnqCiRF2)jpNI->ZQe^x8m^2X=!ZV0|o-7T4h(q?p>27y^LSSTJw&H~j&{i=STG?bX2Y-wyft|f^SrrBvNRsT?pc_Q z%zoyd<#M*XpuDN@ycku5L}KJVXKQ8>dTkGs`!k78qg19*TMh>Mzk%Q5LM*&--HIt0 zE;Som-}6-8%T>+t&}Rp3xL)`WdohO6r}4koDvq$E5y^+J2Hf{b{vF*NNA}y6?MKEr z{(}$3V@3VbUS1i&VK4MIAq)`FMCOg+djQuoZTdjgce?;8_^k{1*jP83WfD44RC3A% zgNN|PY`svvV7wA&co$_>Ns~#I2f5Top-aFbcD!l&as$KQW*fA(`#|H8q{v<~9FPMG zK*&`cdI&@5VIbr^Oo!UU*hskQt!|_$i;rg0^cBuRV}M}i@q|5hRO|NqXia=N->MeL z@nqd;4D=!PQwnx{XUJX7VBj#LdZx`p5G#xm}NEKl4UXA&VvbLbrX*t!Yu zb(c5`{O!rhbACu{#Djc@(v`=7L`*AeT$oXAO(fX$Ir@6;j;D5mT=%{M9T7KL@u!XVsgUcAS4Jcd^veZHbWf0|lF!#PpO_$e#3rxX5 zC#{h;MXEG1A9X1{p^RF1z*tkSbFs-11KqMFH)o8)*}zdM9)P)>!SDD!7_chUy2x@O`~|(EEM2HF6t_P4VZ~>~DvUxT|E|^NDEZGHcK^ zSSOL`aTRpFF)p>cG_P$Y$yJ)3#2Tkmm5F{)J>_g9bCT9`h+-~Wp~cFo;=9*KS<_`Y z59g0_b>BWAz2S$+;)==0jzs_@Bf39ID-1=CAjXllsxi+@Tou zU}vZD{32)$LW`KzCKMQ!k7JgRekqztmXHz}1V?1x#laqDb-LKCS$if${blK|)J@g< zi5I%HdM;5ay_!d4Q~N)VjE9*@RXm@c^zS*-kC8}fCVPw&(&%1f?ZhG$e@?0KOeS3p z&mE9G6cLWbS^!zRvKORWFevRYs8DYdiHD5hhY5GG_?4N$Vg^F1@dt;WHBr7<^EXpr z(J52n1`}&PkLq<2v8bf6coegAYf1DrU>lFRVzlS5#`9j$!U z`9a&N{Fq*p46&l&vE@@MoAu5qV z8gJsq+|YM1dX}M(;$)N z(2pfT((&4ay|oIE(2J2Q!pI>@4+benk#tm%A^eKE6$^!GTZKwS_{GXCJeOby3x)k! zB3n=+D>=4Jc!;e`Ge>GdXqd zV)&^qYiO(0c9Uwp;;LUH+2vASK5u{1&(A`qQ|@Q;m4+7qhMJCC0WDrV*!HV7PNWIV zjP0G8o#*KA?N84lJiOn-KkdgDL#m>#SgiYP2mo;S?<8KjNX9)UM#5JH0FYFUV*WH< z#!dp=5+@vAe2h&#?fu0M5as6tIpu@u{yA=!)Gr1i7gyN=_?~PdpSEIJH^t&uqh+1q zz981{_B?V${?+bp6?HH5L=`8g1d44?3K14nYXq(Ldr=eo+~*E@Kh4KG(Z}-*aBh$m zGWzkZ^6MNsQt+=hw_uUfoCJA_;PJQdk<)dXyacZ5K7CN*z$KK0*RE5uKqJM-RT8Ig zZM6;hg3Lc1N%ThXveZ!$gNMJ@aK($@f;4xUFa;#=oop(s^x*Y7U>7`hmwI?5N;OI- z^FE_bN59mSkBa>M7-)w67Jk~jms4HX#@7hT072<0#N|_2NfCz>3J|> zFM9w=rM2T696NE5k+wV!I$33my9z{X|7OfKk8<|9`Q)Z-_)Wm;2Rq8O;jC?qv(f5Y zW>85nPwn=|+^A*wPxiX6$EeR5chJ+6V?>~&@~-b|D0uaR-uHZ9D_M=c`(r>VbvA2k zN8ySm#HUHxR)C(u?e7*^CjIEv>me=caAAD)c;OYDV|J73IBR6pLmgdQ?Jb{A~AY6h(>oFd9*LF zQ|_smO)7o$3Ci6u!rjn691UhJY*|Eqs5-q^VJN`oAVhNQpzgLb3~(kX*j)0Hk~9fD ztgFQvF-JmzYTHnLk(fCoK?fq!NmUYd+`vklTxo-qh$ca!_Xr~UZ_U@?d{r7CjLTtH-Yf4e;wLb{~ z^tMG?_lnayr}e>?XN4G7@ai5F|3tA@?Z2nOqnA*d9@3*Pm)O$A>sH#$7{#LR+y9|; z^&B(wllS$8U4Vzx@QMT=J5APcH97AQYLyT$mU&D%@`VMVTNSr!)uv3)8^_vBHP@+>JS-X{MFG7S(=+x1g%^7gn>@rRtxnVD9J)cGG$6Gyaz7vLchXjJaYX>lkuN zwZ6-fniTtO=uHU=@7G2%Jvg71mm|DUCN~6=$(*%q5@ocM3)FYGa(bT{nGp>gYZbE)OKFb$emtzqn=uzf zmmLzXZhK~UGCj+4Uwn{Gf+eMYnH?yTq-DDGBZMrRi=w)9)RJItJ-jkAMMdC<1l|(P z!fQ{OK8g{{CLM!_0e$?9=?fuQuW9|c_2Jo9FKIwRT;+O>%>(aX1!j~T(6vexjW{Y>oka)-kKk* zdhQ!TZs0>&?vaIWe)drqQA@6=I-{)mV9yU64^{U?58TY?w(kf-ug&ov+WVs!{ww}nz z!Thl_Jhz|5||N#$7bRd?)L*>w4~Ui{E^iIA$$f8iKVvhcHcM(IA`CpxVHxalCxIB6O!W;wFQ_;kq^54=eSS z-r$+l`Pa?Oy7m|?~Va(Ey~REb)XWOC&&rG zPVj>qxBGR}*x#yPE;+l39B4<(72bCsHammqR4>;Go+dyd9vL5ginI6oMj-FLjgxhk ziyN%9_NX0YL?X1tj=Fw`w$=iWs7x!Q;NlqLSYi zbzu=0KsPfo!q2u}x0dbF>Zfc+4fGZ%OneMCsRIlbuSAtMgdt+#k6WE+yPJWHn!Z_= zgUWSuyVZ90A%3yL3fu3?e?YHgrBNPap1K9qpqbr@%Gu7tBqHqJGQm4@D>||9x>ae3 zkghxyLq!f`-DwBLru|+B-5)n1mCWDoKc8XM8UHdW*sAUmnqd%a2N$IXNPptfPE^9$CH>USx z1od02t)28A`i8n8**m;F$i~lq*F4vj4K3fg81oh6p=3dL*()Ml&))TEX{jo?eVHh%v}AV=pt=1`?6_Y9HL#(L ziNuuc@reTW^o|zZ=^YX3#x&f4{U~Upy3&glu-`nV-k{mNku!Gr52;WH<7g~&O6UkO zfD@lDI$Yu7%08-WukrfZNJ+@im!umo;CdFLqw&+e#NxLQ7$B!L5 zfPf%dl_C7%1w42|o3JWZ?cHvFj2a4FuB_|Fjl!tWXaODBhw}^8trYa--i3_Q;NKeN z{K~u^bVI=%7Vo;dq{aEpnJ~=nyI?sQ{fPZ5xuUeV17Ma&stmJm)K^72N-Kb)TI6jv zG}8+YLa%_y8BkkBCN&(9%qekm9CpfTQvoCM@XAPU*JvJ*g=oV7W#`yaTJx@93At=c za$0O~@|E{6;(!B)mq8jGzRdh5z9Q{#MqFOdbE|luzxO5fKfFw#>Kw678RyT?bzAuq zBe{scZ%i*0KAfGdS7WK}rn}2zP5FwAT0;W_U}S8lsIFAf-}kxE@`|TQI}vJ~$A1Z4 z=61u;c{|F0LI1Tn+#U@qjHTkJ>?lssxl|Xk&FRD8LF;=T zK&g8r%!4?Qb=}$AI(1jfd25p<6A%cffM8u-Z|xSdGTw6aYz_|2LIdy zBoLyyT=zr{qdpZ_OKl9pX~rn}F1uV9oZN%n=|0T<*ngewp`P>-xLIj|^n>QP)$=$t z5#%w2is(?qhCD_VHKl=1JZ5)~?G|Laq6_0iQCT+DtL2jMyE@(lC1z)8<+YX_A9Ge0 zQJzYIVrx(ei<9vzW2#$Ysgcib#Z2#I;QvR6eU2?TXpi4dKS9Fmk z(@(!YdMaYC$#>KP6Bg-a6*jV$wkCSB3j*Kp&TBtGM@d<$v*;p%&VTZJ4FvpIWJgp13Q#Y+RwY+WiLq?)#`TNi20ce&)+^*`hDfj^!msxeo1xSnE{?xU&z=OS!*27 zEkTc&M2+x_wilPzpx|fa?G{;eX6#M{0F_I33$KWUBercx-O%t8}@!N7wFkPqw~d>p_m-+ugzo{2Qj*;hpa_T|j;3 zykOrw9AS9Wxk|XU@h)g~%}KDPC4F-fZ+j-(lI{#}FQv7pi2rSJf0gv~1SjVBK;u(j=DC}OS)PuS8kH)W z=FQL2)?6(UH6|<75VX^1P|Fm6?1}@mi@ei(x;5)=)!~*v>TT)Gq(8$J5+uU069VI;Reo4L8 zr5G=&Evnda?YMx>ai5$l=~dhzu$+Wb4*sLoRAXRR(FTqcasA#g}rR5R4@td6Cq5bU%_^PD90tE$oFK31t3n^&XvS8E2TqQ%;lAZ!z;@-dE@FZ zt%`;arWH?h%#J5T!bu|!j}K=Yd~G@})aQhLSe4s|G7AO4SL=*E8TT!k$N+Sb2`N@c zcvy>fM<}tWZ|Us&{Q+r*-}Ac1Hha9)2Z5hVi-FTzZ>FB$}l=}TAE9Ln2L$7<0<_NvP z0|}jn1^bFv#)pgLTv{Uvo{=aZJl?`5k!39&T5sNkzX(=FdE~341nu)ov+-2YI z7U^Q*xa6LIrrJJ4$6Bj%X?NN8T(*vZ#?HKM9(mo|)a9wQX=-AscZqx@6>zE_Ubwq2 z4a)*HxpbzspKEfI?|QDi%*?asAOy{sT%~;5hj&2lxPYcx?89%Q0qv@X^Bw*k!%Vp9 zc7p)8C60jnOVF}K!Viti@vFR&)^+QxJ}l_b;FRwf+YXxUxq=>_>$=~$=YLFGU`A+jMZ)~uH*T(@QeQcqc(lcqq>{g=@ zKUprIUW#66&UdZi#MDzTCqN(rM}pcUd&@Q$x1wmEQdjz#$Z&YMNK& zm!~kf7({tG15+kf-8ROpRULU{zb{^6jK-q9^Q)P0^D%s7^Reo?lu5H1Ey;U;$&<@W zo9sCF8wJjCyI?rPKU3Z0+s1~4q<9-(Jq>MB?c`E%kFBD%mlOzGoH=BB{mi;T=2JBw zg?jv1YT_PXsl}w?A1e%Nlon-dyUyHkppqbWPF$*ORKYqZ?<0>#9`ouk_zc#Qi1@<^ zNxFLzPwRxPiS0Q#rC%N4ZCiX5_(P zTP1s_ub_L8PJWC-q)?_rl34`Al;eCEX}?&Hxq6bmf??=&i!o6~;6+qtPE*1r`*#yz zT9VN5RLxupp+DJ>!f$V$ONe^qI<)Bhs@88to9v@)N1nRsD+phEzFCA ziy=NXvIMD>E%Qj*LkOkr-&o>QsDHZyXpGo*_ogpilbM#u=j?l(P39Z*LeNevW72@F z$^~|f*$`HdOuoWNIz+6bE)Z5^x+)WqbJ+BmUT?yvGIb&-MNLh?#A>cl0@EUqu<9p77UKKkw6%I<4Fr1qSZioOg1UHG(^KLzM~QKT3W+5im@#zx zP)qs-WYKh@*5AH^$!afrv z$c4^N4Q#HEt>3@L@~kAJ@8$$kp%wU=#if(1TTtul%RrkeY@DePOSN$jA=r2#&iF+ek`s*1Yf zttzIk1+0tbUTHz56`a1AI${i8-VTTHf_QskY&9IdwN^sxjAwx=Z@b9Ja_9$eMXNXh zt~O;8z=@m>+0g4R(}=R~e3B{|xH&eov0~bw7kxlM+nI6eb6vz{FhQfn>oMj3JTgY) zaKqi-JfZHZm*;M1D4490EbOE_2>WB=O?^|tQo$F|uHtTCCfjxS2i zo%#)ic=57t`U#)3^!_PxUJxhgiZ=7LyuvJk%Euu)wNi47;knWlVz4^G(m2BMvTSXq zW@Sk^f48%ev&{u4;cbms&oQICB&4-9j~Jyp_|?DAmvhD619>&*#HrL0i+0-}<4kyl zhTV^UE{t`Cw(ynV^9ru2+Gvw3yL#Jlr`t=*v@t){VtQL`<*xd+5%(Rj96jLq#?zI4 zmB+Jnu!%Z{W=vtV<9}(I$wBC~{ttf4x1||uy?bMj&#wRB^31!Z-){C~u8zpB@?Uq{ zmF`WX?l|^3M3p|u^VW>3=g~M_aHd&n&A&#SQ_Z>TiK5R9U0%KU~! zy&4rXnF=Kz4y*{-Ie4U&$P5=*3s?JtH$~bsfGuu2)k?&na4w+6rkz3s|3Gc=7}*InUVYT|7w zgq8nPvhy6T6)0hTjD!o1N%n*8u-2lL1_DsQiu}FiV`MoHJPO7L!4Kk+=xMh*$F`^@ z{(8~F+vgfsNvyzD0+EP+Chf-_JhtiFKFBLLo>=oc6Kv)B(Gp@l(U%hlld>89CH-+u zDk>yX(kx-^{Wpk(rZSnwyWa$@80<6kQ=O>3qtV3e^wTHim26jLqyQ_9`8h3H-8M)< z*+6LxqoGI^iLBMEz(PV!41LYfO2?l*KU~7z4s}jpoo(^BAGPu4S#BFvalpq7suoIK zJR62Ot04Fwl0T2kjyJM+3CCr~DUF)c*>L{KFwhe+8z?5R>{_AzNr&l}JYD>8pbZ0p zBF~Y~vxgfM2}*ll>=#xYL@IZT(bv{7Ad;wMAG&;oj>-c;Dhu&XZWXc{@SEXV=nXk! z9KZ+cmwQR9WbBgcfny?r*zaL9!KbVZc3W>!K#X`o@SlN+72Rw%rHW2VrrF2Yz{C2Y zSbqRFwRS{!o$?DjLGA1SvjSz+$RglcNw#fT&WxA7{&TSYv*GB$*K74y%X2gVhFvzDiBIwX5K z&bA=8u7-Jx{HTe!ApAgmyoRmfS_X4~=9vJaP&X*1n~ zJIaGq0=O!wQ)v+$ZS$xGFKlep;!DxEvm@8Hn~@!T6dB3?`BU zupc>Q!Nv}JZ@dl}jbgeU$58FSHC@bXwfZtXp|`jh(>>oBP*8ilF7KjPxj1z>U5etE z(~|Y&X~&o0-pNSi=(Fp8OA&M2Jp*j)-gz;unwXa8CJ+qz-9YenwKjAu&HqYgslO(K~=F|&2XQu27Ov3 z%y_esl+5DG@Z|ul6z|v(j*gnyv%5o{HwGu5$S;0FITyvMkj^nbc?cJGajVeKiCwMB zJd>{PFTe_Hj+6TuUh%1Py|7*qna@@dzp`q)HmL`6y05#!ty)X~2?15(%l*9?m}c+S z&_+oe@=);l6@9--ojJiP2Jzn#SkT&nizt%{Y}wbAH)Q|;S%no{;cU^NIIof=s8KG} z8R}#A8@%VlQ_`RL(gdr0I(pe51Vj}cuMX1s;+7}6INaa^n+UIQP+m)<>}k_)XS|XY z2yrDJqs6n$y0fi%yG?j6F{6#vnLc`+%WvTSDCMePS=CB4~^kP0@`pP%DT}PDxBpoq+srRPz7X(NI2l7aJZ3sPhNbj`M%q z(bU4)#KP`}T;Hr|V+Uya^>wY+Z_2OU!6}r$hgzoKC*Im=F{h1uS-gsFq0fk%eM>CG zo#2}F{*=AXW5UJI$vId@=E%vCg*C%l)P!`EFm)1{tO?DYi_&wcnJ-S^UYOTRg=UvI zKgQm=pfhgfw_cU9&61zMnr(OB_87C=5x?86I-If{usXHv)>Q5f(xY><0frNuU zgjoUP-+NE(#ef`mZ24Gr?ETSZ)TRq{C9I#40T88w?ESmB7 z!D?%5K{J+|IEP|Vlq`v^Yi%P8Hr0kAHZ(x^DzW#7O0?|KA6zVLh~pH%oc#83qK(I) z?2#V*>bL8}JEuH*CcF!UO1ay@Le3QTwDx`;t+^+qmQDDsu2nZrBp`3MT? ziv03Sx%C=73e6hJ;m(Xy)B@5`ffs>3(W8ozh_klmh44fK;f&qo^{TUo-E?yk)kgA2 zunodso&k!?i6ERn6&P}P+We>h#P#vrpokK;K|3codl!h-M&ToZqH8obM=?;a5#wTg zLerk2d#1{=0w-!PpByCXv^X{tw`$`?hNhIMiEsVUzNkhQt-u1wTjo^~D8Tt&=*9&L zP^B}~G(GePlr+M}bsPi*9R^X`^0RvF^JUk0cG@Xa?rP-4Ar|f<6Tm}?YF9f2QbdS^ z0xCcs!WsmR2-%7-%W6G6TT^ScW;g==S7#;v^jQA2X|uG85Cw!2+WlWwS_(fh{t``9 zZ|KXnAdb+**jD3QW3fa+KD4`#el0u#yT997@NN~WX+oP8o%6|-srHD!boigEpsEBg z_T)@i9FrPgMPc8zq$$99Tl@V7S-dT7gK#(Q9M(2|k)U+hyL6e7WgHxZ>0B($kU?CH z!Xz*+Q4n~qqz1}9{$AT}cb4re`xE&uo`hq<;$ij;c7Dn%MRwxrl)|0o?j28Yc(uZlH-s7aE$Cf{H& z3m3SHLjqYiLIpYu`Upz6kWd&HaeCUPQRu6_C$T9|29jLh&$1WuD={|910rxm%2`SL z0CvO1+5##+$AH{DIILeRv0AK5eVA~b;SQoaZYwkyM-_-WPA(3k(OSC=IaUNk%Emy- zvu#yHd-qsjK#$O>s*9|ou0Fb3N6}hD)HjAfZ|CAWR1%KGc4t7{Ch1slqWMehU+#os zTBRZ50b0qn5g+eM42LprEF^kHUR#P^{!@fSUfNZ5e1*+O1TXs)iic+&w{=BI9#R8j zjtvl}HkFPT7XTj!8IJs?tq``{h^FM#wX%=0edrgEhob(67!8Kf6P0NGeAwR$PSacV2ux_4Vovp_n+loB zJ@8PxftnZQbomMsR*(d;x;7a@G|HC0EUcKq%_yW=IwU|r!|34a3I5B2dqg^{tyX1` zBXqp;pavQ5NqOU0P|hkrGulDC~8%Xbk&%|PNX>bg#sllL4y#@wM8d)WYglKf^p z=oZGIngGIlZDEKkcRd+G;?sseoEbd0a>iQo$&JT+zY(LvsV_RtJu`dE0wZyX3@gHS zoa8Q28fY?iZul_UQjmJ-%I=yaZjtKpy~mmnJea0FVM1|g5VjH@YPc&@pV52Zigqa%(c{(}Mv(e{I!CJBv6yq!ujK^-xA8Hn({9U@MpyJJ z0u+Lq+HGpim^;n!L<*$afc=+C*|UG3#F!FdW|(l{=c-~IeZl;KX!mgz_Jk$LQ%;#f z0v35|Np=kE8LY4x%oU6Z+rOlmFQvESvM&s}B@?^ti=dU;K(_ZdxF=ZJs3@a?B4zzS z8r#_al%9cB*OMLGT1{q7Wjh;g_g_T7LM3KH3Z_LQ3$#Hz;??S7fSK6d2oy>uUH`$1 z=*}x9b|gb@YQkD7MKzN28HzY8NxSx8H)71k3NU6gp6m|Kge1HPB!J=lM|aIO1$oK2 zxyj-bjLr&E!^B}zHVnz8RmMnKg32e^e?#!&DQWgv70vD2k(qks7@qznK;sFtK$W9m z0kmhe=sO!@bbogLp~tTi2kK-G)~Z*_|83^`^*Y&%e}+2yZ62;SEBhTEhU$@^KQC5) zmtB9(IasnWkd1lTEOk9lr7B%9R#Ns@k=UYi9S^;ew0|3Fk=4nlD)pG>^} z!y-C3*yu~vbaa|mQS;YgM0V5Q2v0RdA8>>SuUlP@tePN|co*p2?mwY~&ZsbZ!4V9p3=sl#nyLcsjm)y)fr0I*FszHaW%M#odC-dRS2?YuDy*P1x{6wCbOXC53w?n6VIpCUauP2YM zqmTPTY=|4~uknF9MAQK(Uf893kiOI+ZRnu8xhEh6?MBP2=U5-U78#|A>E&H0+pANs zHEy%^#zG$Ibi`;kL)=KiH_TDV7pzcothB`2yCj9XtF;qw|3GQ)dLx`rrZpO7MA!YU%xX)g`0z%4a(B-+gx6q@XHTq)Qit}I&Oz)V$M79J1E5ehS)nI_=t9HpznNg z7=7!m*ErsKwrLJ@bbrOG2_GcqH{*ATsJFH~vc?VnxYcW$v zxO3v|y#v)S4<-&Z1y}a)ixgQH;-FyB-K*CV=^KxQg?IewoXSh2;g+HsVR!c#e(ZH4IBCYL>`X7V zH`Y;y@GMypd!)+%GZF)dW<|;)?>#jz=aFcj*_%1 z2K=AS9IS9ff7J%4T`O^f2*&mpHV_TW3-O21pIzW$VtkaRJQyU1ks95~b4K%{oO zP^YVmYcHxz_U%~%kD_!=TbnljEs~y{-jvd#MMlCvN_89lR#UOWD*qfl@*Etrxh@J4 zKTkxAXESLQ&Gne8#FE^G7YHQZd&m91_V<+?gcTZ)C_O_n)_B%ObT)mRZ zg|VOVecskSi%yx2PG-zeYvs?TZ8fa09miDM11mx(XOy75NUmq>|NY$qp?#xoA|x1Y z#@n61{=DZkY%pq<#Pm6WaBQ=04^<~ZOQ)ylR`;$)cM^NK?$pZ4p=zFPbtDBpx($m3 zxIDhyV`zQn!mmDQr5EY1>VF`s%x&a83el;W07?lc)1c)b_$2s`NE3^ZRb3Nz&r1D~ zQcXXD?f)pL^z`gKj0}v-P4x8sBiR1?Qd8>i=*SQ#fXYgjA`;|m6wcfqItj5(mBPBqa=N*{|OcKWdJizC+!w1hDisLf0YW2MHL zoFPV2@u=t;BW5GwU@0tDR$VtqwQig;te}ZhHYt{LT`yO3o$1~N5)79R-v8`&JEt>c?rI#D1Z_v!z-@ju8_2-F66=o_19JKR|pG*#OxP-K`MvRy>#(4_fm!wcSBggaWf+K`6ApY6(n ztQY8m#sjwcs5bJZ*rbhO3eu;HjHLGk!~#<<8gmg}2J<}1-E(joKawgL!6|XbI9pWD zh35g%RXNx<4PaEHmaP~XJSFEXms-H#dtrd@#yYA+E>%o}twx9zf@!TN9*6Z#PdoMs z!wTqbtsR`^+tAq^T}JkBhe87Zk%%pFC~)FDzh z3XvQ+pd<4|Ob)8rvrInN60_kf`(Ed1&=joztEg7JhB>u;+CG5DZjFoFc@g4v<=Nfy zPhIBmN>>boC5Zw8oHSze9EBP)tLGZLNbY<@XREqwRRq7ZrtuHxSfMc(ge)7DpyH)| zg6Uo>i5@Z`S^I6w=CdVN9}Me~bFs4jZ>A+3H&SIW4sC08t zDClw#yUW5D^g_ICd5C2>S#w$PGM9~nlv9hTRRT6|sk~LuTrZ#zasE%VP;kbKe1`J4 zC37B3_A)Cyped(Z;$$#yY_sTGJ(?}vs|ayfJhZ#4vz>1(QXn_~FEWcN(y;??L;OGG zA`4O*IUH(Zy>#yK(1aCe6v=OysyX!~h>1y#L%8ukWnwd`G?IKP$M^*))H-xp^mgq$ z{~$t{mA+4&oX4yz=hkl7gj9ShQl_^C3jIOuM`oru2e254fq%n|6=Q$IFj~7;V8$JC zmiq=mnJ3<&JVmR|$(9!mEfoE^^MRr>Vo>cPX(iW#@vh1nsGIV!wfDPXy6E5z^#I-o zudoBcUtlK>W4$D_hf?v>F!heo9%a7pJckcDi@CC^VuVsqoRrHf`bS&Jd?IQSr0wY0 za~VuyX6jswZSe}voj6c8Q0a#ORnZEu_`*@CbN6)HiE2^0Uy4@~T%?_W1 z8T3}xkUnan_R8i@RrZs#+RE;NP>4Gu3Uqb1lA01e64|Q}K&i;X8O0hEAKQaP)=@>? zmb6Fe2G0@d0xMv^>pM*lS}dq`O`ig{{)NHkJ??v_29LO?C1!+CUDvIw(c>j5s`*co zP54HJe8-2LwsW43OK$PYP|>uZ-fQ#%a2G6wMrx2-hs<(rT+HG|1O9HHL%{^X?U*iz zY)xYZPwgXl%=hHhWT{tw`>%ynEl5hTPNKoSJPqT-#H-*LEE6E(M$LIj+tI{-Nt!EB z0pVd4A^~?djdl`5`g?t6{bL=>&4cgdB8x` zz~=q130)jhq5#I&Yp13NF_=Wq0V-yq0*}+$$2+UQG9Hxd4EMX13ciby3cQqJ;Fcl? zANkxVugHI(Ko(TXTGduDqCv{O3x=zAph^5guY z!P}D7NTD&++tkG&=&D|%C9W(E266=G@ww5F^&O>upH9Yuw)_T|Xf|G+!yidzuWdv= z=|)Iba8~VaYjED^yl9*`x+YU2wS~WhIHQ-cbIEkNO=I;V&ibrc2!$((uhwVR=J95C zT#IjN+`8sm6^MCp9pu)4c6UBKH~uk4&0lS>9-H74)%O~)u+{Zj2p)oPYp^1-f=+iq+P21~{Enfr3&w8oOiM6byYnSb4?iOZt1;n*}ZM4 z?jKekip}8@PIG(FX{axfsO{W6GEk^=^yM>ETR{{iuoI{$*lS<^@)08al4@;Y7*2KzWWN6IU5t zjOyA6ReZtbq#qgMqM5NULXi((ScD-EQEUwIphhzf@Ol2fMGTl=Fy`DyI=U_2?@(DQ2l7pnaOGtlf6ne>0Ka6 zH?x=*yXB53omqvSx4tLl{Bf}n8M{K^xeqa;o9vvut=(QXxxg|3bFW^=vTWG!4?NyK23sv7~AK zR{LRd`G9r>+P-n04lmw2j#`NUB1)4@&g{0*DLH-py4TNLI>9I>c^{eI+MVfIa&zi- zJ3RoU1nie4LU;n#UL1C{I`2&$fQ^Za&&R`WMK7W_2WMMecg@Bq9&HHPW^j-As)^IidAwI{8K?AzOg?^bTE>{)e{B?la7v?Y zi@ieyLBM^sP}g=d_YvX~q4)M_^mXgdLhs+8{+^N%_H`4SMn`(&2nu%eL-dJwZsT7E z!|79uC>zA&lafIN5w%S_7upgqf_^{-3qkEf;rMNigKAiy#enO?xt&HSF*C)T+lM)W z)*T#Tr_o6|KRPyD;C&R4&g>lqHYNUP#RqvkK?eS3mMPor(<+q#D4z z5`*Vg`iapnLbyyvXlG$EN)`4TdVs2l#xh9tD7;62Z7FleUUSHP3i_7#6o1$DTbJa# z9l8MW+&4mLgR>YE|D3RdTBkcC@9Ymt9r2d_=EiDUe?c@bBT2#cFtBanCM%1rQW5)* zb=a!4W+%8ke1JG?pK25=B6LNWH#PN@EGY4$J{wwyIFOFEaadMFDb!mC*$^d`Y@rNV z9H=6-q`At()KBdycJZRd*)IVV=t>5o?xSf91qiTdgPn;GkW+5z35T5g3*5ZQ4jux$ zb>5I(@XKOcHO07r=pEE#G3;o_Hs%KQNT7yu5*JPs3=f~iG%Ko{aILZuwNYB8aYLUIK_rIxwL4KxwA9^W2$ObH z^bIQ&VLOR=Gb<5^zfo7x_^ErBuw4OAA7HQfmF|egu#@5dpw57+p?a_Bj7-})Mv@(7 zFLY4o08SzZAW@B2&IwC3{3POP7~aO9Z0)xapHNQX((Z`SQ%m-?px}Zbph`WhT0zvR2K1X>G_H~TI zj+j^Ghj|U zCY(5KwP}u1mV6*=Lm;d1CV#YYMWz_QQt!LWdI=e0US(=QWkZ)pvM~2aKwli`y6lSY z0V*4e7uT!T%c@bE;{4DrPS(qgagLIiK=k6)foR!Jg z^E!Wy`;l?7&dF2Avw~_tDcC!2DPk2P7W~j3zPUrZ}^>;Pb_b^fsANpsjyx9FHf<1&;S&EsY4OL8e1h# zA4WQs3lkdLyp7Y<@yhF{Uz$c1eld?PyUd)@mWLc2jfuX|`)Qb|Y11WxhLyuxasAhU zLMJkR!f{%?`{a}!N+2v#w8EfGKM9rbWF(R-EBN7MB|i!zhK9(GXYPx)SgGWSCTxze z3=L&34(2>2fBQJE*+Y&lXJPZ@C#mk~2R>Ej>LTo#a1K5(byVY?)45Zy4{mAD6jJGl zJSIr`gwugBm>hc~`^j*yrc#{q5mY6RRf}ziT7`rbBM4ww_X%&QDrI|Q1gfL4CZ)~vjI14dg;YPLAmYy(}_ zrXz+9X|Q5sC_eW*uviMeD>!{msrTWv0xQ(su#n$N-a*yYK?!P%sjbOSo{^!mdIkNF zFb6bMg{%RJ&uBtsMr?s?hIHB!7ZShP#DD4k61AtMnJHimkKWmS-A7b2h$K>HLA&bJ zrU1*73lYC!dqUMRf|g~1C=LW2e$M>FSDq>v+)P7h`uR&VNPPR0c5$YHJ4h`guHuU= z?GUSlThA{UC&5BzFE7pU3N0I6Abb&<<9ib85X~pF5dtcDUz_-6f2@)g&yd)Z)a{e- z9Y-^H9^F+wVpxfPvHV&>(+S?rHxCLThG5c{BNO(>tNgE zhYaNE#vB9f&)+n@iiQ3~sq@i2M1^GXu=it32IaS=Az`M_az)o*65M^U^Qb>M{Y((# ztV6lc{rkS7A5zP+P6WRy1XuuCx@M;wC{Dgpc&cZ8f7J!H%g|un?esWkJd-^@!oudCewSJ=@!KiZ3k<-6KdyNG_-1dS`v{3+ z5#X#}tpA1TSXXJJkfe(Pwl*>5@fKFkrY(eaxS3k+T)d8`iG5)|kLaUHW|hi$U1A-i z@QID74Mx#hg&UfeUdp7VN5!ZnS?yM#R*kaTyH%QMgh{nPi(DGfps~aH0sX&CzW?YZ z(7OC{|AGPmox=hFq5pU5%f`;w#G2t>Xpy}~iR!N17CWNvnx2CbJ~3INVb^s%7J#u@ zhoF_6vcA^@VnCx=NVpI+(UIimi7OKIX-Z@#rXKkIb?2_P>o7u%Z(Bw3@7K@>9*P*H z)t7p>+5pxMPv~*h(}CFMfMvty zA1L1vG|faS`D)*TW(7ERU3A4D`lB1B^(o>uC3{CA&q3jE)!w>c*$2USxbnak0ehDT z{|~t{y-DU*bY>`5EEO0uK_cYk3TE^g(Efx*9>hWLwcy}LlMl@F{(45N!`v)+4|#aI z&`L@2p$|DG%n-`EcLm6S7~#a^(|U_vn@@=U0=Hz9#5EJ6@e)kJEJcL!x329;xT<$t zn;M!r2`Lg;sKqsjVMv~2(M=T2=V>8(rNcdSs$K(NMNQU4h;9~m>ax+oNK$^D%&Y$G zp)b1{cY}M9j@+>fpLaQbnc2R5zxSx9K_E+ERjUoxH7xfcBhkXPfq4n}6g0tR|BWzJ zg}w%B;ZDw15Y0&^=Zl>HunvdZOgxu_;pTJ=4MgIbz&4d;nLu;;l;v^Ep?ZN9vXvi^8dgWGa^LSe2>M+-V8`JE)ZH))V;*n{&e3an%qmu_{+01k*xHx81vc9G^ zy=sV?)L(0|v0a0x{dxE1F}4mh^*E0@95yZ(ghcw-BfCgQy)tOH249{~-Ksg?y>ZmL zoxJcAPc}ip;QeO6NJZJVm&ENzM9Yw{#V)a5VKWLoDfx^t|cJN>2pb*Y7x4+LgCT{~S3r-TA!W zj=%Y5>S7eI0O2U#aP?)7;X#4n=M^@rUOU34m(#GXjYs8*Y zal=#ce%P%ze6wPtQbD~@;%Tc<7FMlPAN>x~jQ4yWPyR-mDiWeB;vCYppcBkA&LPtF zc0Y9>7gP=St5Dn>-^mt;9jPuR} z?2+2T`T1Z-gx6_b8kcqxUVpp3v=Ja{f7vTZI6P$B;Y`ZOFCQz-+U3xw)2Y?Zp^V1i zlur&_=+HQzM}<5n$SW>_Il{XI4hZ=MK4BMfWNkg2JX5HLEzHF}&{WJPt@{^0v5-v8 z8)zg%Ax~;w}kKmsJrfDh6gR7V?VEIa`Lj@7Veycg4f;YF;k&j~65AyI$5&FQ-P zIRTz<)F4O{v_Uqf3b$kNrXUt4h(#9yMlA*rmQ9(Cln?C@ERw$k8Na+nsgP{t<_Q=x z$nv9?KHZ5Awq{T~4ZR{=ZGdNHiLKw}T4EIu1(+=|?Vk(qle>TM(u&%Tk6;iZtL@X$ zR&j0<+NeDOWleDR5J}HBBIEA7jSgnmqboO*kfBK{4s~E5-pDYiFQt`8+t(og@nG0u zc88PzKkr)*(Mcla0)Cs!VU|dHN~1_6qFHF^1Vd`1Q2X#FlDalorb>mmS;Ol<%M2X) z%u0FEH{^fa<0i50@E&Ei9{BFeDx$*Q*BDL2VhK3FrbiyS*_CFN!Y;BIF+=DbE>VoY zZU8I7SE6!P%y~`h{D8^xt zRa$IiB(afb8k`jt5~1G_yV;rqyN^J9WZM2!X<~f2Q`<8+ErFXYWrxmx3iui64+H`o z1rB=`*SU$=diIey8|P#83!;_Aw{-ImvgWY)zBn<@F)*av3e}ZAxv9$dC8}>2I~c{i zC+y^ou+UBVwXj%wm#!yky{9i;_uv4{OQF@K^xjc(k&egy$)VxRX9Z=t$i0lNuM+;{E-I8RaxhKYG@#!F-jmHtp5!Ql*9#;#Q&keHP)8x*7%c-9+Nn}pd_jd zmB+egL7^FM@u*ntMU zia;b=kzgn|ie}}2;zab(7OgJ_oH@Da5kND;NHJxXU64RD2jtk7uD^Gm#N>p(q`d`7++4bP5hHf8K4p_@~ zveJ7Q>V!m|v(0ERhn)1vRW@=0^%KmyfZ@y;xc+H-!VRV9JR}N-AmiXJx^|aKKWGCS zrG^}V*i^4IN~07`9U~i)Az~CSpEWW3bfm8>BS&9Ab0_g;tO>{J)>jZ{!Ry5+UxOCNeO&AU3xRCqdMwo)Q=s& z!MAM&ZaS`z#Y_!!-#s8!f%eJ=;%j`_D|rFB-H?Jis|D@W3-j0ep9KPb?lKuXsis;@ma=RtM(LRhtftH<9h?Fm#g54wju<`Ho-nq? z-8E{#T<^WmIM96r@ z74j_dY!=b~sdpW!68T&LhV5+!Za^ASKFj;pIsFq|0mKy}H4_cQn3P;$rTPE>4l8KhJ*uypE#-i; zj%;{r$(S_g&={~L&QEj2cHFJx=u+L0!fzT?wHn6}pCY*j-ih;|zs+y>2D3~rMu5VX z&GK|)ub5oOINgaRkKY|QIunA`--N)t1<|K^oMm*ID>bK2F0s?xRIjuwxF9Q%yN=*B za(aSKm8s{9m4o5bBzH)xEEVR!Z(*nFf@A~=wKX^?sl?*{X_yweM>y>KOY1VvkQ5X& zR(xGZm04`E@|!o@ZQFkzcZw;JutjxRX)i>gu0hC?w$g*3jt5-GrHzF_DR&Fd&=^g@ zMQFV=j8Tt;Vola98#gJrlek0ljSC7zME1Ex1&`Q~uI3Aptgj6{% zu9DV09$t%Og13sh!JWxw2wmM1V!epJq6K3&4-H?!dMGjL^(67#s+6~U%G}0^xZ_P} zpld15iltg_i?x$kEe$;IY#@V$K>8JB8F`DC&l#YkNRO$~Q)si4PJ@2PdC6{A@fr!6 zsrhX{_qV`@kj8;j%j9uOH7Mx!4iYa70=c~EvwjRKs3>4hk{g$xmtfh>Zj?ar7CW5BV55zs;CJF~C$`-wTrnsxX zcI4T|OCrqO$}=8`D+A$GDG(bTWbe}!+v`_drn!}KCyPv*v+f8 z?OW$?kNdG}BR=)}Z+_lYm+5acKA3kt(mVyE9pZnlR^l%^|Preb&FrQh>CU+L{$lx>6s zr)@AK=(GnOD_Se(_Ix1~T^x4kG|cK;0YCP?c^IksiJpmF?9#by%o%Aerec4o!oHBN zE^8ug_&t@NyU$v}&Sa*@XDSpUNZBUvp?M=vRJDk+2{%fOF9DJdn4B0(!TJ5DPh zLr+B~DUx&#%a1ZHPJQq{y)u6BJAAefKtPWFyfTFU+fNM~jm#~aO^lpf98La1?*18O zihd)0&(;BK_-LJXz~DA)Nr-lI+g56v-8ipZTnH-13kM|Tk%Qn^CH%fJ+5qrG#XUEF zJCMO~=Ay^ujJ!g#zgXe9*?sHJ(&xqYZa!0lRQBPtv-SKtj5MA9fCu!ozap+e+5PQC zs>NdRHUtR^)G`f-8$d2+j;|}-G_I5dN=%^g{*?Lrj|$8z%Ty-hQ}{nYt!UM=k|Dg$+^M;&3E zak^V^;=HrlLPcR!f^xPQH`n8ZMI@*K;0f(>E4$Ln_6+M1*P{NEbc-Y$O#NP%^P*YQ z9jYueP+7@`Td1!vUsqQt2N^0dRz;OeqTv7#Q~zWBO$*yW0+)&%NmF4TQ4FMIWa7D@I^(bnKHKyZnA9q z@ac~dAyisvvkXJ?RST}BWBww=`D97a7k1f{7?g}c6%8V721+{_4FT=Y(ycl_=?#9A z>3YUB`dI#pcOX$Z|Rg zvq1x#po|VmRt&$J_UNqynP@w%>2SR8n7a^l1AZwWikoL8Z8*M?+{}gjOoh@nu*j7m zv?>afYP?!?u8M=?37MKwEjB9-&*pJc0^-mqJFZ(UgNBiU7KZ~z+t*~x)UbV_P_i4gZIRa=Q{Mf>;6-@5Zd_W{D~h2cA_eGWU~rB#AV|OWQw|4 zvDB7cn^Qc$8CL2~MK@{`pNK5#56I=&d0g>)H*YW|dcF8#!b@c?E7p8NI3M%(|DBcV z%8zt;tEW>B8RCXf0o6NxzcILrj#x2C3a{hh0*l&Zg8Q&z?0aC4w#>Xn2Tc-DNPUa+ z*UWa{lh_gDvq#T4`8GKq1+iH#6IWi^lbV#sT#-sn`>)%+kHiT@d6bX%gB9d=2OAREjDqZ&kzw!#| zf(v?_roZ^o$|(%ayx4^Hn{ke%-6P57eM6xa)-DGP$QVnRirgG`~ zE5n^#8d>;=;b+({!YbE~Ooxfa9&C~#F#NMsZ$0v^Beel?S{deC+iG5=r{eSXcw@Ko zlaIpjsDBUTv0~#bzHg=u23V1rKYG=mP@v)H{aLRdR_7`4VSfWh z-tWMdHVG8qdSo_|ZmF46RrGUM*E=g&E&71c$gWz+1FEn5QMflf0pzsk;83g(IKn(2 z&K@c!=Br2oG{V&Ilon@D&e#1xh*iLUYbDRU!kc0CaKg7K4M7KNhknFA(Q(5)oXxr zrn!8!%j^VWBX}Ux*b)vL31G>e_$Oo2tUk#AZQL_|`I%jkocF}k_9Vg4_rzzk0&AWe z0Q7g$P#s8NNJ#xrZT?pi23QLOWm1f$pO>Z`yOtA6ewgM9oxy+1u35toi3fj&qU*<(D*=yFPUHcDza- z7LNdnc^C~9`w-eDJpjTe@3(~+f|c3>N>TArbVL-poWZrFqVHN)5H6S2hvo|5KC6hKJgNz?zi;-JND z+3&(@i?G~be>);ePg~CIiU7w5`{O}E!|4Hr7*(---{aov+i^DiG!O@8-O2zz^qxBZ zj!O#<<34`kD$>#R#B4BSL_m1pQ!@&|p%c|NP6O>We5-yT)lc>#KHy{J99&3%UqT$r{ znYA*`T=Vw~2+ly9P7s_P#Khb0TUWk8&aqePN}11SVUh*~`qfKP$?QF-@nBu7BSypx zBoqqN_b|-;RK#MMQDjK|52M7`sDQ{BJiG>tJ4a~%kCB~yut@kbA0>aYZ*j0O@+aI6 zpTN7eFn@pQv4&|2=5 z?i!HK4H&(X*&jdN?r!tP{YwpFVs*8m^r6Rs2090$AvzZEk^|{;KB{A3jnpo<{mj-m zm+b|5)0UoGHc&!81HN#$W_ISk;DaL4EJSZ8V361WZA|N03DwkBOg;8c1)nR?zB2sM zi3G`L%|P{<^T~eNJco@_X*yP*ZjUMbf~;vm{isEqf zhbpqM;b-$GEGv6d8&~}51!3xd{NX_%jLe52dLWM!kQAqvC*5gvocCv!ax+4+jyaZ% z-QM5ph{{M|#JX=P!?=zY;#t*nhe>x*C_ZqI1TV7N3oeN-=zCddusDwV8B^QFO#7(S zB=X$5gPNMDA*(A3hm=>a&J16f3+PX=#H11YFOXta7zt~KCB~-CGX^32zMwsR3J6}T-2gv4poOcgBT6AgR zkSV-TSaXkg;rJHTZ}LUYKg42o6m))_m5%nP1`tCY@&H&PT=vfw4Mf&n+D+KYu3mxR zxC%mqJ{8G;P_0Dw`scS*To6LyLGddC!QkPxXVvPnbRnG6W){?M==d9q0;HquGUs&il3s z_$76aer}@1a``|m2`s@I(jt(=QvvbkL+zBd|H zS$#ke;4%>`&6Oav6hHG+#WZ>Rwn$G(>Tkx5MMHK&R~4;PBr@An}O(;Ka-nY?nWT zN52W`c2v7!L>w&`>%g0`45LA}#n6CFccocimkasSw3RTU9*`faYk-N3=~@9OUc zY@9N08a8^=LfK)w;~!kh1C4{H9vNK7crNCS6r>oDvuoSHWqx7BTgs_8 zHB_e^D{4kb%Inc>qFF#LKoAo7Q9Xh}LV}7q++KLTGgwat(j51VVSi|SHp&UQu2iM* z9-lrQ1odzSJ__FQ;E9SL?V#WT=1nJ|NpB%Ey^Nn_Prm~0ev3v4OMRil-J{*Cy_hq9 z`^jkFIMZQ`&#RE68d-IJXsHk=&sKH!4vkh{YV8StX0X7X0B(cyVS(OWRipQ*xu7;m?!f3^q@zezmZ>}64hEO?4#xXZ8lb2Z>Iz15?}bTVQkFW} zINNt+ZBZ4d`9iQE#QS*`&y=N}S0t4ip6)9koR9V{W4Jb+An`$ry1dZnZ~?6OTPidn zg$W{bu4efkkWj|v_{-S1 znWNDSquC4`DkL&fp~SON0%H624KUQfU)1&Mt8hKCiyk=jtK1r)Aq-m%;yC-kcXf|e zOkrWo+Zg3-JtlM?sAGWF>DN>?OS6goK3gD@xwB5{QW7>mXj!D}OWcVKi)rIJrr<|8a)p}OJpYM#iR^i?e~0~?v{)pWa? zAqBx`YT|#831P5QcI?L+nYSCZz*O5Hofn6zr8`kgMcks&%S{IJ1{ypSXy%Pz@0n=; z#Md>fP6A-JKx{5XtxXEV)`d<_=SM@u%ewg6 z`}q$VY$OX|1Ua@(*MHgsR?li0B5b)0c!}WheU6SsDn!NVo%c)^-gLFEf3mMfyFzz% zz%H!WEAC&SVSwUO`e1yNUvA1i_#WLg%s#~5d`Ct zcExzj{nHkDZEraA%Pn^{%(Y^CsH%(-`>4tR4Eubct>7pyE(gg%S7`e0$MVPD|f3(>&=G7 zM<|iGrEVONNulE{nl3%OZZDf(B{v~h1}{1x2f_QSPr935CY{zHR!VNSbzI3+`u5c* z#Hs<4RB~N8CCG1H-G-tuy{Um^^zZgxF&Yb0XvUHe{rFTVrt+Q;hLM_Hkp*D-maJ7h ziR#vf7I?iD3l(OJb1m5Af-o@H?U)RkXz~6SI=+6YT1C^O4K1w2)X5QgqpF8M- zj!9vzE#Y+ewx&sBn)UR!XqmySK7D!-0-)XvLgskF4KYtpK>}P@MyP#?Mt@_eU8xhM z<6thay=IyerxBoC_m6Ls$@=J*&<5OfBeba&a0QnD0?_D9MlMEAO=Cs)p|iD zx7H~+SG-1;w(D**=aXt4Mb`Q*?3IFmXlZdR2~{r~eQr3;iSD2KP-wHUc7@saVWY)C zxswlefu<>&0z>o>b>2QqI@3Cb`eh8cBO;R1sP3GLldsgf8%0}s@p`UklxMv}R@kFW z)%&JJkMiuwLqVQ9^?Mk8+SS$_exP$R9hw5Jp6NM{lS56EF24k~sIyy}H)nS!VRR8cISF`!k8CPhb#U^vAxC zw-slJ$5G8huj!M`s^)PKAto8qU*-8t&dLs1%eEWcYk+-MtDUpavcJvi&&=_~jc#nV zvr&F4VcT%kw5L;3n^sromd;%G2<59SL%x*D_zsxqjd7oobSeEG|7rWEK-+-4$i-Ql zfV6=x4FL74{0~0F?BnsBn_V?0-x=X=IXw@$7FqFU&^l6`+qQK0h;L=-I3Ul{nuimo zqvwtigdKA`2XlHFPwNIFx~rqz1$mCvBS3`(IMz2oXU6gL{Bpm8RLu=TcokQWL(A4m zkXOd5$Cz)aTD6CD6x-R#5!267(B^IM@?ch%S?lkyNaZFMm2eu0Ipz_YHpM5dE2D!j z+&U+1>t)p?1(E(QP=dwIo)g=!o48_~a;!$4C_7E73e8R&S)Q($)9u`a5T3cd&~dPk z2@SCC@;U3QEfmkq(cS1zLgB@p62(`NY}lXFtG7PlQd3>%_z91*yYNqGN`RKO&W&61z z;y(Fsqy4T=GOZK1%9$o0;5)~Ed zcn(cLpc8sp3=fitGoc4mTVg{^`5>!tU$35B{w!RIUCa>Ctpe9fwxcyDwvi1!1fj;g zCK*H;D)5IXw@Ng-U{bd^^Ba({)?FoCu_FW32k&j>8}^h^{^T z?|T@e)THUSmth`*E4|(mZM#wFIcd)t7tZlXZFj41F_ri{!RsvqzA^qm=MeP_k%!-< z2{M9>2QyL3H<{o>av0mrhK!e7wf%|ca00cEg+WlyM$LDu-#pLa5Z@4nWq0`EN!uG= zPV7OR$xwW%#OgS8r-*At#9(&uq{?n2U|}Qa-5M8>-N;F7SOj0J^2y!9ax~6$^d7|3 zOVKB>EHzl&Epcu^BpFZU)(!JcU}Jb=l8j2acCQ8*!Vkh|5DWj+52NDM;2KgP>p~^U zcIQ$a&UwJNZIy}1v5-MBjnPsrzGUGWuZU0f=!$B(0LU0oeK2d7T}cXAtk0DPdd~hkV3bZ)I}_BeirwvSnrYjxPh|Qj{s` zu{#s5xrD3Cyue#1R4a}ZRUD;F4S`eJ#IP3z4t9vFs~eBfB%K3w<>2nN7JU?ApwT&U zVz;l$E$+SE4M*E!M2n*B<`VAWG*g@D&cx83eH9OxMa3S934}Ll+>h#eE1|-@2dY4B zRy76SL)4A>!~L?9XFLpYVS1K-qRfb$m@ktm1dl5;_ggJD9C9mRj6OLKJCI6na9v(4 z^IGs2olw+g*qGE>1n;xeiuSUQ+_Cq&rh+lb0zxe)wsj;>FYOzTs~&HQ&o`*@eS0%n zY;L6Nc;6WuF$u$TT3CKn`D3+ht4!M0ha#tseW#q4wk>~*G)7;~=1Sk1yC@N{>E4w+ z4Sp`#S>q%h#Dl8US7?gWttus{Q!ky@bVD$y_ewYC&GX4!)++Ph9bbo3yW4&BI=*>v zrz;xsSnR}(5HX3lKY`XdMH+bz#mVU%YEbdCZVlR7M>%u%@(8`~vEtelcF<#;;<<5f zUok;g1V7QO&QoA!A00w_JpQsalWhbwJSkt~(C64wNTdZLWfxY%AhNms_# z!V$6Kx$*lSYW4{2eo7bwAfTInGu8i9&2DD@uTOlewd1taj^wwd7tlZ;y~%9UwZv;) zY2_n9@8m+?^UBOKcg&5g9!$!LI*wROV)8!bhCu+Po_F^rq>*SC@r?VB08Z=3sBnUyrj8gzh16!jnW+WV({3vAPvfZ3WG23`>t$ zp*o*vrtuvSMYb?XLV+R%POt~yV&T+iv?Mpphw4mK;9mbl{&%5sV<)@QRQ-9}+!7dm z2E5L+NxadTi9G8(OS^u3iKSbX)=6KqP#g@+*1Mm}^?9_=yDnpqZ4%7;DK>>vY$@=j z+OUL_>_a{1yd;*!n;#EO0kj%{{0O-;8btPh>5WbOQXnn^^YR$23@eTHiJTHEKOO3y zpY!MMy=Rwva$YJ@HTZ%73whqw;v$V|aSGaUL65MxfeHO_#S60bHOqb??HdnDL(&i; zi3I_LcX~u}%4e=BKb}aOohPlj0kOCUx<5$qB@%O%=|On3@HwebXh(@a}hfewc?X*jB?*2$2@7T zC~*>hq>k<16gJq-eq}6_1Y*=AyYY;okMAqjbXHz!O)*GK1eC!vVg@8XOQQ8`2?^j$ zt#_%!BSQQ)Se4NAk{_(}o%UF@R~Cr}>i6vKs4ZR>w#{8-DHvagZ;__Vif{}(Z-$!z zjU`<#i{E0kIXhpEcgLk?4c&gJR7c?T)+pvsTd4J{0rvA^d=~4TkPy4f(jcgtn6+rU zWJnYDCc}+z^3Z(~BGTL)zM2h*NamvZipzjTO^JHo6V+l=It+|;@Ew`JZ-1$O#jxk# zAV9Tll*Odfzn`#UJktR8S)eqF7h|k*?d^JPeTD7wF)7geVY(3*mworJ9}$NAd z4mW@JP&Rb096s)1Z1u2Aa5a?DwOQu?@#EX_F{uAR9H<+l9{Tvn7X)tS-`dbQyMTb3 zDTblM5wWpLCI6!p0FCfbYrv{#R~>kIiCRNPfM? zQ;$AS;W!1la`@`waXzpXABg9it3-cjWg*oNWow?jeDD%e_Tbx>q4Aq%-^%o$?gE4N z>QLHDbj-v?vg!VIrf-;!S83)m)b4Y5YLf?tT9k&iPq8 zHm&|?ds<-{G%55q8l138?z{B}JIIq3pEBOt8VXFy$sUFkNJAF)jlI^~5hC$|j|?3_ zv*8qqM)K*|6U~Bx^-kA9=I&k7<>qQON4uQbpcO@r7vt!cO}K*a;RRJ1B^0~7i>;m>Yjy82!||LO zaj8?r5Y#W7%RiB;ih}Y$RdWrdBC8%SOShOtN$F~Qx*QQwVUiQ6WZsoR)K?FRs<@chQK;khi@u>A5K)fCFiw^J&SyPGo0R$a%WKv+2oGBWfaG{Sr>bY)%f@*#dkb|KnXqZ z=Z|ga@HCooxH&rrOg=vWdXsoCt45WAWzo1*?kWIy?8N$C}}UZe?{ zyPQ7BAdVu@uQjOJf(PNcaXu~z@+BfANLDOpW7Y06!pGHU+1fvhX3~k;O3+kU#+_eg zxhA+u$!7;wR}2mGmb!)zWzi-Y)hvM&iEeMA0hw?~O4Ir=;Y#(ksBhdDKecH9!351z z)?;_WGuW+M+G>GIcXZgxHz2QKmE1CD(oi&PYLynp{JP2YNc1Zz7T(hoMG+EW`gr7=C=>rPMSi*q$ zG&LtTee)mC@Buxh&8WW{h+my1s$k4_Y$i-Vd?eTHGh^mzx$%_=Oz%(DV&1kh<(PuN z)0Q5ZnsV`cQp}c|T;I!>+&w(fq}Wv`aEoo0epaZ8 zmJGp7SiEUoaIQB{+Cf=&7HeSsM1!lWT$FYcZZZ^!b5^o%#ctummX%&Jg=G8H*sA($ zfDGjG%}#5*B!d^t27(nI9Fe=g&Rdzp5V$(sFTVQ?I7gG#z zK82=;A}ouDDJXO_h*Q_4(aH)Z)`iE02y*TCDPvocy{Lpeb$!lzbMT^ivtMOA=#MvB z0t&`uG@S(UJpuR&LPBO^R6pmUG~{QpiTlhCyG1mQb=-5%=AF z^34OM2M4@w5o}8++w-PYfWtN?LWnZtQCD4@@7iJ3GYVzGxG9nY!9C=QFuVbf43h)3 z#@Gx&F((f;+v&-bLJrmuQ+-+kC|t{h@sG z;I7W=YIO2uS&49!KDL{dwOhz8)$|7mgOu{5FG`S{glicz-qKa%4n<@8lzOu=>*@ou zoHLnEI-_39a8hFd4|HXEP+1pGmv1<2*MSk@ICT9kmF#*!R|v4Rq(;4Wl_Lcnsc-LY znZFfIc&G17fxJ2v!lkVnj&Ju$5vr6s@ag^JU@ zITjv2E$Y{afp4Ib?GiTE!cZa(iWpw)c_$`gb})eCVRCjRD|%gLA?zHR1q-4q z`)u2`ZQHhO)U$2dwr$(CZPc?l)iEEYyJI@~H{8g(BTufg*NeIFEs*JUtf1O)8OHv! zHwjOc@A~eiSmB&nTcEtN~YZGij{*E3n}i}-O*nM z&y;RxQS{K7(&Lx%{t)b6xjUuMZKP#f*%mw1h>c))zO=qCwj}P1lTmqo+VQa zo&EevnnzFD%xEsZW%>-D-g8W}+4#?HlHJr_T_8ncFOX$Dt3GPM^45T{zw~>=l_TH@ zen@s1;D92!W_jmoT5SdCdlkWQy0DHx1b6+1oV$n_bqGbI<&eIR&-Og2*^DLzD1m`_ ze6IH@XW^yh?6No%`yWLgYzpg(y^QCp`%fw_z+h<7Iz5XxJ026RPOj|N4^R&~ip9&n ztSxE3zo(S1goZ+*Mc64AyXQ^mE;*`SC@qwC&dHh9`nWZ_Hk;Osx>~tmg64~ELr+LY<^l8IsOM` z#cLh6#gVx8ss0BU$Hr|r*A<*|`)MF5%LH#?hYa1OaUBdsIG(n%##Z~`a$zS|(0A-R z|I=7q)jPbHLfh2~tjUX!A*odYE$Z2F>sd zp9reJWS!=v@!}jt?sx!m!#AmW8>DGy)=HRl6ZkfjB8ujdL z@({bYNF`S9=m!hh+*CJ!h_fNl)XP&At;fS_8K0`vEZS(Z3o+hJo3+QZH;X2_TfQDu z-E(PI)Np>Y2mJ^;RkA9=ANe|4xj+P<)vZ5hsFDk-3H)}2EO13DQnlW>L4 z5QD%9=xIX>oU;3_5qKxb^qIQhdO(Kf+BSupx7Oo=u!2dgc#uWS<*yy*m^|S?OZm7DqWG6Fml5vFJ03Z)vCXQVy?*6 zuh2Y`ru5rHA&M>%M_Z;0m>&wjFt@}>;SA;HN_z${3NOisr!m981u@!U9k?dI5zydy zpsbo*bwYEvUO%%Y$zlWqaY80v)_enK1@}07E=e@77px$DDSld{QECOZ{yseh2B|>p z$k9-d(of*{ANFov`8j4vr1r}^*Hl<1AV*gQ=*+)%80K?kkV9HSG*Bpg&NAN}WxX47 z+_?eG#*_};M5qU15jkG}=!AA<#yAkg__4EHu5^btWzmB1i%|ph=9S=tXMS~Gt_E3Lh*N9vE}oS91cSPH&5WrF zedqwNbsdFl&Gsytjd6#e4I7Cvm<^##Vq2DNgFxX_fp@O>Zx$PBPcy2EfUxs&u0_i$ z+%J}vmd2V)D2{75s+WmtE7Udvv7{>(fOBdC=mmsV3<_)F4-~kPn8jwKWX66~ zF!65m1_#VI?;eelvJYgnr5nYAqDFv#4K0~Uw22_ZfYyUjt=4=Ai`A+>4$KD67-%7( zw>ILJ4~gPHPGPI-K$0PpWE#v+teDEa98peXlF; zUTh%<-JFOgcIg;4z{nH4%%&O?9T;unIEF>(+3>78Bn zhpZx(z~S#i2Atw%-B|B;Kd#vI;jnG9o;VqH;ffjNfgV~}Ruq5YlkcdsrxgeT!=2^J zIXOtds@Nlz+Hybe*(&W``nq$`;nEqWf4(GnO&uDEp20Q36dOl{!7AB<^$$vlBY2;2GB#{Rzk@SmQ;Yd$U2?sZ80#EVUD zF_flUh?mg*nCvma1rxAD_nF;+U5+~c?Lu+DUpn{MB~BS_GowA3pmaPIle{!Ct6n}E zyb=QF>U~meaZA)1HEk4P1}BOAZYkxjv`{f?y2Y^c;K(G<8U02MD&lqBJN6{HozAW(Ny>|#Q=|6ZR#}p6b*#Vm)V;v?AGhnX14marmWaYS&9DaJn`kOWdp1tzm*~2m=@ZTX*Np2LABW?Ex zd?j4)*Lo^vc}j_`oXNK86;;PomzDp6EXZjIlpJIhzHk$p`H)f33tDafn>&yV=h;V^q?9z?&I|1$i?D0PhViY+Wvhj9 zs%U}Aej_#jTH~P_xXgAJcQxkvrr8O)uwfhO5NT0*>l&`cDMX_QNGAjg@_}lksltR- zjL+*-)H3kmlKKxpU2qseG+-fNYm3mqIXQC0TtE3enTnFLo;wMV;q)^&H)DtwgFFL8+pWsBuSO3UsNlyMX7E=QDPFN60Taf zm8)yk&952HeJpFLK%=zwS&wxY}>;#y>)1a;6*2V+s-d&0KU2^_$4 z%~{Z9x%-SXyYg4B{7PWOo%99t|Is z$fKvE@x`^hP)PpH*b~Z@E+es+r?6Hhdu1X3hNc|ti;Xmytt9F^SrbDcE^K9#TV5Lo zu`137o&&isNk=CYS#!r&hEa$n@kB&X_nZa`tTT}#uaombG=ub#ZYYQ19tg>PFEkEs zMnI`%p8Qw#Tpo2wg86e3>MmOCcx)9K(Z1Zfl$rmk>>;k(#nN+ zzt~Z{rTCKfBCQ7H)Y05WGh{v`w*O;K$=|>PD_W2MUA@B`f52ie%tL{cXFvv zTNXBDn$Mu?DV3){;!~}ursVCn_S+T>`vqd-oq}5S({8UH@E%R0&q6(VeN~3&BR5ri z`GCwbS6)+}BNR*(&{4~)y#qVyg@cR)!HATa8YqZkc}=}ez~usFf>0?xPe?Ph>=n_S=Wl@*!BHRZX6GV4kQI%*nRi08Ugs5#5w+s@lM*0tv8JG*yvu+=D)(nN)rsJ6ZMxpju_Elvyo9(c{F@{z;Ve)KWr_X?+HibM7VZ^pr|g(V^96h=UlNgBU=)i1ZE^ zgqlu>0YnBx%zawR`gifHs9YC~C8jhQ6$dhGX6eN_Us zm$dnEPH?yJuc9?Uhv!6+#?iAV+`Jh`1^P$3d31We zZ%buSYrI=U4frKtLhz+TSdNNB*$g$M;5_*nYqjsdgyJ66+XdwSjky~qZGd`u zyf3FcT7OzkG>^}CqI`aL#1nXk1_JDTDDe*S0;p}R6lfi|&TUiL0}y5XQ0srqfl6H^ zChs48v&G@}?QN~A*y;?zyWAb7Vc7)wg(0Z9gwpAYNNfuU@!Wy~8Ula;@e7zGDgR*f zbq>q32Q#hPaL%7+Tm;F`l1#38ma`>l!Q!$ReYS@Io<`m>y(cHDYm>{oS11|vdETtf zsEwd(s})`(1$p+_q+-iWI|^67G8CZ>XAXjONNMA|RzUl5RlxYlm9WjUn>J-+T`Hl| zhd%qZ>O=d}nc-Yz2FT_r4jQAK9L;%**iIu#g=d*%n=d(SwB!+)x@dtP7yX`hcm zxR-*n5c#mm=Z~@Hh^3bpPU8DBdErQ8HH$ekQ}%Ji`Ei^bw1t;il{4RKEyLe2{=)2z zY`SyjXUo1*_N>@nRDWIWslNUy@~+%c+C5ri)EM-Ya&yh2=Ip}mJS3kfR$X6=Tq6j9 z3PDG<*OruuASFT79^RCN!!G2<@&_}>T4_trMqH~kL$6oTg;iqxEj+nh z7mlS)8;KPWa8a@MG}M%JCp6889c(ZdqARI(^I~EGDatERk%1^}-nXPIFnQ(CH&v7w ziK2lKtTXKDfD_h@BJ>tWILHX2tEAaK@P=%fbARvmfXa@2CuUQIMcjo7hq{-8*Tm^P zSasw*8+#*zb-3U_5Us=Ln_XrN{Gq^rSs%gA0hUFA`RAIqAySCJS1P<^LTQ$l7TA#v7bvPjZH=TN zqJ25^A#b(o)*dI)sm@ewj6*u##ZKcQrsq%)w|3s7wgn(!5&N#VY5?rFp}v|srvq8% zb}%f(NY~bIz%?KvqLa^syTuZ08ebkj(I^#Br1A>&Qw{yCiG*&`F02b%Tr_xuVaEFQfBSf>Z%L_j{1AUXJh2=Nv`Qd! zUXOBftbmTo2`X2zygT}7pP6d`+vh@02(#P+SWTjX&>nphAtNe-8^ANz=yE3G_q8U{ zR6W|J&g!&OppfyP-m_6a(AEisCWR{}bRN`{l4n&^YVh}RP{9zW^RfzP(!2rx`mt!F z*-yqj?K(jK+f&`86pRrcIeXfmd-up5PrqhXEC!oFiNJM=8?Q*8(N%3F-Fl1v9D3+z z7S<2QH6A40)#h-2oo4nl*wbUhY+1kDe8$C$$+6yZyF&X`#8(0^{>=%eNkU4yghOis z6SOn#b=`8tZCSBUjkqihtlVqOva1H7^$cEkaUJ7Z-{IcUU71Mb;A_a#k^*z_+T!jC z#eos_V-QI)y|xBzEQrKvE9|?z4$daD4QchPs)s%XoCq9h1YL$arV*L?IF8wHiXWmv zC7*cdQLz<}UGnmVE8OVF2uFbMp<>qj@Iw(wWEs^%{@OU#hZhmuJ6PtHeSYC)y8B6VNV(9(W_ zIy}*E@-AAXkt9BW%Y2Q+l@U(gZ2HVAz-IsC%<}OHMw6{?=ZFnLiCiknpqIv=oNZ}< zRL-20#TQh$2NE@5;f%IG1#&Rp=Eo@2jI2;DO9l1jfDz$cHOKSAXqswG+{`3`64vk! z_l&L|ZwEBJ^;vVar@;`=aHYGd$s!$FELNm_6gssX(nlCh(<5*#h6Vj$oQP7wff7Gt zNeq1e=p2!VXt3>)&y2qSpQg48^E-+0Vhi)c-1g%=~NT^=&`Emm=Q6iyw%G@ zkFyp@tIcFxNJ7meIanlA{t>Sb~3)y>gV!saT#vLye{K_$7Oel0Wm4U^>BC0eiHxvgWcFiF35>L-FL9F& zcbV3I(CLC7wjqtq1#|IK?VZs3g&R%hPGr5!Rpa9s(6)UovG!(E_nyhW7h`v!w? z+;$=AQBj!H4tHPM>pqpGM5^;t!P(%!{F&u%SR(N78LJKR)s84FsS++;E)`vX1 zq1%eR?(|_hX<%9&m5*_rZnz9K{nD6>F-lAshB(RHXZI|f9m$wLZ2-&LrJ3pqiW!n4 zO7>~m>q=URByk}5igjBme^mkv7WgSWuE_$;v;lIDNyQxAdTih~*vmvpKS46}>N%rd zv;Fz#&@ojUJhPmc>~9cBlpxJz@6mF zUrYw<{1>Aq=aJBO(HWwp46AYZINm5G@i{PPCS26bHpn`P$xMAz5aD0vOoaE}ZHHC+ z1p{;&{PTdIO~5Z+q=6e6C@i9-gTLMEOyx0?<$GNh2UUK~TdOu(FF>s=^y(~=Z?%ei zKY=Ip@Dv~qp6u7weTr*X^>(SP%U=NpF7nho@rgjt;Fx_g>@$lI(1ZdAJ&d zXDAmHy3*fK{JoQ1ITT@{-N@D!uY|F2@S{Bey_+t0{(fDT9n?4h6s`9ntr^tHak;&f zLm7vHT`}}7P2lgw9g7-+h|!8~`Ux3!#NBqR4R!cuE{5JlmKEf)IRkf zZlKRyN*kYcV3ccL-*Jz4FH@%mu(|Hto%p|cBTaPv)yIu_0whN%t;`p0sM%t zI%n$#iN@Y?M@n+58QR@pQQvd1AF*eC#mat}ERCEi{aP)ZFM4pRU>l6E({H=5_Tp3? z4I%n%%!F`)*ghK6WgEwWXSfI9Qj}6lds$0i0&_(}-_@kAAe4yA2aCh+a*O~~jXH#qg}crAF_+^JVsB1RH3dU?c{SyIV0 zt2LE(=T5qHnj0)a^Jw&)hewWac6Jc8ne1!RYh5;sL1y%sMs{B=r~Mj$fGb!Z@<#ymS2CnDc6i1yrR914q9DSeJdk}23X2#nT^Eh3taWj zqgAMa-2)cwi+@wq*N8$t{4AdAoh6F>kYa9r+XV;?2`8tnQpevGc}I1UKJ;3|5csAC zy`T}XG06GMDd>i=g}i#XbSc+!VJ-T6w3t0=?uPcJQSM5nLvR(R@76J; zD`_w5f0RbY-vhxUpDNimtbNs%Lzg-kW=Q?%8=$4C= ztq?5N?)rY8!+o8QWyUvMBK+A9P%1iAA;=rWKCmiwhnthr6zmyzV-EGV%!5k0cgQVI z=q4}q;FYKb!A>)R{fd4ov%_y;p2U%T?6`PqD`zo6W*m7A%G;uAxjF5So8Mx7@uBlC zsUnw4-_&kyumX4<+nb_`wMg)XwZ3Sc@WHDsokE4KG z;L@hTS=t5{^(jy6yb$9NQ|SAKu9}=1#5oe$oYVKAa5Gnk8>vba(rhD@>MfJ#RC4M> zn2I~{(OY8>lS7xtY+ zi>8>q=>dY~UD9+a$ffakz;WugU43aT^?X_~`eyA~_p}3KuR1GcAG63IF~~9Ubg`@d z!`h(5khm)83-+JZfOLDAUw92RYIoncqlQj?^B=D1jbFh33L7MZ4(M+FtCnTN{~urj zM>E5JutANczWo*llHXj}J|~BWBksyefkwYSzEbhUl%AG}SKEN8a!>CS`38gP zRL>_jd^MjZJ^P2`LgLe``7k!k00T*E#vQmpkO73|F zq0<9F3TdD`?iOz@KIXX&A5&vl;}D(1W=mJO`6SYJzBaQruejRAhHg&vXf>N7&yItS7Qb&PWxH=x$#uR1&w7s zV<0ow@aU7_P0YX0>rC5A8q54DUcPQ5X;~6c?nZhVxn1lzBgeV%%EDuYE`JwBb9Wmo z?_FX++`WYycOsbx`{CM1S6e-0oy5E&`sHECb6R*<_8>CZWsv4U%?NN_ctraEUf}|Z zlG~^2HK#mLD~?UP*SOac&H$HmI`6RY9CbY;sDB1mV@n_qw7BZp%> z6R>LQE$B3@I{|LfH24xkin!~UnTh=50WXvrU}D z-EU5V_kISl4jrvtDUYSzYZI^txlBi$R))6HZn=3oVUDx_M7YX%_K zrkk)47zOc8>uXe%b!YF%n-*#<7kAm-#%0H6<3RY%oCuRK5gclyf~CgBj`Z1bn73?E zWYP6iKkOAqWUo5f65T6NUECb&OxObZ!5N0!tsg zTC|z5{VN)90<99^)4hSE^yq1*2dgdcN75Cvosd`|l_{iRajG0qnoY9*+B2{Ox|)3u zL$I3#^Lv0@8_R7tvK?2>n>i==lppjUk3ieFMe32KdVAU|(jHIx$cxr_SsZTmXnqTL z&djyn+y*FP{dNi+SnzcJv$zZd$MGx};0JzMB|3w)hIQ1RPGBWkxHgi&IN_gN4wu>` z5>D971M_o0wTw<<6~30OZopCi@P3P%W@dVykd)7MBb066aWZ*X@pw6N8KwY2dcjdz z+O(+eB%G8~f&`#)I8^kx5O8DK&MivPbVgU*cLQ`yY=mB+{>sy!5(n^nO&7FGawn45!iCAIhyDd>a_bC zMSeT_6v%J1d?3E6y2O7(#z3HB_dKKE=#scF^Y5i%aG8Hsi{{@0fdztP$+EFysBWD&Bx-aHG96RAS@>m;2535bT-qGX?pmpxdCU)W)oN-+| zH^p$Ho{kn7w;O^PbVkzw{tE=_hKSyp^4&))^;?$Sp_4mYSI*r7gjKe41T2uLMXfU$ zQ#tB$MvEo$eJPWtsZFZF6?377^I7Ef`Dhkb;ufurKTkO*F0ApHm-Yq%g|y8LIEjKV zdS4)>i$%$nQJcr)`D@JJGkKIOMe*p{YJx)0f4Ma9vDpapC#9Un-`73*YhOUGS#n+t z=uZZ(6ov3SDv8bz{_(o16j(PJ~4hf1-aSQ~Uw+-`d!jxD{ODqaU(MiroE?p~2;CZ_!XJm_=5i zqh!Q1=`1nHtLG0I15{hTqriDKMJ6UEL8|K4q04mwz<9y})^aobn+@zav%#~NZF1T* zjZC@2$Z!(_q+yO;{LSpa^9jk$isJ}t;MR;4;MM2uO8{7S_yu-{7NIoQgJkLI)4?xJ z0kTftXFWywiV}k@zK4{!f8ea!Eb!0X@8?L#IbuiyP(b#OnJ$Zq=?L1yxA@h5O9viN z<~*>9W&ALylp#uc9GG`@yWBV59@j+|c->hKhsTviBbOtWbiCOfWUjJi$O35Q9>Q#p zi-T*US+RrkPnp>I|L)cR-aO`r)lN7p-Q2N2ay6?uA6xAUB(dXOKS;bzD8V0Mn_ zb>@Yag)uRnZ>A$0K4w&mH_y*z7NiSogj=eS_Vx9Byoo!5SKr26tld6}=Veaw2O?RR zWCz$sW!qvpYx6z5G=3z?d`9NyRXauel|XqNW?SiFG-3%JGJ`&Q)H8dA98jPrRL!*( zxP(|8{SL(yO9Joo^(?LI+F;Bc6%%&Bg1sHJtzW!226eh}e=8iWj2!h(Ru`Go7_(RV z_=M3hSs!<`6Y2TN$RDi~>Wc7L$y)~LK|&W%2lgBu--LtEmlLHOJ2M~igL zgn;q}qd&-j8Z+0ss!GL0?GHKA&n%vln}w(1ILshHtGEc19HG<5V}s*NGmeiEInL3L zW}~ORuKp;<_^}MSBKD3~g=TlfAJ27e|0Bi~U3;2X_)RZ^{P?R2(bDJ3>j3?NQx{s{ zvfUT;v(>o3BA1T?suBbc1Q8o(7nrS^J;Lt@sVncf-!(8rBSXH6?~m5qLhia=cm>S+ zVcxy=vGCKqRt?*B+PRcF?Xsdm;J?Jk} zB1lc+%BJ$LX!3RnLB%_8=l7nR+fnVJcp!Rf27U(L`+t*-!?JJU8U5>|J%j=Pp!@%I z(*9@H_&+Dnf3QOoL`DKfaX6R`c*u2YlU+13=HGg>>D&8s zp11J?`SJaEzWq}tH=cJqp9g6Rsr3Tu9QY=pFkp14oG&KxIc<)zHobnH)hjxIOGIPH zPdJ!bZEla%`_DSXN7zekH3+PQ?6LrG3HjpRoK+-xTbd|HCu$wvd?WbTD4~^xriD(g z-%$_oTO*B5=X>fQEttTMS593c1E*ftza@OZWvkUd60oaHm&gzYCW#Oh#)yHONh?GS zIWFh@l1xJk_A>koYY2f%!Zeoni6#-=sI=z`1$BoT-=x?u`#sN7jOpa)`9kc>IbkWu zw!yYVn4H{fo*axVAe&F709~#M1m7kN)|1rLqb2E?ny=k{d2-{5ds<&-enTLBgdqM% z8k@0rJ_D2k$_4H$hY+7yU}Uk$r=+QwAXTC;KY79gslZybC&WMU`vuM6qyOP4RCZlEZI7nlyTV2JS@W_8=Ey#n^aSC z>cE7XWtpwDcS;kF!XCI(IapzhF?*0MGS@z5AdhoT;ZS5{Wc*ddZ3O`$WLy!dCgwNM%ZvcTt}7XY_$7>x z2Qs->D)f4?Sqk@|=DfV|JCYVDD_Dj!s_6qSidGqI{VvRNsXM%*%S$|I^J{?^ZI|-0 zIRzPufKcnia3S1!T4j|d=t3i``qnz=Fh(d?eqDgeMrhiX9@iVHuv16{N1K45%%+O(i=N0g!el?5` zs3XOe`RY}PNnk1{k`A4@@6EgSd95=+ zoX=GRNm1lI%mx`K{-!Qy@-;i7n{ov&HJ8WFIE@-B1jDN}YQ?k|Z&f;?-F~(qL z)uRaT{1zglu57DmlMAfm7WFBPFf{bSJ+3>d%U?Gvue+KVk2Y(I)P+0eZXKpeeSbUY zR29lC{DWZw?7FUN07x{6r#e-O4!gQjgLFPgOvj}2G=tXLqPQujDf|n&p7ySMx5;4l*22IJFV-ApD))@QbK(bEh28W zQhRRk|4>K)fi?g;q&Z|98#EWJCf=G+px?)n_J|tJD4#Q2GPy!?L2tLehuC|& zysjGl<$&ZN-ruf6%#r)oZvRgW3_Mq<4tHjR5J=osS_{evL_PXWqIeE>>BKYATShFx z-QbE#8^vd`7ea66QWq1n!$y)M~ zEJCffx)`{(HkUlcyT{Tyh^sLYdhFi^vnM<_>QD-Q&lf(d8L$;Os1T)R7qQ_ z*Qut`>37_8vVX~St|(cjkgV@P`n-a)RH|Ce9%2-CZA4sIu19{C^VN1SRwtn%00QPj zjNv87F6Zz%^9W-l28ns+H<(Wrt|oY{5(C_9_iEi%2PDCIGt* z$3l`~?yxZt!IX0@^8|4kn!yyOY>LZ?{qfDhKaw~oL1brcjZ#1-?U2V?!m=?Y3ufNFPt#;%NzoNw~-%bs%qX3)Yz0$(~@12a2DyCgPp6QQ!e5glm z!SZ4-pD>`W23O5}Bnf9r0mfDZq(ADlB8juqLny?1mn0fCet3NdlEau70cVm~455hT z-jUu}L2YR4#$dsU`sY(g;-TT9aRMZ~auYtcSw zJJmc!Ad7+_l8S<*Eb^Thgu4~Q+lXlkvW{|_n1MWOf%W9THY+F@3HiB(YNdZ|0D=F=;3;IR`ji!( z@vdYFd;$vla>$Z|+L=Q3SR1QXfE8&!r8Zx@lq7t>BTHbmwWGnl>g~Rz@}^t2kovN= z1D3OLUA^BbOV?l=(cJbJRat}iZ4`+Oi_aV`+wi5M-g%ClzqLb&r}|zSed`yCn{&vO zI~L1UZVXZv3yzaS&5!uGFuoQ2v4T`TF6rHf<>&<{#d@#Ap8uiM26bT=Tv4ypvehN8 z$O%Ev=zf#XaVVh+R>DWNO3FH8D@O-T$V1EF4CS{R82d?7?C&)S~{8QJK&31z!K z785QsW2;5Z?|l(7Wh z<890!kU-tR>MC9jvPbv7`q)sIR95EcYj?3ke9AOicy_PDb>}eaahmJ*_^#S`ofI^= z5lvEh9Q4w$V4_0zBopW3l3+y`iuQ&}oRWEsqrZdv?2^m1ME6f2{v3x5Qr_tR@0C&l zmjb^?V;)O~R=md>8u77wq_bYYF&r??huR_Uw3c|ZCYh~TYRu;4FmbVY9>RF1MqfAU zH!S_sLFK6W_#bA*YSiTeZ7k?h z$B_4A=-=>pbS}KXUG3Ikb2V3~$=$j$!w;^jBiJC6M?f5^v=9UxYJ0jiZb1S;7|MkF z+xS(dHTqJTf5D3O5e1tEoNmSViY*Zhrmu*7K8~ou~h| z8h-E&`CBw3d6*Xb_H-!?_v}Bd<^4z4Y`Mq|>HjOW@g@B?E&t!b=Kl)W{_E{%L`yds zM_sK}`&ak-DkIh$hr852#Kk__;V{^SVb=tio}}8%92gn06~)4;v(n2-M|Y)H)z(Xk z^>9G23&M0{i0@`7IyheJFd)qbknrC|Oayzry8$UeVhfTGPhj&wwo_GAm6sWkxv)sw z)k}?b+s)kjd#{t-mMt4CB;Vd|6F&UUX(q{cRr7=5!3UMj1i#RMBpx(r*gzZ3r16O2 zXk}3>tGMwuCE$b9rs3&9>{N%G%)UTcA_GrDr>vp4)1L zUGUC5^DMIp`%u&ota*^6y|ms;6JI>fl)hgzN7f(xSFAsKcc|@sAg{7@ps%#QNZb6- zueNo-uehCkFt6y{tnKl6#mR zi}SfD_vN)8NuHDQrdoI1vpoFs4}7}=p*neCh2G-Ic$ZJ@4Wj*>nIPfeF!XR6)~&-I zvE=W69rI28*fmhyoz&UY4;&=d&Ka4=0VnSmJh+z;Oyf5 z3^g4(%W6P}0p4rcdq^6F|9ilKRV;|JaX&Bj>dk!*?r^;E8aUn+3u}k6b&{xaEja{} zoMLMEXJ?>fCzvSZWg{V*F%EYsthznvLQXD4W>SMJZvQ>OlO|@}7&Mi_p8JC#dgrOl z>z|h};$SN-G5yS%Ku{4h;IV%P)<-YM!;w|glBxqr8}rmAu}Pd9>2|Q_*Vn}jD|k0X zmtJDA1|j+7JVd;%XVdRF1^dUs*_bJOkpu1)^diRL`u!j#$8C4p!`#|2er@!I7oX+e zJ*yoTmwjC8j=gGchbqq&zBv-WN)^z%2LZtDEipLrUBOqMZyD}cg-?kIc5_7Od@l5VZr>IUZMxP+y;LwRToNS53LF=9+M2cV1_$D^r=g`qD zn?sgStM7c3jx5-V9^>uqUtLpo?y;C6;Y8FSX7)__n>WVey%PmGGAXIP3GS+C)8bshN@A8aBLeov-jLW+oO4CWx3k7gRp*@+~Kq!T$ z+)K;2l?0f;HpGO66Ky|rt%Btz$K5CoNy`8}u-qsyv$ELlw?S{SEGqpbWR(9Bi-SgL5 z`JH-qqo+?jp7YmtQJmG>INc0;MoZwx8=YPbrYf%W()H8UOfa zWdx!4OAH_>Tx9_Y01m-2W5{vm%RNB8KUf48cC`uIGi1T2Ds8;{n0E%~@#e4nKb|=6 z5r4~CvF&u*+;%`R0fTh7C3nW1I?TlluE3V`~Ha021uT{j9+i2x|~ znHTv6V?PSSavg}yHc8+LQQ}$B-^+%>o&N6g=;wRH0xZRu0C0kezvl)E0sEPx6@dLf zxzBpd--{e&9n(NsP_ygs_I6`|A$gfK@6*^hzXewci1-zSjS3>*r^5~nC zEY)QnB$vmoNs$1kR^n?$mds+UfWjR|Cf8K5{h~n4$2(ILi79*Nb1Er=LK^As#S08B zCIpyyg^O96Q7yB?nPTw>CsquYdNB32>E8y*#cJZtBit?Ymt{CkpKJ4*t#=04DLi7t zfcmhLEc8+W;#!VmwKM}dn+B#@|Dnuvl=p!t_bjwSj(x}*352N7Oqg+@83$`NM1Vco zCW!TWH>>quJGKFbxZ;~jVZb{f`)9xSv80m+ign&>0`%(mw2pGYt2_XDzb}gc)dBzka0!BKz7Va&3WbZ*SR@J9nsjRxA;n|E2xpjb7?cf(mPt=65(t z*e-e?Ww25LI>i5VtiVV>HL^>*EImq+kIRnqR;vbd?ZuX>-;_2D$cXl ztI=&+*6@=cXT#{VPj(AD@7XHj`L!jmW zNDXFRdcKkCHh1MWn>684C81X>9H+n=sZ;E0E`;P3;~qHv3WAv31z53T66qLdqRYl< zzJ!P?V0%m_`YAly^!9Hj|D>N=LStJ54;0=TqA;)&;c!L6O|GiyIs=l|`zKot!84j0 zpQllPB~QQM7q)SH(p&!Qd#$9%kqUy)%*`oPK@9P(4`fXyhm#!S({?_K8OS#jNWV_e zn_%579VFUgL?Vo@JQNx+3jDPOSwTvo6C~zT$PB`eVvYvQ5O_awy!EPe z+)y=e&JmE>`sIlD?dbxjnPCgLrB0{?;D+bny)i^9#%)}fD*Q#uoIvDYn0?fnOl_;5 zz$~C}A3CBCZcsmQ@`pGZ3k>+crN-E2(34K z7`>RNsz^WBPl-xCo4D$6H?21A)uF%?F$wOAA#1c)xr<#7x)*Ht`Q4IWRvtUlBwc2a z3AGLD>FX;RSPpb%nq3xGQ=ZIVu0KRJGun)=F8CKZt0*awhi$zPUl5DHAHI+$04nbi zgow}204}0i>@I>F6a1wH9&AsRj4&q%AQZqqRN$!78?@d+l78=pAf#2_IE(D0pCWuq zeu^H(?3@8WpQRsFJv7;QSg7WuRg* z=WD9Wt!R1IG0tIJ(S!6M(SV#*_-hu*AWdDk{TXg~h9N;AQEu!K)3Q9{a-UP|KLvmy zY@nz#8vtKkt1M-h042ECd=`>+y1DjWSyC)2QmhBC;tzSFa2;$=nfS3cddT){QpCs& zyKGmc$lYH06pJC&Kb@Ko$64G5S&+7~W&z3vLkQ(V8s8H|2SeI2R5KU8x}Nu=ZAWml z?a=2U&s{KOo*SWRL4axgk8~MHUE9nHM-XG*idlJ>(X18s4Igs zSoSPu8k3|8aX_8sYx5L0P|s5>ApLNo*y`Rc8rC4G7zOqysDD^#!k;M_Eg+}AH#j6G z>E%ZnlKXmM;OE2MYK;Pn`)(y4+YD{$!i*QYEJjcDS(a42z^&?YpoOm-e< zth`B>--&TGu^bU8N0SE#MEu)%Kt;ZG8ro^^c0&%}i-tLpR_3a}ntkEQ=*wt(IG}ma zUhqFa4#7qCv<&?EL6GtIAh-cM!r_tihN5e&yS+`@-rDuqcZUb)N!wvY_>xom+mx<8 zkmoQqBkeye<}l{edTG81gV&1RtelL0YM#pDF`0);`A=>o&4`&t0p5a-!RFO=wE*{f zQzeC!*2&VDbSt<&)7$lD1dL-1xccaJ2kQeB7Zf;nvEKCd-gL0^Os%B9C8QXSQ&sa! ztvq5$*R0VO$k)E)&YAnjgcm^4fp}fG6A9l)=19hv^Hq@%!o_Iq!x+{n{F^iYETjM^ z0SN%D0SjZM1jk6mOq|$j_|1tcQ4uzZb~t0^BsVp>gJ;q3s*!`KCJ{E?DG6EB7;Aa{ zg|hMCi?~M&@uQIVa2-CJ%s>U-yA`h|)81PZ9c(oW$(U6FGH6W;Pif_qC}q$3U{31B z2ag@l!`VX*W{W|ID*VH3EWfu7_O{pI`HXPBn(A-4w~FVZ_}dcAdA6Xz7${*F!a2ak z7_hpm;(XMkrD?#@H-E9)emb31i#omFS~Q0}#|GnyN{3Y^8d;x=v^Ns~+nJX;7ZN<8 zN2@m!rh#*BB~y4Oy5hhw_5ckHcVRYG&&N=cYdtr>!+}&)2ZdH8L3F}U&+u3JNTdT6 zqr6@8m0ye`Q`A3FouTSo1g*Kv=Z8A})Q5#W^w1dS#+k?V%Fn>xIPYIY1#v-P`eH^w z^0GOhy5v&ly~*MG{i4%3B_%ie`!-4fszE9Z$KI$HRoq0QON31*4 zd?1Y9yWRWV8~H!8ID8s%oQmA>jmYQ~$;`fwnbwY_u9T15cbXw0H5g-#{*ry3Lq_<5 z%b$wc8}HXb46vWqmC?ZkQOzE|JH^=`B}`cuBTpAkLB&_Zd(Pdl-MN9Bm#HXf0ULb_ zWle4%JS~S-D#>q#6ypF!)at}z$19VHnR^A^O_2s#nX2BKM(r>RAy-g0O}KWXvPu?M z(I??{5X$FpmUcbMiFp+7LnL`v-QWd6&l2^`kR$93M5k)pRnZ)1Lw(`1Bvl$)I6-v{ z*iy}^`aS4pN+vK}@q)f?haHG5IR2T-U)gbzRr-33p}p7pVT0n%iw8(|HxVWP_NJbJ`d|8m=WVf)8Q~0Z3XQzmcOxDb7%) z(7lZn$HAzXHphM|a2!MQ8Hi7kBM~$kV6h+lSU~ujB&gB1Y<7C{ zVD)EpT^pMW7ckSgMD1spGc()HE$6oDt@poA!)!~XSSSCcbY4IJ0G9u2TK^xr=YI!w zop>qBK?Vf==lfbxj&SstKZHf{lJN3^gj_bkHk7g9hS(Zlr+D4M`Q#5Rm2=q6%a}rc z(hYXFP5EJ{QYXcL?%%()V&?qDbQsFm%6@S3B1&Sj%8O941DMB6PxrJR)`{=LMH@%= z>Z8r%`jFH8OT7}!)S%=}oF^s@3_a~E?N#0QZ{R_jMD{BSVN0Nb*xI&y_8!zfbaG^e z4sCLXiG-uY5h(Yf-oTxQcAYrXOGV0U@KtC248F10vRO8wmDGr+vF6)skApebHaiGX zZm#7mRdhE3VP@mUGjz5lN#b-TT&nD~G9fHePPZ|wh9-Tose{cr86)wP^9 zM^Hx&L=iyf6J7FY)=jf?2TD?nC0dNwB9J%h7yl?M)+@D&o64s_E9CL}>jbV8&q-Zd z&OL`??<7?FA$Rk>OM+nF^>c}HI}oSED=O%w>&8mTs{pf z=Rq)!wVVa5;4q~Am1c5D8&|?LeR3}09}W}ju5;7roy6gCHmAL_dQI2z>Rqj>rdv(5 z+tKWFwR+aHF?qj`(eazT3CRTPRJr8r)T*h*-{)PuBti4Rm)|{mkC}t@-n23&_mu$5 zfV!sp$-qJkrVtxX9qWM!;f(;H5=BZm38pkq4L=gkcSIdg0sIut4)axIC;7x$CDYdO zEq2YZiqh8dE^fK`1$!y7j_4(}CX*~yB6n5gG2of3eqmjkl;H7**2QymP?WJ*)2MVP0CA8M3 z3ZG>~hKra$cz8gMXDtDOmydA5e}i2^wi+YHQOP?mAG{H9Z_7sz1mQ*gJjnOQM1e%h zLdcUQJiiwB$Fw+rR5nQAP!}mq0e*79`no@-yXHK&j4|8`!mpj)L)JYaxBwQtH zQ@u`Ju__ZG*B3o_m{j&>$k67?7(6Nj(EIkhn1%modHs4(^Lyj@`F{SRBwu1oIZ-#l zQ(@)M|6Dy%axFCCVgy{s`#A!$L`|fR!BiDpzhNv;yBKp!UbD?JFvmTR=0ixT8y7H3 zHJVD^Wgyv2z<7>R-B^178^$=*px&5A)0S^C4Aoe+8S9ctLJ}*5_vo2=%Tir~ z?uKF1QQu*}HsBx7DuZo20hhr*BU$2!&Rw z_@&D=vuz{*K66(!&l=@w_)NU&W<7G(6Lky+ZLZBzdFD{;&Y5&&TyS}2WIObv9)(>< zYg>f&EJ@>rYe!7R#kd}0lF8cX5Ub{XX_>V-1-7|PfLLu@kRsib7@^8MZ%L*(Eo_x; zPK#VEfN(-vxxc<$V{*?GY%p)4-i}^(;n{WbH$1~zI0hpTkocM}LzyUvAR*$bIUjFU z;1|}3(=&kM&|Q|7US$lZ3Devw<%vjrsv$2yha7W~iOKc>9?%Zth{UH1(1#KUe%wG) z<7HU6fL}>WaxeDGikQ^H{6qZTPXaOGn`jG+RPrRaZmZUR2ILIxQto9|#J`&Ih+ml# zfz5y0Us#eyMkq6)2>DOCF`A}}IuhYz{A%3g3N%n_*=Eh`@R4toS8R8)731h-J?T~*7? z!V4VGgN`dUb=uU<=shk7H@4D7y=#&?7KlpLCd69H^qHBvJD((E$crM$gOtYCNrFHH zy$LTT--2mvR)|c;2sG+tnO!SxSz(6>;QqM>`yZN1HJ(i=67HGwh)n7di#dC4dMQvi zK3oz|)l-ixE>S{~0UO@O-+aE;tof(X`1xF}U*_+8ga=i)F?e|tS>Ff#aAy*MIUxWo zQFZH-H_Sd#Iq+;zQOMRx^I5RKbNNm9IcUo3N8p5Gaz&xMgG`9!%hC27lM1_E9G7&rud=RUtd_ z)x*O>RP<2QE&%F@t=<6z+)F+}o!S>ahp}JMyWGPZ*4X#@A7rT14>^;O@=_IGJI+_7 zJ@e@25eW>Gf}^acM=g5O@XN7ze~4@QJ;{?}R|AF0O!sKOx}3K%LVxLDc7c zC+++JoBiVgnt4cpPDydk0Tp^zj*!eTK`4QzzBcDzYG|DzQa2nWY^#J9lMuy2X)A`@ z+tnl{Z_O71JOU7BBu1I~Ik$)q0a1t%qdo%Gq`lgPihH>8aMww5xVTfzv-Dvkh#=2J zEB$}F_EHEhB{n84;A;!RCi;{RH7S!dO}^Z~RN#jYD+P3J<+=pTkT%*KI0-VbQXLAI zvZn%8q&qg#om-jPHeo53Q;0DuWNmrM$>714g~ubMveUpua*Rmn>7O-TFiiNw-arS4 zgXIwKDoo_SNCyP^+<0D?gW14A3nJO6v3&IK#e3#*35!ZWD${Qa(e8g#d(jIU5IBi&|tCnjquD=7M9rK zw4Qv(OyF5n_hQN$Gf_Bo1D4`AWDdH>D5sAN_PAxexD1qT9Ss9-8yYI^L6(K9+28N} zS9g49yd2ex2>`Ic`v1`#|Bu@Dza8GPrj#|ws!O$=o0{L6bs`VWj96hJ&1@Yw3M+Gh zNjl~@rU!}0Oqv48Fc#ZUQ`C1wriiMVQ$@x2xhx`}G?xMl_1pjh5;&mvAU3e*o{GP5 zk_ZsU9MxEZYGk8$vNcXE&U&iTjnA^40zN$qN!GdAt<$d0uGek*EsvLNP2S$E7T;U# z$u&LIL@yU%Cc~08f)f$Y`Yv^Zs4@)%^x?!ZqEZSx1hXL~LsA2M zhQCPP>0I>dw*}W`S3SrbEq3{>vAF61i*Q--qDmC%Z(e_f?5(lNO+E)UpmFve`l@Xw zKlUb6OQ~__leu&1g<3711v{^;t;c9)T5=lZVwN{3%sP9;JT7Ccmo!91nprPzGM4d? zp9h=oz>I-Z}H(G&LcOJ6l8i2*nY_HY>w)0Q^RQZ%*x&ULr zCd&FFS#m(El2hK=s%NhFT(*#2je+I%-N>Q6vQ=HSR%<28L=$nDb19ftd&cg@wp34V zpwuEv5rwU<7gAv~EH?>}XPxgg!%~Cvjy-R#*lP+_hTy?>TDjpottNib9TiK8VX`aG z-_38SNM}5sncu(QRvxbXn2R3osz5JtiVQKIvMow@fjV()lg*wTMNT|xHFw#1xlBe} zZD5Jn*x7B?Xs}LxfF!k*h0_Kh8e#a>Hm=H*Ixod;u0LkQ9*Q0Q!yhjSmtu?R;w$ZW zYzNJGj=;8ddaYcnjg!b$pi5t1($` z;6;iZ>WL|pEe3;FbEF;?IQr)!Vip;+-B+n(VzPE-!)0U|i(Hy}hxk!ED-MC zlGo;5(OC&>>TWlH;k~G!lyKs;-VWesc?nhU1ivQVZJ4qLt6ZQ_j~Qd{L&O+S$n`>OG9h zpbRDDAbqJ&lGTQ|2SEKeW)!GqaOrd8e*~?Gv<)*VOZ|Ke%OlF$@=oWmo zZIiwT8J zuwbcKJn50x)S)g%{%wl1yeP4(SbxGl&{U<(t17^@OqVj2RmD!$Z(I$dVapr|Mp3A3 zeDjIL@{B4H-wZ&>RwAJ0yj9>+H1ZMS2sFO7>`w|L0VfrF?-1?mEe=q9g%&2J$cbn& z8cUZyoLWubRe*0C6UaWa&f-x>m&!TE0$vllznl{Kbc@>hvmJ#22uI0`{9Li~0$orYNDs2$2uV7@c&ZKk zoDlP=&h&JILPg7HUN$CCA;t-Ph314e?Z7uJn>WZ=I1tnvKuQG47c|wfF+` zOmy=k6f-A}%VclnjG$kT$Izby%8Xj7wX2Xr$*OF5J}I;HKH(@XDREi^R2cIgSX}HL zigSTVWShbXusdmKFuQco^*M8z=D_dTG`vfXQSd$sO`+1M$d2F^{{a6L0CWWiW8n&B z`&Likdfha^UYJACW_WG>hPj=;KZ9w20VnzB$%bNid41h>>-#8N3-_0}WXET;u}oT< z(4DGw@V+4OHF9ZS>gGTSwVapN&y=Q_R!0EjJvx{Z>&@t0b79e+_d!RQi-*-(1z2FH=Re!HgI^-{Z#u)_lh;OwOWiZ@8$g$<;38V z2+&P-UNz#3FHWp)v(q-E?VIKa&rX(RL2`<@baBq{vYI4e75#mpyxoGne42p-L!o z34-=PfxCHofTDlg3Y$oKbB5P!w_{A7vjvo%%!=G<63H^C`F%{916zPVP0dvX06ro+ zs%p$l^FdrxRnT1oVStlRuvBsujE_+?8_7Q97K|tAneu&@!q$bDL>UGcTI7;mpjL=< z%!{fGvtjF@0P;pTAbS#Kt_I$6DfZC|vzF}k|Mli~tm(CIGXSl%R6ia0!tX!1l~CWn zv&z7tA%}*_oCI+MF1zf@& zV5w+n{*vSWeN3Z%%TCyWi(*pauf3+xlV`6D-mw}rOK5Z~sG*?X6Z`j0^~ag=;4}(q zb)vuzy#gv_s2G)$?trb;3DM=WjLcy#x#6)dg7X9eT{XF4;3LNjCw{;7u6rE9rCXBJV>jyo`qmhE`c zk8cd%i_fGxQ_6+^nvk;OIH4-XEWi@Bc;gmaPVtY1ADSZN6y{lgDms3xm6$)Ly~|VG zDj>l{OVBI=CZy1|wXNI!^6iTU3LxQ;eo$aS8hEbBKJ+bULb9FBEG$Ghc<(snyreW+ zvL-pZ&|W1s@pnHhR82P7^S9`1>HCnf{IVxpcTlchB`-N~IaY@3$Mbt()z81=RSHy z{g)GM;Wtety#=}pJ1*87s`7Lmzof{Y$J^a^=#S6MS+b4n%jvD)SVDX%wk1a1Kz6UH zEC71I`YIv%GNW#U!R+unky!PdSs>X@HYLjeC9P6hDGF*6-Wh{jGwU#h zF$=A1+LoZ}YdD;HLTuj;lAGV+h<}(1#7Eazq3frsuv3DIT+u7HpqWsSvRn`kW%xm@ zZl`S7{+lff{BN=hH{4!qn(3fYJ;$48{cXl(3t$*PAP->{0u+G$%YwfEZ{V?qT531i zy@K4eTkQ@e-KkPcy03kZf#?=;e!7w$s$&JXTbPys8Sep!sb#u`a{x%sGgZ#YqL(}I z*l*}fJdo?u!-*dZBu1#6-ts1m973EXg7|4H`M+g;!I0fJ7RKDY6%FM}22`+V6QzaH zmQ=m{B}`mKRJr~NiyyV6V6^0HZ2g`O&T`5_Ba!$!cVl(wTqb{TP~Ec_+($tU(AP_c z|3;HX!}f7MxI9Rt19|U7DdJ7R2%3~eW-)EBBUc(sq}s= z7=%owWIIumew;EoKD4SO_!*?#>IE`4`e#K63xqP%#P@m?L{4!Wlf~R9b^%O?Tu^`1 zk97;Y?<;mWs~`8Ma=?={Lo9L>^sDEA%?QIUyl>7dfR0-wL^DReF=g+}kGY}2&kmT= ztDy#e7++TaDOJg}{7X!>37d&=m{Cld_D=9V?#IlxX2r!M!rxljS@yRsVEU!`(q6$8K`7<#Z1|&IfTzP0SE?<CBBeKHqW;;9y(0^%Zz;P$z1QK6-<^nk^Yew?kx}7Ez}9xy-}Est0~24FTmApx=TCT|uNr?gLwqhDqqdgvVa{J!bf3g*ccPCgzO zcRFA?%DHegwUAuoH##q^AsgkOn#aS@8MBZKt%Cv?)K$q~kF1W{N35`loYI{IlCUcUzrauhCj;h>z!~$%mjj+fv!W-h?vSZ4|d7 zLKPl*(OA`!8UPg?QQCp{77fEF*}P0yHKQ-pk^k3K9OQMBQWNy#`lEyzK(ZGx$qtPspAvBK>tHeGSYy zYtXu?J7!ufAGJx^pEv_kKTRq+OVl?XljpCr5sh}naL{^+-q#sG5S;|7 z8008J?M}`Jt>NkkMpE8uU|DsI5Hc=1GqVGm`_H*ubA_Lwm>{2iAj5FEh9KCBY6&f? zH(>H5xK_`SjXoyV6L|W0r-TPwcqmtexT9#|9;+>y*LU;P)Xn$iiC^JXI#w<3EvC%w zDMk0M>R9_A9lERVH;OCN5V{5~TFhr>s=o;nmucjqo8RVGN+Kg~mV z*wsbhu7Nk-U^+nHboI;v7dGqD+xYA`+Z!!L%aG$(?~pFFYC*dYR8ZM zF7L^vJ_oqdq0gWmPf*0-ZgWR}s#c@*<3b(c0{ww5`t|cqZ$Gwy{kjMjxCZi=m^6Cj z0AX8mtX#Bv1By*r{ilxJGU{}X1ly%uf{>m$W*)GbIAf~wc$;FZ0g4;_d^a)@(ebR6 z;EynMxAVgAkW0?`2?JOpYvIHOGO6b4wE}O(Al?c?n8To*`K`oE703VPEm7ZfO3A-Y zI=8>(lrQ#Yzep)z`%aG0pJzyh`HXmh2E{&4yp?vV{gJL_aJXi()}H_@4N%CqoyjHn z&L*V~_)chsC^Hk2p{H4)P%A@r^D(VL^n|w?;q(cr?c-uJOu6k9R}&IK-T{Fk)+6^* zXQ%mylfpV{jnH1XCi}|f{>ru!4O!ZKS5-z4l?uvXcExxfgl3FAhqI3TMldPO!VAVc zqhv79-dN6Gj3Sb1o{RH%QJ+vCEih^hnfKY~nmh33_LBv?p-vjv(#S*EF5CW0uv4V$ zDt6S49Ybd-C^lLsGnxJ4#%mV=K#Az z8fI4h{SpV|<-q|M{M{I^IOCeX2_64>>jtNH^f^yXF*}A}=)s@Cb2ep5Cz$^*(1Evm z>n}}na6j}3=g7XpkI(3uqMGt|1Zh~tW32dygeopg!NAY-?)Q@Tr+Q_H&|mD3aS{Zk zJ5Wr67xF;77WdhWcy-zvGY z1uxZ>zuT%GZ_F9qTmo`3lxS`!}6=Tm1#b;yWjr%z=aa8W;i7_eU z7R@ee%Iiek^Eo$J$z1Itj`Vju8P2tc5Cy;gp^ zces9U>K;uN<@K?&IKI%1k$qf-rUXeaWOJkC-8s6_9s?BQmJsQdj`H{g3?;kkZ?VLJ znxGFe3&jW7GuK8b9WsCQFdP3lT9mHH$UJRG2Dg3G^59T5gITtE{rtKx$#iq&s<^s?xYCq11Pfg-@oV8~X0Ft&pf{y>sUW?oQ#Wz=c2ZY0R`tZ8 z->#pk$a1+|G3k_Sbb;#ciIayB|J`h-8hiD4)t-veyo62MX}@Yf(IEv0?4jsWBDoAc zziKrfE5(VzQ=BQ;=lZLfs7{NAJ#z-t`HGifK3ls7^+ascm}xuoB2L6jg9I!Z0)3o_ zm6Y1PP?kC%tVk73iI%dZL(R|gQkW8M8+Jrtuf4;zukVsMi^Z$dpV?3ob};$fD^Xvq zsJ<0b%jejh+xtF?VceoV1-4YE#;Ymf1A=u$V%huuC7;_BX7=v-hlZ8m1%f(b%39bs zF`Zr;(zA)(+53q95GAH$!D9I2i90A3J;V!5Nj<`Xowxae)jvp#GE;i(4c9*k<&9C# zr#Z!kHQoPY4RRE-m~9}gQDUjhXcaOFol9knk_u(>2q~hnL(N??7Br;C;8LwFHmb=J z&fS4DSFgx_Dk%{PukUWO%xthIvV}qP=^4Siu!!lgl_(lYfHlY74kP1a4p8bGHA{)Zwz)yILM z!c)1=mDYR@%0Ci{fkG3!h`j!Mfe%z<$7$C4ejMfKT8y>sOt;hN!;aJJYj!Tz$Wk&{ z-!A^|7avydR}H~eCc{hVz$xw9+uwA8w7_wk{6Wj;{bq<&LzyD0@dQgGpGnsAo)k^E z9ux9i1uLp&g)Bj_N{ej6YVl{BC8=i0jv21S?DP#7ol4nuX06QYspXTjIeHDoSIYGcAnnhx81|nALg}*VEn1Y zw9!LGJ``;Skv77#3=MCBG!d(jjBO(?#cPnJlqpK{%m!KhZ;dj!OlJAq{0}F2`pic8 z+~N<3ve|U8Jm268&L!#-g);nYWUJZC1=@VhUr@>BiGp*w*6qBVtM-__7j1I|8L!Eg zeNR|wUA8(d*WHT9gafacR8-X6=4~n2ORQ78QM?$~+4`?(T%4;OTGL+ z33v?aTEi1O2-e|=0QZNI5C7|WO*~6bB`<=$X3NOBrE&_t;mr=~H`QN1=dT3|N6*DW z&%sTmHk&qBDLK+TdaI0_FCE9M2%RU{S$O9~%Z-|1aBTZgC!E`!>y{^XkDrSReUz^t zOl`RwU0V%-?P@nzTj#Ihi~Q}quv~k$&+*pj{_EyYb`mwlcWr_7)I5mbKhPl^=1>IG zEvhL*^Vgz@R&5CxGY|O~<2}bL#4X?Qdt!U#^u66Xjku3joVGg+MxEW)%$uirwY@(V z8kNhOn>K9$ryQBC_AQ6$sZK4+IyX1Mmfg0UuJX0L(Y>;ZhTo12T$AV1VAvV-x>D!f zx%uxmgWj75fQKpnsJv@Nd_|m7x>vnLyG|V3%8d(JFYKjy(S$i zKd`17s1}=uK|>*7c*9n!_3YL@+d9z~V0a_eXNzGJd$h8u09SDqMjk@fnBbQ5@bh%giCIGc(TJ4m_$px}R@ z%hKthb!z|ZOyQ_Do$fl6Yk|c$#;3MWNE?#hJPZ3|seC$_yVw7G~T=NCPV#Gt0d}|9~@T=vP2k5~Zr^Nfj|edK=;3 zJrZ0;w!Iv)?Yg%5_f17?=F$RL*S!SnKhZE9f4v3c3RU;e!nAdyO_}>=hk5+`gYHFu zUU2yFZeP>_j6j#?R5CJ<$gD^%eKZRShEVQyxY=}W{*r%tZ8hm^O%FZKRbSVfYSJxy zlW5SI89QT#o5L`3Ml;x+L})*=J&|tTYf7aaD$6aV>{eYY3#@)(qqRD>#jx0%Khfm0oeuI3l`x;${Ly1nkn_mh?>y} zP<(HY3mT@SkE4YEJ5toa8TU(DD3lG^C?3$$>~i*+hsYM)9+b z?zczQRR%*5rua|H^528%0#1!_O4Y^4v&)hM^tmH$;GK{W-;wEM)Hy3FD~Ypf>YJz% zLd-OJh`tb^4YI?Ck=%*NQ3m9zLA>P6nhN!)^Zp9#O*g{v4iOKbd}g$s<|xsH@imrZ*Dr&HNiPuxcm#2BK`5ctMBagM?vC`rW)-H zm?(b37F~^8TP9yziVGRKgUr*X$^=V-JZWHR$_;Z$&8T@_Bpf0_;zLGPiY99tUV&+M z9MCz9=tQ|AcATTPOGH8No0IbFqTOeCr(~~2m3!c$S6wD_&&lr2sdC5GbVdCJR+e zVkvY*TLl(F4K2A38+4DW(%n0=#-mti!bv!|Oi_AVak8G4rnqSg~o8G5LdjfZty z&V@=JRWZcU0?k>#kPJ7Rmu8X^QmGZV_aatS0U$qVuFb5NtLR7CQ9^b? z7Z>dj5uRuD4%Y{?2xk+r%j-0-x4j}zGk!Acw5Y1Y?(PE~+c@jJrMX-sY-wtVkzMtO zo~Qwc8D`ep;O$2F3@&{pTvw?Kdn&COc{w*QJ5zJ1&gsiNCG*nK0}hfD!mfZYdhyE= zKm?ax&6oAuMxt;|oMVF+t{fPh>?1it9v=U+4HkD@$xToHof{OZMQy_`X;3?njp=)&(sct+;JO z@SE#PazoYG{v(;Ti4GsU{S_BX#?VX2BX6Kbzzg$O$6TjW$3?Z$Lwq&i2J+ejqumS~ zxIxR1uLo!)-^czKdq;S;W4rGLU4!OV3qdVd7H{{8{1&5K7Pv}|E8e-$eJ+n}8<0?L zqmL}Qpl@F8U_wFU*eL&(m+ra&KWK#(iVyi#fQBpnK!_3PuNu*c`rd4LNZ5aR#ginJ44*}l<2qT0Okp`L)tI*X=6?Re6gkw$07D@2zH0CK%6(6)tX8QKA;?jbV@e848+u@%7*k_rD( z3gKCm!5HLNmEeeb4WJ#m?Ms+Tw5WHN_MgBt^ZQw1w;lJ+pgp!S)x+2at*7Yi4YO`7 zCyjqh3$>)ckodD~%KHZ0;k=S4Y0yfu)Y=&ZaEfb&3~AT|QgVrd*1B{&~`XO7?x_--?J_P{$=xV49k6Yb5Th1u~7 zSqIj<2gI173WBpR>g1_&#gh0(=iW8RraSTB9X0H4!gilxS0fI(1H%wW8=nyX&`>t% z=(Zf_HK*no9J3_;SbX1D_?XQ(jfPeD#d?w_4a^An z^c|e-1}RvcHO}ldImex}7sJ8I=FG;vPHD&9Q0fr9_Z^i@xD5nv0tv*GdU@Z2JO7AU zVv}W0pzcGiH{~-atX&P5bIYr)`Kz2rOsCgt@rE$>uIl|8d(HmmE3n7e*5vrvGK--z z_#T&1s&do8nHy5`weK8yj15;uip&tX30q|6Zp*b66qZFf3cCcB84JQ)oX?LN$+k82AK2*b)pthWiq=dD}!Vy4z7fTPOqcQ@?!dG@xt*YDo5kiqn^!Tz^| zf&Epgr;TxsL zkcl>t|D0&t#WDfKT{hY2l0X6PxRs{o=3SL!m(87xs@>%)Hrs!w)f_WBHen+-v%EiI zZ9Dw>Zg+&!wB#LJeJfp`VlIpu>hb>6;al;7?}X$_br@%H$QN?VX^#_*>(%jLe1X`Z z0ze5U^8j!YwADwC6Tk_@{}kY+CrvxTU8J1Hc|q;IZX2XCo8*va*uDcBtorEVngmvD z5}TJWK^0R1!B^xR)*Kp0mM5vPGXTL7s$rn}zOlxzdlj^!sUruUrk-U4b#jleJWlv4 z9N}E^uT~Ld481E2H8&%pBW{Xx0j19&xgTYLZ4lIi%zxqBGL?qmf zzfT%s_#3IL#Mz%%4s_Apk4jnp2@{3QRD~fn(Cb(qO>^S9{f#U6RbMOb8^TtI9S!_4BY~_B_qz}e0TK$r76|7)FLU#A(^TS3laFwVVi#mvsLFBzi*NvbuK}I3Y9H{(%e{Lq9~tz3JG2a0VqVvN)l< z&5+O25UQl;cqa!$w>lSiBt?Md4%qWlp=ano4_0Z_+w3Q58EPGxptu{s;v`qC5I`yD za7o)E`aLxF-6vD1eF;kPi$F7+y4MLilNKf_1$e+VEyOWD;_=JX4^9)VfD1I$+$V+i zN&YcL@h5)@BaR(?pH7?ENuZ)vJ<-radbHT(C*>iz{gpe6<~nm^MYBzZj%w*nH$dAbP2S~AOZ@<~xUqxa~+=ha5&sVt-AB9Zt z*03dy7=XLuN;gC7X3neYgD7<5{X#a_&H5>l-FJF)NM0Y%=ppQnzwam%ibXZK2IAQQ0Dzx!?UJwLR>rhbsA`1xo(qMD? znnK2?s-cw}{tJ{XcNFCRJoZ5G3jCX^2=ey~#PJzep-Xy=gX~;0E5LQBK$x?`eq%r; zd}b2>%%q1V?qmjf_TVp%kl{bk{6(<==BSMX0)YHYx&GqJP7p*Gtq{GcoPBy@C}?lo zxj>K`3LXjBX7EG)0LwK)yY}$=3~}v`JPv??rsf=>RvSji5%eLFh+^sxDxCjF6HF=4 zf_3IpbI_*#0w1H5ogwnp#6?AQ`X_vUU0IREIAihc4Vzqx1u;L|gX62_K>ZhaIM@Mw za$?l#!sb4a%lN=^lY(5oTD4%J?xdtTi=BrJAZHOm)o@!E+UT?|0akl~I8e=3G{IJp zN<2Q7BkXazL=WTEvwvcap1F^?b?`o`JrP96^-x{?Cy?5!F3P3S}%NQ`7z6CG%FMpk5KaD-!lY+Hwo3X6>y zNQ(Y2@l&0-ekKIoB5EdAhVRmy>EAP=y_R8vRyGsygF3CdAOI>zxLnx3X$oc>L4=$1 z;7jO3QI;Y~T!v!SKRXDYPGdyz1V6n`ky%Qo0QM~a%s>M;*G0w|x6@2ZIqk*K225-F zbCnwEqa&`3no+l;cf{e^6U}XvVPY zq)1A7SU%DI?#B)&ymhHZ6}UVv0QuCUvPJU#X&*STHPv8BWl}J}?#xh8LgVbdn5)kv z6rAaHEuAszcBANq4WT-O27X|zy61GUX1-YPBMA87T1Rth^vNw%O!)pUJ6StJoy5C< zvyebhME%67U^YM(tBzWCeEmU#FXd*2CSLcY=i4r?4<+jOqtp(CH32jDn1WJS_ zA$}N8t9P?8x2ssRvvo?8MFf!GM6E=-QGSfzgJhYF`D>BTYjtJr$4_Jw$9NXFQ~p{z zE2@m}a(jzj5GGj1S?8>iANf>9W;#A4Qq-p^n5^;!9i*cYcz!buCUbr6We0&Op5yB& z<5@nk<&o0;&nX^G>6qFelj@M0z^91N7#IV5- zpn3XJ6Vd4|xJw2zKq6I@BT5go;sir7m8}zA#PKc!v@O$iZ5{!CYH&db%H=^NWMJUZ z-^~auw-{M#3U_cqu2-(`D~r*xl?}(%X%?IDhBoHb?hHnXj>D2K)bLWO^qM1N7LT_1V(spG@ z4{FcG%Gv>ZfBf(-K9sdJ0N03RtA~5^VK6bHfbzNeH$}FdhJqh&g)b!uq+_aN`M|9Q z3GO1aW#r4GkIEOC=ki{KrORhC`g6xBy|awheT;2!q73+nJwWWk7p|RLjxV*ZFxWMa z2c(uG=$WvkC{wu?Xw8f|WHw*#>?#-2YIY7Ty(cV-n}3MyGgAj=Tq+-~+PElt_xX>- zXg35&%mJ|!_SNsV!Ddd}unGoK>^<%i`+bkid4jiE;2tb-`+@m)rqXW9mShmr3iQJ^ zjXe?4r-$}#5Is|$7&V|Kl|dz>2dT<=Mh*-bM>_q5Dd-dXRDCM_PyhdHSi9B=*eTHf z03e$E4}gyrMo#~&V?E;w?SixBy8h+<5kM)HYlK=Z^DruBN%a}6Tba z+9k6Vr5k8n$hus+sak2=8n(sSg|>!%GNM+&YZTusezWkB!MlprHdfKSyt$fJPkz|B zb#voD+t%7fej^#As>Qv@eUp2m`%?R2=gmG4rp4a;y6#P(ANm#h5}Sf=r+JfoolT){ zt-9%b({q!3lYQV963a)q@wtwH9ezb?gPk34h3A%YW5AuCa`S!d)*5{iwp|l1aShgb zCK-97seBFW;7;7MKO59V+1?d-6WZ0|;Er%>XZE>ohrhi8=H6ES?a;Q@V=LPNKk!|1 z4X}-0@HynbKLR)SE!deK_PMoA*v>EVD!z_ulZ6|IiDT;CSbU9f9LIRn&EmQ?IaI2= z?Fy8xi96R>Ki{%DZ>u?W*BrOwcldTg9=BtO%kOpVTDll4$F6aHkDPW#r*Fm8<6XwR zmrL6dLBT*OzCybD6!f99W(_hE2ab@;TF2s>xHiYRZD~B}yjjZ++%brBS8qogP(7eK z-l43e;x!HHVP3Ih^^wT-_Z0e!_}iyy!=bpaNv>-Hic4?l0gpQr_dO8#ErIewy5`SW zYt9BGD^(8v=lpw9?opy(=EmD*pcgT$*Cv-suTH+}7a1E}$66Xr{7SA`bR`FmgD*x= zo{U_ij92|1cKpnVA=YJ>5BPyPfIejjAb;#61uYPj4Z@nrzl)V<dyk2 z>cRwwWIf7|&zz>=&4hJW04r>Q(1wI8WTmRiEEz~yM@YxfhJwd!(p97urQsv>C{);& zR-6p0(1x5T>?tc!Ehe1up zVqEUsp6p89r4qL6yb?LqiI7v5^qjEiN5q?8*RQ1X5isVK6VDnd8K}_q=yFlh%L*j= zF;8&jbn!5ENo$Z_1j%R1_G_2hPOP0D;VB(v& z=da^~H2Hd3agz@rSI#pA^meNxcKJFMZwI@d z>I9G_WhJbf3G@{{@Yl^qR9BPANVV?e-QrLxG_j5X(T@A)-X`m~r1NXB?$UQ@XDBAA z@>vtN{G>;>`C)F@OgCE(0{S!|Sjw4hx|%B%=+Bv9Ihkol&if6=k*9!w`Qa3rAg)@4=KATcc72aS-5UWKkq_6`}z&l>?GCb z0dyYDxVm|L?W>j?w0^#qdOm!BxKnLsI>4Sx#kA^S&PxqB_kw+^XxEOo9m;b(sset5s0ifX{gk=d>T^y@LG8RF_hR1IT^iV%r-ohN%bf8xSbDY`1xGETsT;+%YzAA1 z zSN(nDvC~bQl#+?Rlim$3Vp~e?Ojd9gux7Q4wLQW^P=j*K;r=yK(4whRH~3rl3%hR8 ztsiq&80Uy9@r*g59d1smqqMK79oWFptT(bzmnKRN;0xT09j0cQgIFMospzO{|^~3?fOI6Q4dU*Z^ zuMaV0b*#~rUButm_Z!pE|Lsfl@~)>o>qvlWh+^x|2li1oVGoSS&I0Mgloe0%0DArk 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 literal 0 HcmV?d00001 diff --git a/pcntoolkit.egg-info/PKG-INFO b/pcntoolkit.egg-info/PKG-INFO new file mode 100644 index 00000000..4cd5f112 --- /dev/null +++ b/pcntoolkit.egg-info/PKG-INFO @@ -0,0 +1,9 @@ +Metadata-Version: 2.1 +Name: pcntoolkit +Version: 0.26 +Summary: Predictive Clinical Neuroscience toolkit +Home-page: http://github.com/amarquand/PCNtoolkit +Author: Andre Marquand +Author-email: andre.marquand@donders.ru.nl +License: GNU GPLv3 +License-File: LICENSE diff --git a/pcntoolkit.egg-info/SOURCES.txt b/pcntoolkit.egg-info/SOURCES.txt new file mode 100644 index 00000000..a6e10a60 --- /dev/null +++ b/pcntoolkit.egg-info/SOURCES.txt @@ -0,0 +1,37 @@ +LICENSE +README.md +setup.py +pcntoolkit/__init__.py +pcntoolkit/configs.py +pcntoolkit/normative.py +pcntoolkit/normative_NP.py +pcntoolkit/normative_parallel.py +pcntoolkit/trendsurf.py +pcntoolkit.egg-info/PKG-INFO +pcntoolkit.egg-info/SOURCES.txt +pcntoolkit.egg-info/dependency_links.txt +pcntoolkit.egg-info/not-zip-safe +pcntoolkit.egg-info/requires.txt +pcntoolkit.egg-info/top_level.txt +pcntoolkit/dataio/__init__.py +pcntoolkit/dataio/fileio.py +pcntoolkit/model/NP.py +pcntoolkit/model/NPR.py +pcntoolkit/model/SHASH.py +pcntoolkit/model/__init__.py +pcntoolkit/model/architecture.py +pcntoolkit/model/bayesreg.py +pcntoolkit/model/gp.py +pcntoolkit/model/hbr.py +pcntoolkit/model/rfa.py +pcntoolkit/normative_model/__init__.py +pcntoolkit/normative_model/norm_base.py +pcntoolkit/normative_model/norm_blr.py +pcntoolkit/normative_model/norm_gpr.py +pcntoolkit/normative_model/norm_hbr.py +pcntoolkit/normative_model/norm_np.py +pcntoolkit/normative_model/norm_rfa.py +pcntoolkit/normative_model/norm_utils.py +pcntoolkit/util/__init__.py +pcntoolkit/util/hbr_utils.py +pcntoolkit/util/utils.py \ No newline at end of file diff --git a/pcntoolkit.egg-info/dependency_links.txt b/pcntoolkit.egg-info/dependency_links.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/pcntoolkit.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/pcntoolkit.egg-info/not-zip-safe b/pcntoolkit.egg-info/not-zip-safe new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/pcntoolkit.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/pcntoolkit.egg-info/requires.txt b/pcntoolkit.egg-info/requires.txt new file mode 100644 index 00000000..2d549589 --- /dev/null +++ b/pcntoolkit.egg-info/requires.txt @@ -0,0 +1,14 @@ +argparse +nibabel>=2.5.1 +six +sklearn +bspline +matplotlib +numpy<1.23,>=1.19.5 +scipy>=1.3.2 +pandas>=0.25.3 +torch>=1.1.0 +sphinx-tabs +pymc3<=3.9.3,>=3.8 +theano==1.0.5 +arviz==0.11.0 diff --git a/pcntoolkit.egg-info/top_level.txt b/pcntoolkit.egg-info/top_level.txt new file mode 100644 index 00000000..6f8e8f14 --- /dev/null +++ b/pcntoolkit.egg-info/top_level.txt @@ -0,0 +1 @@ +pcntoolkit diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..afc06339 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,67 @@ +alabaster==0.7.13 +arviz==0.11.0 +Babel==2.11.0 +brotlipy==0.7.0 +bspline==0.1.1 +certifi==2022.12.7 +cffi==1.15.1 +cftime==1.6.2 +charset-normalizer==2.0.4 +contourpy==1.0.7 +cryptography==38.0.1 +cycler==0.11.0 +docutils==0.18.1 +fastprogress==1.0.3 +fonttools==4.38.0 +h5py==3.8.0 +idna==3.4 +imagesize==1.4.1 +Jinja2==3.1.2 +joblib==1.2.0 +kiwisolver==1.4.4 +MarkupSafe==2.1.2 +matplotlib==3.6.3 +netCDF4==1.6.2 +nibabel==5.0.0 +numpy==1.21.5 +packaging==23.0 +pandas==1.5.3 +patsy==0.5.3 +pcntoolkit==0.26 +Pillow==9.4.0 +pip==22.3.1 +pluggy==1.0.0 +pycosat==0.6.4 +pycparser==2.21 +Pygments==2.14.0 +pymc3==3.9.3 +pyOpenSSL==22.0.0 +pyparsing==3.0.9 +PySocks==1.7.1 +python-dateutil==2.8.2 +pytz==2022.7.1 +requests==2.28.1 +ruamel.yaml==0.17.21 +ruamel.yaml.clib==0.2.6 +scikit-learn==1.2.1 +scipy==1.10.0 +setuptools==65.5.0 +six==1.16.0 +snowballstemmer==2.2.0 +Sphinx==6.1.3 +sphinx-tabs==3.4.1 +sphinxcontrib-applehelp==1.0.4 +sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-htmlhelp==2.0.1 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-serializinghtml==1.1.5 +Theano==1.0.5 +threadpoolctl==3.1.0 +toolz==0.12.0 +torch==1.13.1 +tqdm==4.64.1 +typing-extensions==3.10.0.2 +urllib3==1.26.13 +wheel==0.37.1 +xarray==2023.1.0 From c7d646d137d2922156da305d93ad76ea124007b8 Mon Sep 17 00:00:00 2001 From: Stijn de Boer Date: Tue, 14 Feb 2023 19:23:08 +0100 Subject: [PATCH 10/36] Changing hyperparameters --- build/lib/pcntoolkit/model/hbr.py | 4 ++-- dist/pcntoolkit-0.26-py3.8.egg | Bin 201350 -> 201434 bytes pcntoolkit/model/hbr.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/lib/pcntoolkit/model/hbr.py b/build/lib/pcntoolkit/model/hbr.py index afb86899..c1ea1671 100644 --- a/build/lib/pcntoolkit/model/hbr.py +++ b/build/lib/pcntoolkit/model/hbr.py @@ -173,8 +173,8 @@ def hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): pb = ParamBuilder(model, X, y, batch_effects, trace, configs) if configs['likelihood'] == 'Normal': - mu = pb.make_param("mu").get_samples(pb) - sigma = pb.make_param("sigma").get_samples(pb) + mu = pb.make_param("mu", mu_slope_mu_params = (0.,100.), sigma_slope_mu_params = (100.,), mu_intercept_mu_params=(0.,100.), sigma_intercept_mu_params = (100,)).get_samples(pb) + sigma = pb.make_param("sigma", mu_sigma_params = (0., 100.), sigma_sigma_params = (100,)).get_samples(pb) sigma_plus = pm.math.log(1+pm.math.exp(sigma)) y_like = pm.Normal('y_like',mu=mu, sigma=sigma_plus, observed=y) diff --git a/dist/pcntoolkit-0.26-py3.8.egg b/dist/pcntoolkit-0.26-py3.8.egg index 156c7318e72ae4d2e55a132ae4d0e7a6c0b986ab..905fb5f1ab5ef43031a4ca31dcecc2109caef2ac 100644 GIT binary patch delta 18707 zcmV)RK(oJwr3~7o3=B|90|XQR000O8jGB=Q>H>_KvG_~@0*soIMgnpIjGB|W0=oi? znzMHUJZA!pnzOZP9{~c4nzPnx7y$x|nzQh1=rjV1nzP!WQV{};nzJsmVkQENnzNGG z)f@tinv-ev=mL$Jvs3p9Q4V#QPF7^9x8GAB000Z8mvIgO7njcl0|vKNy##^34Cb=GjRs&D{E#X2gN!&P?MAH7iD_~WKe-3HD!Iz*<3 zVRB>A6VG}9Y}eM6+M(2Q1T32se>}hE@px-6nmn4H)k(@GJ&~gC1>%oP|8dou+Wc2q zrao`7^%}KxWIaU#F|TscV5*_9yD~be+4ce(F{##Yhfe7aEvmA`qGS8~pB|W;if4K?Kn-f_4 zA{lrGv!jP0Dlft8um{fKOeiql6v-+}W#6HNz$b{$vK)`CtXu-E470$r3Yz*OVgvpt zD!^`@c}aI=J^e1-g7R-R!aPQ_Quw;Uyd|L+Er6CHhlYI$u#c-4Oly&$!Bt&hdO8dN zmmAW7$Pl7UBsR8)Y$9eEe-TA>|dLMrb<%Mfd{JJhSJSLo}pXMO54t%VEJthhj5H1N^zm1cs((C$F92NnzL4p+#4 zF8Fn#KlL08D0BPL?`O1@O}xHhTDu?C>VG7S$MWAmU^uSvtX&k$xV6uLD2jnF>WnNV zd;9v&GH3#O@HXLjf0WOc6xhFxwsfq^{>*P7Qm1nsG-#A}ER%Z~Qi6pP<6lQbGG|p0 z-STTA4ijejzgd>nyXy6)}ga6`fxGZe_nh9`9enu$mhu;4*jT3 z6$_%-2Ub+GzquQWtsFjL_s|60V4!fx_B77Mh{n7&CVMUSE=lapHas(;N$lFo0rm5=yr1}=(11P&Rj(gNRr;QER5;Be+z$`1v5QD79Pr)ncmy? zZ{V9nwl5m?0`_IIXnsc}9yOS$UgvZ6S5ARpRbagK{w?kYBR!4@38Yxb$_s%&4z(4Q zD^NQ#e_DX;hEbSBJWFdti36m6>6P#Zggu(0f9YL2aP8LVmwH{z6p=jz9n7kxI(wwz z(xFffTgmX0N&GpU)I6EAEOm;9jGDar64;H8f$rG&dr%vJFhrFML#h1A{u}4Xh}f*y zxXY#IB!3~6fk1OjQ25@P+C61s-OaW`1PFHUf7sWUT+$R+nF8C=G=zZp3x)!HXZis1 zYPF_l*t()3ypb!sF~BPkGnWCPRhLmX0w!u3yhRx{|7dg3vyPiu=8~m0&P1v+(%qXD zbvWbHXnB!wAB!@ho^xugO5Dc&A|N&Az(W`3uv^v)A&h}2c!55nUHMl5=Emi|sMb&% ze_bSLjM155rzh7V^lRMv)5Yr_fBJOsx@UVd_M`Mr=)qJ(LoP~$6QN|fR z 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 i*uEG{D~NMybONCVc delta 18774 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 Date: Tue, 14 Feb 2023 20:11:42 +0100 Subject: [PATCH 11/36] Set some better hyperparameters and a better setting of default parameters --- build/lib/pcntoolkit/model/hbr.py | 4 ++-- .../pcntoolkit/normative_model/norm_hbr.py | 6 +++++- dist/pcntoolkit-0.26-py3.8.egg | Bin 201434 -> 201504 bytes pcntoolkit/model/hbr.py | 4 ++-- pcntoolkit/normative_model/norm_hbr.py | 7 +++++++ 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/build/lib/pcntoolkit/model/hbr.py b/build/lib/pcntoolkit/model/hbr.py index c1ea1671..18ed67e2 100644 --- a/build/lib/pcntoolkit/model/hbr.py +++ b/build/lib/pcntoolkit/model/hbr.py @@ -173,8 +173,8 @@ def hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): pb = ParamBuilder(model, X, y, batch_effects, trace, configs) if configs['likelihood'] == 'Normal': - mu = pb.make_param("mu", mu_slope_mu_params = (0.,100.), sigma_slope_mu_params = (100.,), mu_intercept_mu_params=(0.,100.), sigma_intercept_mu_params = (100,)).get_samples(pb) - sigma = pb.make_param("sigma", mu_sigma_params = (0., 100.), sigma_sigma_params = (100,)).get_samples(pb) + mu = pb.make_param("mu", mu_slope_mu_params = (0.,10.), sigma_slope_mu_params = (5.,), mu_intercept_mu_params=(0.,10.), sigma_intercept_mu_params = (5.,)).get_samples(pb) + sigma = pb.make_param("sigma", mu_sigma_params = (10., 5.), sigma_sigma_params = (5.,)).get_samples(pb) sigma_plus = pm.math.log(1+pm.math.exp(sigma)) y_like = pm.Normal('y_like',mu=mu, sigma=sigma_plus, observed=y) diff --git a/build/lib/pcntoolkit/normative_model/norm_hbr.py b/build/lib/pcntoolkit/normative_model/norm_hbr.py index 8ad0b11a..16ec21ce 100644 --- a/build/lib/pcntoolkit/normative_model/norm_hbr.py +++ b/build/lib/pcntoolkit/normative_model/norm_hbr.py @@ -104,7 +104,11 @@ def __init__(self, **kwargs): self.configs['random_intercept_mu'] = kwargs.pop('random_slope','False') == 'True' ##### End Deprecations - + self.configs['linear_mu'] = kwargs.pop('linear_mu','True') == 'True' + self.configs['random_intercept_mu'] = kwargs.pop('random_intercept_mu','True') == 'True' + self.configs['random_slope_mu'] = kwargs.pop('random_slope_mu','True') == 'True' + self.configs['random_sigma'] = kwargs.pop('random_sigma','True') == 'True' + self.hbr = HBR(self.configs) @property diff --git a/dist/pcntoolkit-0.26-py3.8.egg b/dist/pcntoolkit-0.26-py3.8.egg index 905fb5f1ab5ef43031a4ca31dcecc2109caef2ac..8ab949265a03161e0e9df43f51c7bd221f75cb67 100644 GIT binary patch delta 20753 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 D_5%^$ diff --git a/pcntoolkit/model/hbr.py b/pcntoolkit/model/hbr.py index c1ea1671..18ed67e2 100644 --- a/pcntoolkit/model/hbr.py +++ b/pcntoolkit/model/hbr.py @@ -173,8 +173,8 @@ def hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): pb = ParamBuilder(model, X, y, batch_effects, trace, configs) if configs['likelihood'] == 'Normal': - mu = pb.make_param("mu", mu_slope_mu_params = (0.,100.), sigma_slope_mu_params = (100.,), mu_intercept_mu_params=(0.,100.), sigma_intercept_mu_params = (100,)).get_samples(pb) - sigma = pb.make_param("sigma", mu_sigma_params = (0., 100.), sigma_sigma_params = (100,)).get_samples(pb) + mu = pb.make_param("mu", mu_slope_mu_params = (0.,10.), sigma_slope_mu_params = (5.,), mu_intercept_mu_params=(0.,10.), sigma_intercept_mu_params = (5.,)).get_samples(pb) + sigma = pb.make_param("sigma", mu_sigma_params = (10., 5.), sigma_sigma_params = (5.,)).get_samples(pb) sigma_plus = pm.math.log(1+pm.math.exp(sigma)) y_like = pm.Normal('y_like',mu=mu, sigma=sigma_plus, observed=y) diff --git a/pcntoolkit/normative_model/norm_hbr.py b/pcntoolkit/normative_model/norm_hbr.py index 8ad0b11a..d4e3519c 100644 --- a/pcntoolkit/normative_model/norm_hbr.py +++ b/pcntoolkit/normative_model/norm_hbr.py @@ -105,6 +105,13 @@ def __init__(self, **kwargs): ##### End Deprecations + ## Default parameters + self.configs['linear_mu'] = kwargs.pop('linear_mu','True') == 'True' + self.configs['random_intercept_mu'] = kwargs.pop('random_intercept_mu','True') == 'True' + self.configs['random_slope_mu'] = kwargs.pop('random_slope_mu','True') == 'True' + self.configs['random_sigma'] = kwargs.pop('random_sigma','True') == 'True' + ## End default parameters + self.hbr = HBR(self.configs) @property From 6496612d8d7794865b5563c7c643b95180edfc52 Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Tue, 14 Feb 2023 20:31:38 +0100 Subject: [PATCH 12/36] cleaning up --- build/lib/pcntoolkit/__init__.py | 4 - build/lib/pcntoolkit/configs.py | 9 - build/lib/pcntoolkit/dataio/__init__.py | 1 - build/lib/pcntoolkit/dataio/fileio.py | 427 ----- build/lib/pcntoolkit/model/NP.py | 82 - build/lib/pcntoolkit/model/NPR.py | 80 - build/lib/pcntoolkit/model/SHASH.py | 271 --- build/lib/pcntoolkit/model/__init__.py | 6 - build/lib/pcntoolkit/model/architecture.py | 201 --- build/lib/pcntoolkit/model/bayesreg.py | 568 ------- build/lib/pcntoolkit/model/gp.py | 488 ------ build/lib/pcntoolkit/model/hbr.py | 923 ---------- build/lib/pcntoolkit/model/rfa.py | 243 --- build/lib/pcntoolkit/normative.py | 1434 ---------------- build/lib/pcntoolkit/normative_NP.py | 270 --- .../pcntoolkit/normative_model/__init__.py | 6 - .../pcntoolkit/normative_model/norm_base.py | 60 - .../pcntoolkit/normative_model/norm_blr.py | 252 --- .../pcntoolkit/normative_model/norm_gpr.py | 72 - .../pcntoolkit/normative_model/norm_hbr.py | 235 --- .../lib/pcntoolkit/normative_model/norm_np.py | 229 --- .../pcntoolkit/normative_model/norm_rfa.py | 72 - .../pcntoolkit/normative_model/norm_utils.py | 28 - build/lib/pcntoolkit/normative_parallel.py | 1275 -------------- build/lib/pcntoolkit/trendsurf.py | 253 --- build/lib/pcntoolkit/util/__init__.py | 1 - build/lib/pcntoolkit/util/hbr_utils.py | 236 --- build/lib/pcntoolkit/util/utils.py | 1507 ----------------- dist/pcntoolkit-0.26-py3.8.egg | Bin 201504 -> 0 bytes dist/pcntoolkit-0.26-py3.9.egg | Bin 201529 -> 0 bytes pcntoolkit.egg-info/PKG-INFO | 9 - pcntoolkit.egg-info/SOURCES.txt | 37 - pcntoolkit.egg-info/dependency_links.txt | 1 - pcntoolkit.egg-info/not-zip-safe | 1 - pcntoolkit.egg-info/requires.txt | 14 - pcntoolkit.egg-info/top_level.txt | 1 - 36 files changed, 9296 deletions(-) delete mode 100644 build/lib/pcntoolkit/__init__.py delete mode 100644 build/lib/pcntoolkit/configs.py delete mode 100644 build/lib/pcntoolkit/dataio/__init__.py delete mode 100644 build/lib/pcntoolkit/dataio/fileio.py delete mode 100644 build/lib/pcntoolkit/model/NP.py delete mode 100644 build/lib/pcntoolkit/model/NPR.py delete mode 100644 build/lib/pcntoolkit/model/SHASH.py delete mode 100644 build/lib/pcntoolkit/model/__init__.py delete mode 100644 build/lib/pcntoolkit/model/architecture.py delete mode 100644 build/lib/pcntoolkit/model/bayesreg.py delete mode 100644 build/lib/pcntoolkit/model/gp.py delete mode 100644 build/lib/pcntoolkit/model/hbr.py delete mode 100644 build/lib/pcntoolkit/model/rfa.py delete mode 100644 build/lib/pcntoolkit/normative.py delete mode 100644 build/lib/pcntoolkit/normative_NP.py delete mode 100644 build/lib/pcntoolkit/normative_model/__init__.py delete mode 100644 build/lib/pcntoolkit/normative_model/norm_base.py delete mode 100644 build/lib/pcntoolkit/normative_model/norm_blr.py delete mode 100644 build/lib/pcntoolkit/normative_model/norm_gpr.py delete mode 100644 build/lib/pcntoolkit/normative_model/norm_hbr.py delete mode 100644 build/lib/pcntoolkit/normative_model/norm_np.py delete mode 100644 build/lib/pcntoolkit/normative_model/norm_rfa.py delete mode 100644 build/lib/pcntoolkit/normative_model/norm_utils.py delete mode 100644 build/lib/pcntoolkit/normative_parallel.py delete mode 100644 build/lib/pcntoolkit/trendsurf.py delete mode 100644 build/lib/pcntoolkit/util/__init__.py delete mode 100644 build/lib/pcntoolkit/util/hbr_utils.py delete mode 100644 build/lib/pcntoolkit/util/utils.py delete mode 100644 dist/pcntoolkit-0.26-py3.8.egg delete mode 100644 dist/pcntoolkit-0.26-py3.9.egg delete mode 100644 pcntoolkit.egg-info/PKG-INFO delete mode 100644 pcntoolkit.egg-info/SOURCES.txt delete mode 100644 pcntoolkit.egg-info/dependency_links.txt delete mode 100644 pcntoolkit.egg-info/not-zip-safe delete mode 100644 pcntoolkit.egg-info/requires.txt delete mode 100644 pcntoolkit.egg-info/top_level.txt diff --git a/build/lib/pcntoolkit/__init__.py b/build/lib/pcntoolkit/__init__.py deleted file mode 100644 index 087fe624..00000000 --- a/build/lib/pcntoolkit/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from . import trendsurf -from . import normative -from . import normative_parallel -from . import normative_NP 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/build/lib/pcntoolkit/dataio/__init__.py b/build/lib/pcntoolkit/dataio/__init__.py deleted file mode 100644 index 1208872a..00000000 --- a/build/lib/pcntoolkit/dataio/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import fileio diff --git a/build/lib/pcntoolkit/dataio/fileio.py b/build/lib/pcntoolkit/dataio/fileio.py deleted file mode 100644 index 37ce1ef7..00000000 --- a/build/lib/pcntoolkit/dataio/fileio.py +++ /dev/null @@ -1,427 +0,0 @@ -from __future__ import print_function - -import os -import sys -import numpy as np -import nibabel as nib -import tempfile -import pandas as pd -import re - -try: # run as a package if installed - from pcntoolkit import configs -except ImportError: - pass - - path = os.path.abspath(os.path.dirname(__file__)) - path = os.path.dirname(path) # parent directory - if path not in sys.path: - sys.path.append(path) - del path - import configs - -CIFTI_MAPPINGS = ('dconn', 'dtseries', 'pconn', 'ptseries', 'dscalar', - 'dlabel', 'pscalar', 'pdconn', 'dpconn', - 'pconnseries', 'pconnscalar') - -CIFTI_VOL_ATLAS = 'Atlas_ROIs.2.nii.gz' - -PICKLE_PROTOCOL = configs.PICKLE_PROTOCOL - -# ------------------------ -# general utility routines -# ------------------------ - -def predictive_interval(s2_forward, - cov_forward, - multiplicator): - # calculates a predictive interval - - PI=np.zeros(len(cov_forward)) - for i,xdot in enumerate(cov_forward): - s=np.sqrt(s2_forward[i]) - PI[i]=multiplicator*s - return PI - -def create_mask(data_array, mask, verbose=False): - # create a (volumetric) mask either from an input nifti or the nifti itself - - if mask is not None: - if verbose: - print('Loading ROI mask ...') - maskvol = load_nifti(mask, vol=True) - maskvol = maskvol != 0 - else: - if len(data_array.shape) < 4: - dim = data_array.shape[0:3] + (1,) - else: - dim = data_array.shape[0:3] + (data_array.shape[3],) - - if verbose: - print('Generating mask automatically ...') - if dim[3] == 1: - maskvol = data_array[:, :, :] != 0 - else: - maskvol = data_array[:, :, :, 0] != 0 - - return maskvol - - -def vol2vec(dat, mask, verbose=False): - # vectorise a 3d image - - if len(dat.shape) < 4: - dim = dat.shape[0:3] + (1,) - else: - dim = dat.shape[0:3] + (dat.shape[3],) - - #mask = create_mask(dat, mask=mask, verbose=verbose) - if mask is None: - mask = create_mask(dat, mask=mask, verbose=verbose) - - # mask the image - maskid = np.where(mask.ravel())[0] - dat = np.reshape(dat, (np.prod(dim[0:3]), dim[3])) - dat = dat[maskid, :] - - # convert to 1-d array if the file only contains one volume - if dim[3] == 1: - dat = dat.ravel() - - return dat - - -def file_type(filename): - # routine to determine filetype - - if filename.endswith(('.dtseries.nii', '.dscalar.nii', '.dlabel.nii')): - ftype = 'cifti' - elif filename.endswith(('.nii.gz', '.nii', '.img', '.hdr')): - ftype = 'nifti' - elif filename.endswith(('.txt', '.csv', '.tsv', '.asc')): - ftype = 'text' - elif filename.endswith(('.pkl')): - ftype = 'binary' - else: - raise ValueError("I don't know what to do with " + filename) - - return ftype - - -def file_extension(filename): - # routine to get the full file extension (e.g. .nii.gz, not just .gz) - - parts = filename.split(os.extsep) - - if parts[-1] == 'gz': - if parts[-2] == 'nii' or parts[-2] == 'img' or parts[-2] == 'hdr': - ext = parts[-2] + '.' + parts[-1] - else: - ext = parts[-1] - elif parts[-1] == 'nii': - if parts[-2] in CIFTI_MAPPINGS: - ext = parts[-2] + '.' + parts[-1] - else: - ext = parts[-1] - else: - ext = parts[-1] - - ext = '.' + ext - return ext - - -def file_stem(filename): - - idx = filename.find(file_extension(filename)) - stm = filename[0:idx] - - return stm - -# -------------- -# nifti routines -# -------------- - - -def load_nifti(datafile, mask=None, vol=False, verbose=False): - - if verbose: - print('Loading nifti: ' + datafile + ' ...') - img = nib.load(datafile) - dat = img.get_data() - - if mask is not None: - mask=load_nifti(mask, vol=True) - - if not vol: - dat = vol2vec(dat, mask) - - return dat - - -def save_nifti(data, filename, examplenii, mask, dtype=None): - - ''' - Write output to nifti - - Basic usage:: - - save_nifti(data, filename mask, dtype) - - where the variables are defined below. - - :param data: numpy array containing the data to write out - :param filename: where to store it - :param examplenii: nifti to copy the geometry and data type from - :mask: nifti image containing a mask for the image - :param dtype: data type for the output image (if different from the image) - ''' - - - # load mask - if isinstance(mask, str): - mask = load_nifti(mask, vol=True) - mask = mask != 0 - - # load example image - ex_img = nib.load(examplenii) - ex_img.shape - dim = ex_img.shape[0:3] - if len(data.shape) < 2: - nvol = 1 - data = data[:, np.newaxis] - else: - nvol = int(data.shape[1]) - - # write data - array_data = np.zeros((np.prod(dim), nvol)) - array_data[mask.flatten(), :] = data - array_data = np.reshape(array_data, dim+(nvol,)) - hdr = ex_img.header - if dtype is not None: - hdr.set_data_dtype(dtype) - array_data = array_data.astype(dtype) - array_img = nib.Nifti1Image(array_data, ex_img.affine, hdr) - - nib.save(array_img, filename) - -# -------------- -# cifti routines -# -------------- - - -def load_cifti(filename, vol=False, mask=None, rmtmp=True): - - # parse the name - dnam, fnam = os.path.split(filename) - fpref = file_stem(fnam) - outstem = os.path.join(tempfile.gettempdir(), - str(os.getpid()) + "-" + fpref) - - # extract surface data from the cifti file - print("Extracting cifti surface data to ", outstem, '-*.func.gii', sep="") - giinamel = outstem + '-left.func.gii' - giinamer = outstem + '-right.func.gii' - os.system('wb_command -cifti-separate ' + filename + - ' COLUMN -metric CORTEX_LEFT ' + giinamel) - os.system('wb_command -cifti-separate ' + filename + - ' COLUMN -metric CORTEX_RIGHT ' + giinamer) - - # load the surface data - giil = nib.load(giinamel) - giir = nib.load(giinamer) - Nimg = len(giil.darrays) - Nvert = len(giil.darrays[0].data) - if Nimg == 1: - out = np.concatenate((giil.darrays[0].data, giir.darrays[0].data), - axis=0) - else: - Gl = np.zeros((Nvert, Nimg)) - Gr = np.zeros((Nvert, Nimg)) - for i in range(0, Nimg): - Gl[:, i] = giil.darrays[i].data - Gr[:, i] = giir.darrays[i].data - out = np.concatenate((Gl, Gr), axis=0) - if rmtmp: - # clean up temporary files - os.remove(giinamel) - os.remove(giinamer) - - if vol: - niiname = outstem + '-vol.nii' - print("Extracting cifti volume data to ", niiname, sep="") - os.system('wb_command -cifti-separate ' + filename + - ' COLUMN -volume-all ' + niiname) - vol = load_nifti(niiname, vol=True) - volmask = create_mask(vol) - out = np.concatenate((out, vol2vec(vol, volmask)), axis=0) - if rmtmp: - os.remove(niiname) - - return out - - -def save_cifti(data, filename, example, mask=None, vol=True, volatlas=None): - """ Write output to nifti """ - - # do some sanity checks - if data.dtype == 'float32' or \ - data.dtype == 'float' or \ - data.dtype == 'float64': - data = data.astype('float32') # force 32 bit output - dtype = 'NIFTI_TYPE_FLOAT32' - else: - raise(ValueError, 'Only float data types currently handled') - - if len(data.shape) == 1: - Nimg = 1 - data = data[:, np.newaxis] - else: - Nimg = data.shape[1] - - # get the base filename - dnam, fnam = os.path.split(filename) - fstem = file_stem(fnam) - - # Split the template - estem = os.path.join(tempfile.gettempdir(), str(os.getpid()) + "-" + fstem) - giiexnamel = estem + '-left.func.gii' - giiexnamer = estem + '-right.func.gii' - os.system('wb_command -cifti-separate ' + example + - ' COLUMN -metric CORTEX_LEFT ' + giiexnamel) - os.system('wb_command -cifti-separate ' + example + - ' COLUMN -metric CORTEX_RIGHT ' + giiexnamer) - - # write left hemisphere - giiexl = nib.load(giiexnamel) - Nvertl = len(giiexl.darrays[0].data) - garraysl = [] - for i in range(0, Nimg): - garraysl.append( - nib.gifti.gifti.GiftiDataArray(data=data[0:Nvertl, i], - datatype=dtype)) - giil = nib.gifti.gifti.GiftiImage(darrays=garraysl) - fnamel = fstem + '-left.func.gii' - nib.save(giil, fnamel) - - # write right hemisphere - giiexr = nib.load(giiexnamer) - Nvertr = len(giiexr.darrays[0].data) - garraysr = [] - for i in range(0, Nimg): - garraysr.append( - nib.gifti.gifti.GiftiDataArray(data=data[Nvertl:Nvertl+Nvertr, i], - datatype=dtype)) - giir = nib.gifti.gifti.GiftiImage(darrays=garraysr) - fnamer = fstem + '-right.func.gii' - nib.save(giir, fnamer) - - tmpfiles = [fnamer, fnamel, giiexnamel, giiexnamer] - - # process volumetric data - if vol: - niiexname = estem + '-vol.nii' - os.system('wb_command -cifti-separate ' + example + - ' COLUMN -volume-all ' + niiexname) - niivol = load_nifti(niiexname, vol=True) - if mask is None: - mask = create_mask(niivol) - - if volatlas is None: - volatlas = CIFTI_VOL_ATLAS - fnamev = fstem + '-vol.nii' - - save_nifti(data[Nvertr+Nvertl:, :], fnamev, niiexname, mask) - tmpfiles.extend([fnamev, niiexname]) - - # write cifti - fname = fstem + '.dtseries.nii' - os.system('wb_command -cifti-create-dense-timeseries ' + fname + - ' -volume ' + fnamev + ' ' + volatlas + - ' -left-metric ' + fnamel + ' -right-metric ' + fnamer) - - # clean up - for f in tmpfiles: - os.remove(f) - -# -------------- -# ascii routines -# -------------- - - -def load_pd(filename): - # based on pandas - x = pd.read_csv(filename, - sep=' ', - header=None) - return x - - -def save_pd(data, filename): - # based on pandas - data.to_csv(filename, - index=None, - header=None, - sep=' ', - na_rep='NaN') - - -def load_ascii(filename): - # based on pandas - x = np.loadtxt(filename) - return x - - -def save_ascii(data, filename): - # based on pandas - np.savetxt(filename, data) - -# ---------------- -# generic routines -# ---------------- - - -def save(data, filename, example=None, mask=None, text=False, dtype=None): - - if file_type(filename) == 'cifti': - save_cifti(data.T, filename, example, vol=True) - elif file_type(filename) == 'nifti': - save_nifti(data.T, filename, example, mask, dtype=dtype) - elif text or file_type(filename) == 'text': - save_ascii(data, filename) - elif file_type(filename) == 'binary': - data = pd.DataFrame(data) - data.to_pickle(filename, protocol=PICKLE_PROTOCOL) - - -def load(filename, mask=None, text=False, vol=True): - - if file_type(filename) == 'cifti': - x = load_cifti(filename, vol=vol) - elif file_type(filename) == 'nifti': - x = load_nifti(filename, mask, vol=vol) - elif text or file_type(filename) == 'text': - x = load_ascii(filename) - elif file_type(filename) == 'binary': - x = pd.read_pickle(filename) - x = x.to_numpy() - - return x - -# ------------------- -# sorting routines for batched in normative parallel -# ------------------- - - -def tryint(s): - try: - return int(s) - except ValueError: - return s - - -def alphanum_key(s): - return [tryint(c) for c in re.split('([0-9]+)', s)] - - -def sort_nicely(l): - return sorted(l, key=alphanum_key) diff --git a/build/lib/pcntoolkit/model/NP.py b/build/lib/pcntoolkit/model/NP.py deleted file mode 100644 index 13370286..00000000 --- a/build/lib/pcntoolkit/model/NP.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Mon Jun 24 15:06:06 2019 - -@author: seykia -""" - -import torch -from torch import nn -from torch.nn import functional as F - -##################################### NP Model ################################ - -class NP(nn.Module): - def __init__(self, encoder, decoder, args): - super(NP, self).__init__() - self.r_dim = encoder.r_dim - self.z_dim = encoder.z_dim - self.dp_level = encoder.dp_level - self.encoder = encoder - self.decoder = decoder - self.r_to_z_mean_dp = nn.Dropout(p = self.dp_level) - self.r_to_z_mean = nn.Linear(self.r_dim, self.z_dim) - self.r_to_z_logvar_dp = nn.Dropout(p = self.dp_level) - self.r_to_z_logvar = nn.Linear(self.r_dim, self.z_dim) - self.device = args.device - self.type = args.type - - def xy_to_z_params(self, x, y): - r = self.encoder.forward(x, y) - mu = self.r_to_z_mean(self.r_to_z_mean_dp(r)) - logvar = self.r_to_z_logvar(self.r_to_z_logvar_dp(r)) - return mu, logvar - - def reparameterise(self, z): - mu, logvar = z - std = torch.exp(0.5 * logvar) - eps = torch.randn_like(std) - z_sample = eps.mul(std).add_(mu) - return z_sample - - def forward(self, x_context, y_context, x_all=None, y_all=None, n = 10): - y_sigma = None - z_context = self.xy_to_z_params(x_context, y_context) - if self.training: - z_all = self.xy_to_z_params(x_all, y_all) - z_sample = self.reparameterise(z_all) - y_hat = self.decoder.forward(z_sample, x_all) - else: - z_all = z_context - if self.type == 'ST': - temp = torch.zeros([n,y_context.shape[0], y_context.shape[2]], device = 'cpu') - elif self.type == 'MT': - temp = torch.zeros([n,y_context.shape[0],1,y_context.shape[2],y_context.shape[3], - y_context.shape[4]], device = 'cpu') - for i in range(n): - z_sample = self.reparameterise(z_all) - temp[i,:] = self.decoder.forward(z_sample, x_context) - y_hat = torch.mean(temp, dim=0).to(self.device) - if n > 1: - y_sigma = torch.std(temp, dim=0).to(self.device) - return y_hat, z_all, z_context, y_sigma - -############################################################################### - -def apply_dropout_test(m): - if type(m) == nn.Dropout: - m.train() - -def kl_div_gaussians(mu_q, logvar_q, mu_p, logvar_p): - var_p = torch.exp(logvar_p) - kl_div = (torch.exp(logvar_q) + (mu_q - mu_p) ** 2) / (var_p) \ - - 1.0 \ - + logvar_p - logvar_q - kl_div = 0.5 * kl_div.sum() - return kl_div - -def np_loss(y_hat, y, z_all, z_context): - BCE = F.binary_cross_entropy(torch.squeeze(y_hat), torch.mean(y,dim=1), reduction="sum") - KLD = kl_div_gaussians(z_all[0], z_all[1], z_context[0], z_context[1]) - return BCE + KLD diff --git a/build/lib/pcntoolkit/model/NPR.py b/build/lib/pcntoolkit/model/NPR.py deleted file mode 100644 index 07bee34c..00000000 --- a/build/lib/pcntoolkit/model/NPR.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Fri Nov 22 14:32:37 2019 - -@author: seykia -""" - -import torch -from torch import nn -from torch.nn import functional as F - -##################################### NP Model ################################ - -class NPR(nn.Module): - def __init__(self, encoder, decoder, args): - super(NPR, self).__init__() - self.r_dim = encoder.r_dim - self.z_dim = encoder.z_dim - self.encoder = encoder - self.decoder = decoder - self.r_to_z_mean = nn.Linear(self.r_dim, self.z_dim) - self.r_to_z_logvar = nn.Linear(self.r_dim, self.z_dim) - self.device = args.device - - def xy_to_z_params(self, x, y): - r = self.encoder.forward(x, y) - mu = self.r_to_z_mean(r) - logvar = self.r_to_z_logvar(r) - return mu, logvar - - def reparameterise(self, z): - mu, logvar = z - std = torch.exp(0.5 * logvar) - eps = torch.randn_like(std) - z_sample = eps.mul(std).add_(mu) - return z_sample - - def forward(self, x_context, y_context, x_all=None, y_all=None, n = 10): - y_sigma = None - y_sigma_84 = None - z_context = self.xy_to_z_params(x_context, y_context) - if self.training: - z_all = self.xy_to_z_params(x_all, y_all) - z_sample = self.reparameterise(z_all) - y_hat, y_hat_84 = self.decoder.forward(z_sample) - else: - z_all = z_context - temp = torch.zeros([n,y_context.shape[0], y_context.shape[2]], device = self.device) - temp_84 = torch.zeros([n,y_context.shape[0], y_context.shape[2]], device = self.device) - for i in range(n): - z_sample = self.reparameterise(z_all) - temp[i,:], temp_84[i,:] = self.decoder.forward(z_sample) - y_hat = torch.mean(temp, dim=0).to(self.device) - y_hat_84 = torch.mean(temp_84, dim=0).to(self.device) - if n > 1: - y_sigma = torch.std(temp, dim=0).to(self.device) - y_sigma_84 = torch.std(temp_84, dim=0).to(self.device) - return y_hat, y_hat_84, z_all, z_context, y_sigma, y_sigma_84 - -############################################################################### - -def kl_div_gaussians(mu_q, logvar_q, mu_p, logvar_p): - var_p = torch.exp(logvar_p) - kl_div = (torch.exp(logvar_q) + (mu_q - mu_p) ** 2) / (var_p) \ - - 1.0 \ - + logvar_p - logvar_q - kl_div = 0.5 * kl_div.sum() - return kl_div - -def np_loss(y_hat, y_hat_84, y, z_all, z_context): - #PBL = pinball_loss(y, y_hat, 0.05) - BCE = F.binary_cross_entropy(torch.squeeze(y_hat), torch.mean(y,dim=1), reduction="sum") - idx1 = (y >= y_hat_84).squeeze() - idx2 = (y < y_hat_84).squeeze() - BCE84 = 0.84 * F.binary_cross_entropy(torch.squeeze(y_hat_84[idx1,:]), torch.mean(y[idx1,:],dim=1), reduction="sum") + \ - 0.16 * F.binary_cross_entropy(torch.squeeze(y_hat_84[idx2,:]), torch.mean(y[idx2,:],dim=1), reduction="sum") - KLD = kl_div_gaussians(z_all[0], z_all[1], z_context[0], z_context[1]) - return BCE + KLD + BCE84 - diff --git a/build/lib/pcntoolkit/model/SHASH.py b/build/lib/pcntoolkit/model/SHASH.py deleted file mode 100644 index 5ab91e51..00000000 --- a/build/lib/pcntoolkit/model/SHASH.py +++ /dev/null @@ -1,271 +0,0 @@ -import theano.tensor -from pymc3.distributions import Continuous, draw_values, generate_samples -import theano.tensor as tt -import numpy as np -from pymc3.distributions.dist_math import bound -import scipy.special as spp -from theano import as_op -from theano.gof.fg import NullType -from theano.gof.op import Op -from theano.gof.graph import Apply -from theano.gradient import grad_not_implemented - -""" -@author: Stijn de Boer (AuguB) - -See: Jones et al. (2009), Sinh-Arcsinh distributions. -""" - - -class K(Op): - """ - Modified Bessel function of the second kind, theano implementation - """ - def make_node(self, p, x): - p = theano.tensor.as_tensor_variable(p, 'floatX') - x = theano.tensor.as_tensor_variable(x, 'floatX') - return Apply(self, [p,x], [p.type()]) - - def perform(self, node, inputs, output_storage, params=None): - # Doing this on the unique values avoids doing A LOT OF double work, apparently scipy doesn't do this by itself - unique_inputs, inverse_indices = np.unique(inputs[0], return_inverse=True) - unique_outputs = spp.kv(unique_inputs, inputs[1]) - outputs = unique_outputs[inverse_indices].reshape(inputs[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)] - -class SHASH(Continuous): - """ - SHASH described by Jones et al., based on a standard normal - All SHASH subclasses inherit from this - """ - def __init__(self, epsilon, delta, **kwargs): - super().__init__(**kwargs) - self.epsilon = tt.as_tensor_variable(epsilon) - self.delta = tt.as_tensor_variable(delta) - self.K = K() - - - def random(self, point=None, size=None): - epsilon, delta = draw_values([self.epsilon, self.delta], - point=point, size=size) - - def _random(epsilon, delta, size=None): - samples_transformed = np.sinh((np.arcsinh(np.random.randn(*size)) + epsilon) / delta) - return samples_transformed - - return generate_samples(_random, epsilon=epsilon, delta=delta, dist_shape=self.shape, size=size) - - def logp(self, value): - epsilon = self.epsilon - delta = self.delta + tt.np.finfo(np.float32).eps - - this_S = self.S(value) - this_S_sqr = tt.sqr(this_S) - this_C_sqr = 1+this_S_sqr - frac1 = -tt.log(tt.constant(2 * tt.np.pi))/2 - frac2 = tt.log(delta) + tt.log(this_C_sqr)/2 - tt.log(1 + tt.sqr(value)) / 2 - exp = -this_S_sqr / 2 - - return bound(frac1 + frac2 + exp, delta > 0) - - def S(self, x): - """ - - :param epsilon: - :param delta: - :param x: - :return: The sinharcsinh transformation of x - """ - return tt.sinh(tt.arcsinh(x) * self.delta - self.epsilon) - - def S_inv(self, x): - return tt.sinh((tt.arcsinh(x) + self.epsilon) / self.delta) - - def C(self, x): - """ - :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. - """ - return tt.cosh(tt.arcsinh(x) * self.delta - self.epsilon) - - def P(self, q): - """ - The P function as given in Jones et al. - :param q: - :return: - """ - frac = np.exp(1 / 4) / np.power(8 * np.pi, 1 / 2) - K1 = self.K((q+1)/2,1/4) - K2 = self.K((q-1)/2,1/4) - a = (K1 + K2) * frac - return a - - def m(self, 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. - """ - frac1 = tt.as_tensor_variable(1 / np.power(2, r)) - acc = tt.as_tensor_variable(0) - for i in range(r + 1): - combs = spp.comb(r, i) - flip = np.power(-1, i) - ex = np.exp((r - 2 * i) * self.epsilon / self.delta) - # This is the reason we can not sample delta/kurtosis using NUTS; the gradient of P is unknown to pymc3 - # TODO write a class that inherits theano.Op and do the gradient in there :) - p = self.P((r - 2 * i) / self.delta) - acc += combs * flip * ex * p - return frac1 * acc - -class SHASHo(SHASH): - """ - This is the shash where the location and scale parameters have simply been applied as an linear transformation - directly on the original shash. - """ - - def __init__(self, mu, sigma, epsilon, delta, **kwargs): - super().__init__(epsilon, delta, **kwargs) - self.mu = tt.as_tensor_variable(mu) - self.sigma = tt.as_tensor_variable(sigma) - - def random(self, point=None, size=None): - mu, sigma, epsilon, delta = draw_values([self.mu, self.sigma, self.epsilon, self.delta], - point=point, size=size) - - def _random(mu, sigma, epsilon, delta, size=None): - samples_transformed = np.sinh((np.arcsinh(np.random.randn(*size)) + epsilon) / delta) * sigma + mu - return samples_transformed - - return generate_samples(_random, mu=mu, sigma=sigma, epsilon=epsilon, delta=delta, - dist_shape=self.shape, - size=size) - - def logp(self, value): - mu = self.mu - sigma = self.sigma + tt.np.finfo(np.float32).eps - epsilon = self.epsilon - delta = self.delta + tt.np.finfo(np.float32).eps - - value_transformed = (value - mu) / sigma - - this_S = self.S( value_transformed) - this_S_sqr = tt.sqr(this_S) - this_C_sqr = 1+this_S_sqr - frac1 = -tt.log(tt.constant(2 * tt.np.pi))/2 - frac2 = tt.log(delta) + tt.log(this_C_sqr)/2 - tt.log( - 1 + tt.sqr(value_transformed)) / 2 - exp = -this_S_sqr / 2 - change_of_variable = -tt.log(sigma) - - return bound(frac1 + frac2 + exp + change_of_variable, sigma > 0, delta > 0) - - -class SHASHo2(SHASH): - """ - This is the shash where we apply the reparameterization provided in section 4.3 in Jones et al. - """ - - def __init__(self, mu, sigma, epsilon, delta, **kwargs): - super().__init__(epsilon, delta, **kwargs) - self.mu = tt.as_tensor_variable(mu) - self.sigma = tt.as_tensor_variable(sigma) - - def random(self, point=None, size=None): - mu, sigma, epsilon, delta = draw_values( - [self.mu, self.sigma, self.epsilon, self.delta], - point=point, size=size) - sigma_d = sigma / delta - - def _random(mu, sigma, epsilon, delta, size=None): - samples_transformed = np.sinh( - (np.arcsinh(np.random.randn(*size)) + epsilon) / delta) * sigma_d + mu - return samples_transformed - - return generate_samples(_random, mu=mu, sigma=sigma_d, epsilon=epsilon, delta=delta, - dist_shape=self.shape, - size=size) - - def logp(self, value): - mu = self.mu - sigma = self.sigma + tt.np.finfo(np.float32).eps - epsilon = self.epsilon - delta = self.delta + tt.np.finfo(np.float32).eps - sigma_d = sigma / delta - - - # Here a double change of variables is applied - value_transformed = ((value - mu) / sigma_d) - - this_S = self.S(value_transformed) - this_S_sqr = tt.sqr(this_S) - this_C = tt.sqrt(1+this_S_sqr) - frac1 = -tt.log(tt.sqrt(tt.constant(2 * tt.np.pi))) - frac2 = tt.log(delta) + tt.log(this_C) - tt.log( - 1 + tt.sqr(value_transformed)) / 2 - exp = -this_S_sqr / 2 - change_of_variable = -tt.log(sigma_d) - - # the change of variables is accounted for in the density by division and multiplication (adding and subtracting for logp) - return bound(frac1 + frac2 + exp + change_of_variable, delta > 0, sigma > 0) - -class SHASHb(SHASH): - """ - This is the shash where the location and scale parameters been applied as an linear transformation on the shash - distribution which was corrected for mean and variance. - """ - - def __init__(self, mu, sigma, epsilon, delta, **kwargs): - super().__init__(epsilon, delta, **kwargs) - self.mu = tt.as_tensor_variable(mu) - self.sigma = tt.as_tensor_variable(sigma) - - def random(self, point=None, size=None): - mu, sigma, epsilon, delta = draw_values( - [self.mu, self.sigma, self.epsilon, self.delta], - point=point, size=size) - mean = (tt.sinh(epsilon/delta)*self.P(1/delta)).eval() - var = ((tt.cosh(2*epsilon/delta)*self.P(2/delta)-1)/2).eval() - mean**2 - - def _random(mean, var, mu, sigma, epsilon, delta, size=None): - samples_transformed = ((np.sinh( - (np.arcsinh(np.random.randn(*size)) + epsilon) / delta) - mean) / np.sqrt(var)) * sigma + mu - return samples_transformed - - return generate_samples(_random, mean=mean, var=var, mu=mu, sigma=sigma, epsilon=epsilon, delta=delta, - dist_shape=self.shape, - size=size) - - def logp(self, value): - mu = self.mu - sigma = self.sigma + tt.np.finfo(np.float32).eps - epsilon = self.epsilon - delta = self.delta + tt.np.finfo(np.float32).eps - mean = tt.sinh(epsilon/delta)*self.P(1/delta) - var = (tt.cosh(2*epsilon/delta)*self.P(2/delta)-1)/2 - tt.sqr(mean) - - # Here a double change of variables is applied - value_transformed = ((value - mu) / sigma) * tt.sqrt(var) + mean - - this_S = self.S(value_transformed) - this_S_sqr = tt.sqr(this_S) - this_C_sqr = 1+this_S_sqr - frac1 = -tt.log(tt.constant(2 * tt.np.pi))/2 - frac2 = tt.log(delta) + tt.log(this_C_sqr)/2 - tt.log(1 + tt.sqr(value_transformed)) / 2 - exp = -this_S_sqr / 2 - change_of_variable = tt.log(var)/2 - tt.log(sigma) - - # the change of variables is accounted for in the density by division and multiplication (addition and subtraction in the log domain) - return bound(frac1 + frac2 + exp + change_of_variable, delta > 0, sigma > 0, var > 0) diff --git a/build/lib/pcntoolkit/model/__init__.py b/build/lib/pcntoolkit/model/__init__.py deleted file mode 100644 index fe59b2d4..00000000 --- a/build/lib/pcntoolkit/model/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from . import bayesreg -from . import gp -from . import rfa -from . import architecture -from . import NP -from . import hbr diff --git a/build/lib/pcntoolkit/model/architecture.py b/build/lib/pcntoolkit/model/architecture.py deleted file mode 100644 index 569d4336..00000000 --- a/build/lib/pcntoolkit/model/architecture.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Fri Aug 30 09:45:35 2019 - -@author: seykia -""" - -import torch -from torch import nn -from torch.nn import functional as F -import numpy as np - -def compute_conv_out_size(d_in, h_in, w_in, padding, dilation, kernel_size, stride, UPorDW): - if UPorDW == 'down': - d_out = np.floor((d_in + 2 * padding[0] - dilation * (kernel_size - 1) - 1) / stride + 1) - h_out = np.floor((h_in + 2 * padding[1] - dilation * (kernel_size - 1) - 1) / stride + 1) - w_out = np.floor((w_in + 2 * padding[2] - dilation * (kernel_size - 1) - 1) / stride + 1) - elif UPorDW == 'up': - d_out = (d_in-1) * stride - 2 * padding[0] + dilation * (kernel_size - 1) + 1 - h_out = (h_in-1) * stride - 2 * padding[1] + dilation * (kernel_size - 1) + 1 - w_out = (w_in-1) * stride - 2 * padding[2] + dilation * (kernel_size - 1) + 1 - return d_out, h_out, w_out - -################################ ARCHITECTURES ################################ - -class Encoder(nn.Module): - def __init__(self, x, y, args): - super(Encoder, self).__init__() - self.r_dim = 25 - self.r_conv_dim = 100 - self.lrlu_neg_slope = 0.01 - self.dp_level = 0.1 - - self.factor=args.m - self.x_dim = x.shape[2] - - # Conv 1 - self.encoder_y_layer_1_conv = nn.Conv3d(in_channels = self.factor, out_channels=self.factor, - kernel_size=5, stride=2, padding=0, - dilation=1, groups=self.factor, bias=True) # in:(90,108,90) out:(43,52,43) - self.encoder_y_layer_1_bn = nn.BatchNorm3d(self.factor) - d_out_1, h_out_1, w_out_1 = compute_conv_out_size(y.shape[2], y.shape[3], - y.shape[4], padding=[0,0,0], - dilation=1, kernel_size=5, - stride=2, UPorDW='down') - - # Conv 2 - self.encoder_y_layer_2_conv = nn.Conv3d(in_channels=self.factor, out_channels=self.factor, - kernel_size=3, stride=2, padding=0, - dilation=1, groups=self.factor, bias=True) # out: (21,25,21) - self.encoder_y_layer_2_bn = nn.BatchNorm3d(self.factor) - d_out_2, h_out_2, w_out_2 = compute_conv_out_size(d_out_1, h_out_1, - w_out_1, padding=[0,0,0], - dilation=1, kernel_size=3, - stride=2, UPorDW='down') - - # Conv 3 - self.encoder_y_layer_3_conv = nn.Conv3d(in_channels=self.factor, out_channels=self.factor, - kernel_size=3, stride=2, padding=0, - dilation=1, groups=self.factor, bias=True) # out: (10,12,10) - self.encoder_y_layer_3_bn = nn.BatchNorm3d(self.factor) - d_out_3, h_out_3, w_out_3 = compute_conv_out_size(d_out_2, h_out_2, - w_out_2, padding=[0,0,0], - dilation=1, kernel_size=3, - stride=2, UPorDW='down') - - # Conv 4 - self.encoder_y_layer_4_conv = nn.Conv3d(in_channels=self.factor, out_channels=1, - kernel_size=3, stride=2, padding=0, - dilation=1, groups=1, bias=True) # out: (4,5,4) - self.encoder_y_layer_4_bn = nn.BatchNorm3d(1) - d_out_4, h_out_4, w_out_4 = compute_conv_out_size(d_out_3, h_out_3, - w_out_3, padding=[0,0,0], - dilation=1, kernel_size=3, - stride=2, UPorDW='down') - self.cnn_feature_num = [1, int(d_out_4), int(h_out_4), int(w_out_4)] - - # FC 5 - self.encoder_y_layer_5_dp = nn.Dropout(p = self.dp_level) - self.encoder_y_layer_5_linear = nn.Linear(int(np.prod(self.cnn_feature_num)), self.r_conv_dim) - - # FC 6 - self.encoder_xy_layer_6_dp = nn.Dropout(p = self.dp_level) - self.encoder_xy_layer_6_linear = nn.Linear(self.r_conv_dim + self.x_dim, 50) - - # FC 7 - self.encoder_xy_layer_7_dp = nn.Dropout(p = self.dp_level) - self.encoder_xy_layer_7_linear = nn.Linear(50, self.r_dim) - - def forward(self, x, y): - y = F.leaky_relu(self.encoder_y_layer_1_bn( - self.encoder_y_layer_1_conv(y)), self.lrlu_neg_slope) - y = F.leaky_relu(self.encoder_y_layer_2_bn( - self.encoder_y_layer_2_conv(y)),self.lrlu_neg_slope) - y = F.leaky_relu(self.encoder_y_layer_3_bn( - self.encoder_y_layer_3_conv(y)),self.lrlu_neg_slope) - y = F.leaky_relu(self.encoder_y_layer_4_bn( - self.encoder_y_layer_4_conv(y)),self.lrlu_neg_slope) - y = F.leaky_relu(self.encoder_y_layer_5_linear(self.encoder_y_layer_5_dp( - y.view(y.shape[0], np.prod(self.cnn_feature_num)))), self.lrlu_neg_slope) - x_y = torch.cat((y, torch.mean(x, dim=1)), 1) - x_y = F.leaky_relu(self.encoder_xy_layer_6_linear( - self.encoder_xy_layer_6_dp(x_y)),self.lrlu_neg_slope) - x_y = F.leaky_relu(self.encoder_xy_layer_7_linear( - self.encoder_xy_layer_7_dp(x_y)),self.lrlu_neg_slope) - return x_y - - -class Decoder(nn.Module): - def __init__(self, x, y, args): - super(Decoder, self).__init__() - self.r_dim = 25 - self.r_conv_dim = 100 - self.lrlu_neg_slope = 0.01 - self.dp_level = 0.1 - self.z_dim = 10 - self.x_dim = x.shape[2] - self.cnn_feature_num = args.cnn_feature_num - self.factor=args.m - - # FC 1 - self.decoder_zx_layer_1_dp = nn.Dropout(p = self.dp_level) - self.decoder_zx_layer_1_linear = nn.Linear(self.z_dim + self.x_dim, 50) - - # FC 2 - self.decoder_zx_layer_2_dp = nn.Dropout(p = self.dp_level) - self.decoder_zx_layer_2_linear = nn.Linear(50, int(np.prod(self.cnn_feature_num))) - - # Iconv 1 - self.decoder_zx_layer_1_iconv = nn.ConvTranspose3d(in_channels=1, out_channels=self.factor, - kernel_size=3, stride=1, - padding=0, output_padding=(0,0,0), - groups=1, bias=True, dilation=1) - self.decoder_zx_layer_1_bn = nn.BatchNorm3d(self.factor) - d_out_4, h_out_4, w_out_4 = compute_conv_out_size(args.cnn_feature_num[1]*2, - args.cnn_feature_num[2]*2, - args.cnn_feature_num[3]*2, - padding=[0,0,0], - dilation=1, kernel_size=3, - stride=1, UPorDW='up') - - # Iconv 2 - self.decoder_zx_layer_2_iconv = nn.ConvTranspose3d(in_channels=self.factor, out_channels=self.factor, - kernel_size=3, stride=1, padding=0, - output_padding=(0,0,0), groups=self.factor, - bias=True, dilation=1) - self.decoder_zx_layer_2_bn = nn.BatchNorm3d(self.factor) - d_out_3, h_out_3, w_out_3 = compute_conv_out_size(d_out_4*2, - h_out_4*2, - w_out_4*2, - padding=[0,0,0], - dilation=1, kernel_size=3, - stride=1, UPorDW='up') - # Iconv 3 - self.decoder_zx_layer_3_iconv = nn.ConvTranspose3d(in_channels=self.factor, out_channels=self.factor, - kernel_size=3, stride=1, padding=0, - output_padding=(0,0,0), groups=self.factor, - bias=True, dilation=1) - self.decoder_zx_layer_3_bn = nn.BatchNorm3d(self.factor) - d_out_2, h_out_2, w_out_2 = compute_conv_out_size(d_out_3*2, - h_out_3*2, - w_out_3*2, - padding=[0,0,0], - dilation=1, kernel_size=3, - stride=1, UPorDW='up') - - # Iconv 4 - self.decoder_zx_layer_4_iconv = nn.ConvTranspose3d(in_channels=self.factor, out_channels=1, - kernel_size=3, stride=1, padding=(0,0,0), - output_padding= (0,0,0), groups=1, - bias=True, dilation=1) - d_out_1, h_out_1, w_out_1 = compute_conv_out_size(d_out_2*2, - h_out_2*2, - w_out_2*2, - padding=[0,0,0], - dilation=1, kernel_size=3, - stride=1, UPorDW='up') - - self.scaling = [y.shape[2]/d_out_1, y.shape[3]/h_out_1, - y.shape[4]/w_out_1] - - def forward(self, z_sample, x_target): - z_x = torch.cat([z_sample, torch.mean(x_target,dim=1)], dim=1) - z_x = F.leaky_relu(self.decoder_zx_layer_1_linear(self.decoder_zx_layer_1_dp(z_x)), - self.lrlu_neg_slope) - z_x = F.leaky_relu(self.decoder_zx_layer_2_linear(self.decoder_zx_layer_2_dp(z_x)), - self.lrlu_neg_slope) - z_x = z_x.view(x_target.shape[0], self.cnn_feature_num[0], self.cnn_feature_num[1], - self.cnn_feature_num[2], self.cnn_feature_num[3]) - z_x = F.leaky_relu(self.decoder_zx_layer_1_bn(self.decoder_zx_layer_1_iconv( - F.interpolate(z_x, scale_factor=2))), self.lrlu_neg_slope) - z_x = F.leaky_relu(self.decoder_zx_layer_2_bn(self.decoder_zx_layer_2_iconv( - F.interpolate(z_x, scale_factor=2))), self.lrlu_neg_slope) - z_x = F.leaky_relu(self.decoder_zx_layer_3_bn(self.decoder_zx_layer_3_iconv( - F.interpolate(z_x, scale_factor=2))), self.lrlu_neg_slope) - z_x = self.decoder_zx_layer_4_iconv(F.interpolate(z_x, scale_factor=2)) - y_hat = torch.sigmoid(F.interpolate(z_x, scale_factor=(self.scaling[0], - self.scaling[1],self.scaling[2]))) - return y_hat - \ No newline at end of file diff --git a/build/lib/pcntoolkit/model/bayesreg.py b/build/lib/pcntoolkit/model/bayesreg.py deleted file mode 100644 index c353081d..00000000 --- a/build/lib/pcntoolkit/model/bayesreg.py +++ /dev/null @@ -1,568 +0,0 @@ -from __future__ import print_function -from __future__ import division - -import numpy as np -from scipy import optimize , linalg -from scipy.linalg import LinAlgError - - -class BLR: - """Bayesian linear regression - - Estimation and prediction of Bayesian linear regression models - - Basic usage:: - - B = BLR() - hyp = B.estimate(hyp0, X, y) - ys,s2 = B.predict(hyp, X, y, Xs) - - where the variables are - - :param hyp: vector of hyperparmaters. - :param X: N x D data array - :param y: 1D Array of targets (length N) - :param Xs: Nte x D array of test cases - :param hyp0: starting estimates for hyperparameter optimisation - - :returns: * ys - predictive mean - * s2 - predictive variance - - The hyperparameters are:: - - hyp = ( log(beta), log(alpha) ) # hyp is a list or numpy array - - The implementation and notation mostly follows Bishop (2006). - The hyperparameter beta is the noise precision and alpha is the precision - over lengthscale parameters. This can be either a scalar variable (a - common lengthscale for all input variables), or a vector of length D (a - different lengthscale for each input variable, derived using an automatic - relevance determination formulation). These are estimated using conjugate - gradient optimisation of the marginal likelihood. - - Reference: - Bishop (2006) Pattern Recognition and Machine Learning, Springer - - Written by A. Marquand - """ - - def __init__(self, **kwargs): - # parse arguments - n_iter = kwargs.get('n_iter', 100) - tol = kwargs.get('tol', 1e-3) - verbose = kwargs.get('verbose', False) - var_groups = kwargs.get('var_groups', None) - var_covariates = kwargs.get('var_covariates', None) - warp = kwargs.get('warp', None) - warp_reparam = kwargs.get('warp_reparam', False) - - if var_groups is not None and var_covariates is not None: - raise ValueError("var_covariates and var_groups cannot both be used") - - # basic parameters - self.hyp = np.nan - self.nlZ = np.nan - self.tol = tol # not used at present - self.n_iter = n_iter - self.verbose = verbose - self.var_groups = var_groups - if var_covariates is not None: - self.hetero_var = True - else: - self.hetero_var = False - if self.var_groups is not None: - self.var_ids = set(self.var_groups) - self.var_ids = sorted(list(self.var_ids)) - - # set up warped likelihood - if verbose: - print('warp:', warp, 'warp_reparam:', warp_reparam) - if warp is None: - self.warp = None - self.n_warp_param = 0 - else: - self.warp = warp - self.n_warp_param = warp.get_n_params() - self.warp_reparam = warp_reparam - - self.gamma = None - - def _parse_hyps(self, hyp, X, Xv=None): - - N = X.shape[0] - - # noise precision - if Xv is not None: - if len(Xv.shape) == 1: - Dv = 1 - Xv = Xv[:, np.newaxis] - else: - Dv = Xv.shape[1] - w_d = np.asarray(hyp[0:Dv]) - beta = np.exp(Xv.dot(w_d)) - n_lik_param = len(w_d) - elif self.var_groups is not None: - beta = np.exp(hyp[0:len(self.var_ids)]) - n_lik_param = len(beta) - else: - beta = np.asarray([np.exp(hyp[0])]) - n_lik_param = len(beta) - - # parameters for warping the likelhood function - if self.warp is not None: - gamma = hyp[n_lik_param:(n_lik_param + self.n_warp_param)] - n_lik_param += self.n_warp_param - else: - gamma = None - - # precision for the coefficients - if isinstance(beta, list) or type(beta) is np.ndarray: - alpha = np.exp(hyp[n_lik_param:]) - else: - alpha = np.exp(hyp[1:]) - - # reparameterise the warp (WarpSinArcsinh only) - if self.warp is not None and self.warp_reparam: - delta = np.exp(gamma[1]) - beta = beta/(delta**2) - - # Create precision matrix from noise precision - if Xv is not None: - self.lambda_n_vec = beta - elif self.var_groups is not None: - beta_all = np.ones(N) - for v in range(len(self.var_ids)): - beta_all[self.var_groups == self.var_ids[v]] = beta[v] - self.lambda_n_vec = beta_all - else: - self.lambda_n_vec = np.ones(N)*beta - - return beta, alpha, gamma - - def post(self, hyp, X, y, Xv=None): - """ Generic function to compute posterior distribution. - - This function will save the posterior mean and precision matrix as - self.m and self.A and will also update internal parameters (e.g. - N, D and the prior covariance (Sigma_a) and precision (Lambda_a). - - :param hyp: hyperparameter vector - :param X: covariates - :param y: responses - :param Xv: covariates for heteroskedastic noise - """ - - N = X.shape[0] - if len(X.shape) == 1: - D = 1 - else: - D = X.shape[1] - - if (hyp == self.hyp).all() and hasattr(self, 'N'): - print("hyperparameters have not changed, exiting") - return - - beta, alpha, gamma = self._parse_hyps(hyp, X, Xv) - - if self.verbose: - print("estimating posterior ... | hyp=", hyp) - - # prior variance - if len(alpha) == 1 or len(alpha) == D: - self.Sigma_a = np.diag(np.ones(D))/alpha - self.Lambda_a = np.diag(np.ones(D))*alpha - else: - raise ValueError("hyperparameter vector has invalid length") - - # compute posterior precision and mean - # this is equivalent to the following operation but makes much more - # efficient use of memory by avoiding the need to store Lambda_n - # - # self.A = X.T.dot(self.Lambda_n).dot(X) + self.Lambda_a - # self.m = linalg.solve(self.A, X.T, - # check_finite=False).dot(self.Lambda_n).dot(y) - - XtLambda_n = X.T*self.lambda_n_vec - self.A = XtLambda_n.dot(X) + self.Lambda_a - invAXt = linalg.solve(self.A, X.T, check_finite=False) - self.m = (invAXt*self.lambda_n_vec).dot(y) - - # save stuff - self.N = N - self.D = D - self.hyp = hyp - - def loglik(self, hyp, X, y, Xv=None): - """ Function to compute compute log (marginal) likelihood """ - - # hyperparameters (alpha not needed) - beta, alpha, gamma = self._parse_hyps(hyp, X, Xv) - - # warp the likelihood? - if self.warp is not None: - if self.verbose: - print('warping input...') - y_unwarped = y - y = self.warp.f(y, gamma) - - # load posterior and prior covariance - if (hyp != self.hyp).any() or not(hasattr(self, 'A')): - try: - self.post(hyp, X, y, Xv) - except ValueError: - print("Warning: Estimation of posterior distribution failed") - nlZ = 1/np.finfo(float).eps - return nlZ - - try: - # compute the log determinants in a numerically stable way - logdetA = 2*sum(np.log(np.diag(np.linalg.cholesky(self.A)))) - except (ValueError, LinAlgError): - print("Warning: Estimation of posterior distribution failed") - nlZ = 1/np.finfo(float).eps - return nlZ - - logdetSigma_a = sum(np.log(np.diag(self.Sigma_a))) # diagonal - logdetSigma_n = sum(np.log(1/self.lambda_n_vec)) - - # compute negative marginal log likelihood - X_y_t_sLambda_n = (y-X.dot(self.m))*np.sqrt(self.lambda_n_vec) - nlZ = -0.5 * (-self.N*np.log(2*np.pi) - - logdetSigma_n - - logdetSigma_a - - X_y_t_sLambda_n.T.dot(X_y_t_sLambda_n) - - self.m.T.dot(self.Lambda_a).dot(self.m) - - logdetA - ) - - - if self.warp is not None: - # add in the Jacobian - nlZ = nlZ - sum(np.log(self.warp.df(y_unwarped, gamma))) - - # make sure the output is finite to stop the minimizer getting upset - if not np.isfinite(nlZ): - nlZ = 1/np.finfo(float).eps - - if self.verbose: - print("nlZ= ", nlZ, " | hyp=", hyp) - - self.nlZ = nlZ - return nlZ - - def penalized_loglik(self, hyp, X, y, Xv=None, l=0.1, norm='L1'): - """ Function to compute the penalized log (marginal) likelihood - - :param hyp: hyperparameter vector - :param X: covariates - :param y: responses - :param Xv: covariates for heteroskedastic noise - :param l: regularisation penalty - :param norm: type of regulariser (L1 or L2) - """ - - if norm.lower() == 'l1': - L = self.loglik(hyp, X, y, Xv) + l * sum(abs(hyp)) - elif norm.lower() == 'l2': - L = self.loglik(hyp, X, y, Xv) + l * sum(np.sqrt(hyp**2)) - else: - print("Requested penalty not recognized, choose between 'L1' or 'L2'.") - return L - - def dloglik(self, hyp, X, y, Xv=None): - """ Function to compute derivatives """ - - # hyperparameters - beta, alpha, gamma = self._parse_hyps(hyp, X, Xv) - - if self.warp is not None: - raise ValueError('optimization with derivatives is not yet ' + \ - 'supported for warped liklihood') - - # load posterior and prior covariance - if (hyp != self.hyp).any() or not(hasattr(self, 'A')): - try: - self.post(hyp, X, y, Xv) - except ValueError: - print("Warning: Estimation of posterior distribution failed") - dnlZ = np.sign(self.dnlZ) / np.finfo(float).eps - return dnlZ - - # precompute re-used quantities to maximise speed - # todo: revise implementation to use Cholesky throughout - # that would remove the need to explicitly compute the inverse - S = np.linalg.inv(self.A) # posterior covariance - SX = S.dot(X.T) - XLn = X.T*self.lambda_n_vec # = X.T.dot(self.Lambda_n) - XLny = XLn.dot(y) - SXLny = S.dot(XLny) - XLnXm = XLn.dot(X).dot(self.m) - - # initialise derivatives - dnlZ = np.zeros(hyp.shape) - dnl2 = np.zeros(hyp.shape) - - # noise precision parameter(s) - for i in range(0, len(beta)): - # first compute derivative of Lambda_n with respect to beta - dL_n_vec = np.zeros(self.N) - if self.var_groups is None: - dL_n_vec = np.ones(self.N) - else: - dL_n_vec[np.where(self.var_groups == self.var_ids[i])[0]] = 1 - dLambda_n = np.diag(dL_n_vec) - - # compute quantities used multiple times - XdLnX = X.T.dot(dLambda_n).dot(X) - dA = XdLnX - - # derivative of posterior parameters with respect to beta - b = -S.dot(dA).dot(SXLny) + SX.dot(dLambda_n).dot(y) - - # compute np.trace(self.Sigma_n.dot(dLambda_n)) efficiently - trSigma_ndLambda_n = sum((1/self.lambda_n_vec)*np.diag(dLambda_n)) - - # compute y.T.dot(Lambda_n) efficiently - ytLn = (y*self.lambda_n_vec).T - - # compute derivatives - dnlZ[i] = - (0.5 * trSigma_ndLambda_n - - 0.5 * y.dot(dLambda_n).dot(y) + - y.dot(dLambda_n).dot(X).dot(self.m) + - ytLn.dot(X).dot(b) - - 0.5 * self.m.T.dot(XdLnX).dot(self.m) - - b.T.dot(XLnXm) - - b.T.dot(self.Lambda_a).dot(self.m) - - 0.5 * np.trace(S.dot(dA)) - ) * beta[i] - - # scaling parameter(s) - for i in range(0, len(alpha)): - # first compute derivatives with respect to alpha - if len(alpha) == self.D: # are we using ARD? - dLambda_a = np.zeros((self.D, self.D)) - dLambda_a[i, i] = 1 - else: - dLambda_a = np.eye(self.D) - - F = dLambda_a - c = -S.dot(F).dot(SXLny) - - # compute np.trace(self.Sigma_a.dot(dLambda_a)) efficiently - trSigma_adLambda_a = sum(np.diag(self.Sigma_a)*np.diag(dLambda_a)) - - dnlZ[i+len(beta)] = -(0.5* trSigma_adLambda_a + - XLny.T.dot(c) - - c.T.dot(XLnXm) - - c.T.dot(self.Lambda_a).dot(self.m) - - 0.5 * self.m.T.dot(F).dot(self.m) - - 0.5*np.trace(linalg.solve(self.A, F)) - ) * alpha[i] - - # make sure the gradient is finite to stop the minimizer getting upset - if not all(np.isfinite(dnlZ)): - bad = np.where(np.logical_not(np.isfinite(dnlZ))) - for b in bad: - dnlZ[b] = np.sign(self.dnlZ[b]) / np.finfo(float).eps - - if self.verbose: - print("dnlZ= ", dnlZ, " | hyp=", hyp) - - self.dnlZ = dnlZ - return dnlZ - - # model estimation (optimization) - def estimate(self, hyp0, X, y, **kwargs): - """ Function to estimate the model - - :param hyp: hyperparameter vector - :param X: covariates - :param y: responses - :param optimizer: optimisation algorithm ('cg','powell','nelder-mead','l0bfgs-b') - """ - - optimizer = kwargs.get('optimizer','cg') - - # covariates for heteroskedastic noise - Xv = kwargs.get('var_covariates', None) - - # options for l-bfgs-b - l = float(kwargs.get('l', 0.1)) - epsilon = float(kwargs.get('epsilon', 0.1)) - norm = kwargs.get('norm', 'l2') - - if optimizer.lower() == 'cg': # conjugate gradients - out = optimize.fmin_cg(self.loglik, hyp0, self.dloglik, (X, y, Xv), - disp=True, gtol=self.tol, - maxiter=self.n_iter, full_output=1) - elif optimizer.lower() == 'powell': # Powell's method - out = optimize.fmin_powell(self.loglik, hyp0, (X, y, Xv), - full_output=1) - elif optimizer.lower() == 'nelder-mead': - out = optimize.fmin(self.loglik, hyp0, (X, y, Xv), - full_output=1) - elif optimizer.lower() == 'l-bfgs-b': - all_hyp_i = [hyp0] - def store(X): - hyp = X - all_hyp_i.append(hyp) - try: - out = optimize.fmin_l_bfgs_b(self.penalized_loglik, x0=hyp0, - args=(X, y, Xv, l, norm), approx_grad=True, - epsilon=epsilon, callback=store) - # If the matrix becomes singular restart at last found hyp - except np.linalg.LinAlgError: - print(f'Restarting estimation at hyp = {all_hyp_i[-1]}, due to *** numpy.linalg.LinAlgError: Matrix is singular.') - out = optimize.fmin_l_bfgs_b(self.penalized_loglik, x0=all_hyp_i[-1], - args=(X, y, Xv, l, norm), approx_grad=True, - epsilon=epsilon) - else: - raise ValueError("unknown optimizer") - - self.hyp = out[0] - self.nlZ = out[1] - self.optimizer = optimizer - - return self.hyp - - def predict(self, hyp, X, y, Xs, - var_groups_test=None, - var_covariates_test=None, **kwargs): - """ Function to make predictions from the model - - :param hyp: hyperparameter vector - :param X: covariates for training data - :param y: responses for training data - :param Xs: covariates for test data - :param var_covariates_test: test covariates for heteroskedastic noise - - This always returns Gaussian predictions, i.e. - - :returns: * ys - predictive mean - * s2 - predictive variance - """ - - Xvs = var_covariates_test - if Xvs is not None and len(Xvs.shape) == 1: - Xvs = Xvs[:, np.newaxis] - - if X is None or y is None: - # set dummy hyperparameters - beta, alpha, gamma = self._parse_hyps(hyp, np.zeros((self.N, self.D)), Xvs) - else: - - # set hyperparameters - beta, alpha, gamma = self._parse_hyps(hyp, X, Xvs) - - # do we need to re-estimate the posterior? - if (hyp != self.hyp).any() or not(hasattr(self, 'A')): - raise(ValueError, 'posterior not properly estimated') - - N_test = Xs.shape[0] - - ys = Xs.dot(self.m) - - if self.var_groups is not None: - if len(var_groups_test) != N_test: - raise(ValueError, 'Invalid variance groups for test') - # separate variance groups - s2n = np.ones(N_test) - for v in range(len(self.var_ids)): - s2n[var_groups_test == self.var_ids[v]] = 1/beta[v] - else: - s2n = 1/beta - - # compute xs.dot(S).dot(xs.T) avoiding computing off-diagonal entries - s2 = s2n + np.sum(Xs*linalg.solve(self.A, Xs.T).T, axis=1) - - return ys, s2 - - def predict_and_adjust(self, hyp, X, y, Xs=None, - ys=None, - var_groups_test=None, - var_groups_adapt=None, **kwargs): - """ Function to transfer the model to a new site. This is done by - first making predictions on the adaptation data given by X, - adjusting by the residuals with respect to y. - - :param hyp: hyperparameter vector - :param X: covariates for adaptation (i.e. calibration) data - :param y: responses for adaptation data - :param Xs: covariate data (for which predictions should be adjusted) - :param ys: true response variables (to be adjusted) - :param var_groups_test: variance groups (e.g. sites) for test data - :param var_groups_adapt: variance groups for adaptation data - - There are two possible ways of using this function, depending on - whether ys or Xs is specified - - If ys is specified, this is applied directly to the data, which is - assumed to be in the input space (i.e. not warped). In this case - the adjusted true data points are returned in the same space - - Alternatively, Xs is specified, then the predictions are made and - adjusted. In this case the predictive variance are returned in the - warped (i.e. Gaussian) space. - - This function needs to know which sites are associated with which - data points, which provided by var_groups_xxx, which is a list or - array of scalar ids . - """ - - if ys is None: - if Xs is None: - raise ValueError('Either ys or Xs must be specified') - else: - N = Xs.shape[0] - else: - if len(ys.shape) < 1: - raise ValueError('ys is specified but has insufficent length') - N = ys.shape[0] - - if var_groups_test is None: - var_groups_test = np.ones(N) - var_groups_adapt = np.ones(X.shape[0]) - - ys_out = np.zeros(N) - s2_out = np.zeros(N) - for g in np.unique(var_groups_test): - idx_s = var_groups_test == g - idx_a = var_groups_adapt == g - - if sum(idx_a) < 2: - raise ValueError('Insufficient adaptation data to estimate variance') - - # Get predictions from old model on new data X - ys_ref, s2_ref = self.predict(hyp, None, None, X[idx_a,:]) - - # Subtract the predictions from true data to get the residuals - if self.warp is None: - residuals = ys_ref-y[idx_a] - else: - # Calculate the residuals in warped space - y_ref_ws = self.warp.f(y[idx_a], hyp[1:self.warp.get_n_params()+1]) - residuals = ys_ref - y_ref_ws - - residuals_mu = np.mean(residuals) - residuals_sd = np.std(residuals) - - # Adjust the mean with the mean of the residuals - if ys is None: - # make and adjust predictions - ys_out[idx_s], s2_out[idx_s] = self.predict(hyp, None, None, Xs[idx_s,:]) - ys_out[idx_s] = ys_out[idx_s] - residuals_mu - - # Set the deviation to the devations of the residuals - s2_out[idx_s] = np.ones(len(s2_out[idx_s]))*residuals_sd**2 - else: - # adjust the data - if self.warp is not None: - y_ws = self.warp.f(ys[idx_s], hyp[1:self.warp.get_n_params()+1]) - ys_out[idx_s] = y_ws + residuals_mu - ys_out[idx_s] = self.warp.invf(ys_out[idx_s], hyp[1:self.warp.get_n_params()+1]) - else: - ys = ys - residuals_mu - s2_out = None - - return ys_out, s2_out - diff --git a/build/lib/pcntoolkit/model/gp.py b/build/lib/pcntoolkit/model/gp.py deleted file mode 100644 index b2fb1b75..00000000 --- a/build/lib/pcntoolkit/model/gp.py +++ /dev/null @@ -1,488 +0,0 @@ -from __future__ import print_function -from __future__ import division - -import os -import sys -import numpy as np -from scipy import optimize -from numpy.linalg import solve, LinAlgError -from numpy.linalg import cholesky as chol -from six import with_metaclass -from abc import ABCMeta, abstractmethod - - -try: # Run as a package if installed - from pcntoolkit.util.utils import squared_dist -except ImportError: - pass - - path = os.path.abspath(os.path.dirname(__file__)) - path = os.path.dirname(path) # parent directory - if path not in sys.path: - sys.path.append(path) - del path - - from util.utils import squared_dist - -# -------------------- -# Covariance functions -# -------------------- - - -class CovBase(with_metaclass(ABCMeta)): - """ Base class for covariance functions. - - All covariance functions must define the following methods:: - - CovFunction.get_n_params() - CovFunction.cov() - CovFunction.xcov() - CovFunction.dcov() - """ - - def __init__(self, x=None): - self.n_params = np.nan - - def get_n_params(self): - """ Report the number of parameters required """ - - assert not np.isnan(self.n_params), \ - "Covariance function not initialised" - - return self.n_params - - @abstractmethod - def cov(self, theta, x, z=None): - """ Return the full covariance (or cross-covariance if z is given) """ - - @abstractmethod - def dcov(self, theta, x, i): - """ Return the derivative of the covariance function with respect to - the i-th hyperparameter """ - - -class CovLin(CovBase): - """ Linear covariance function (no hyperparameters) - """ - - def __init__(self, x=None): - self.n_params = 0 - self.first_call = False - - 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: - print("CovLin: ignoring unnecessary hyperparameter ...") - - if z is None: - z = x - - K = x.dot(z.T) - return K - - def dcov(self, theta, x, i): - raise ValueError("Invalid covariance function parameter") - - -class CovSqExp(CovBase): - """ Ordinary squared exponential covariance function. - The hyperparameters are:: - - theta = ( log(ell), log(sf) ) - - where ell is a lengthscale parameter and sf2 is the signal variance - """ - - def __init__(self, x=None): - self.n_params = 2 - - def cov(self, theta, x, z=None): - self.ell = np.exp(theta[0]) - self.sf2 = np.exp(2*theta[1]) - - if z is None: - z = x - - R = squared_dist(x/self.ell, z/self.ell) - K = self.sf2 * np.exp(-R/2) - return K - - def dcov(self, theta, x, i): - self.ell = np.exp(theta[0]) - self.sf2 = np.exp(2*theta[1]) - - R = squared_dist(x/self.ell, x/self.ell) - - if i == 0: # return derivative of lengthscale parameter - dK = self.sf2 * np.exp(-R/2) * R - return dK - elif i == 1: # return derivative of signal variance parameter - dK = 2*self.sf2 * np.exp(-R/2) - return dK - else: - raise ValueError("Invalid covariance function parameter") - - -class CovSqExpARD(CovBase): - """ Squared exponential covariance function with ARD - The hyperparameters are:: - - theta = (log(ell_1, ..., log_ell_D), log(sf)) - - where ell_i are lengthscale parameters and sf2 is the signal variance - """ - - def __init__(self, x=None): - if x is None: - raise ValueError("N x D data matrix must be supplied as input") - if len(x.shape) == 1: - self.D = 1 - else: - self.D = x.shape[1] - self.n_params = self.D + 1 - - def cov(self, theta, x, z=None): - self.ell = np.exp(theta[0:self.D]) - self.sf2 = np.exp(2*theta[self.D]) - - if z is None: - z = x - - R = squared_dist(x.dot(np.diag(1./self.ell)), - z.dot(np.diag(1./self.ell))) - K = self.sf2*np.exp(-R/2) - return K - - 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]) - return dK - elif i == self.D: # return derivative of signal variance parameter - dK = 2*K - return dK - else: - raise ValueError("Invalid covariance function parameter") - - -class CovSum(CovBase): - """ Sum of covariance functions. These are passed in as a cell array and - intialised automatically. For example:: - - C = CovSum(x,(CovLin, CovSqExpARD)) - C = CovSum.cov(x, ) - - The hyperparameters are:: - - theta = ( log(ell_1, ..., log_ell_D), log(sf2) ) - - where ell_i are lengthscale parameters and sf2 is the signal variance - """ - - def __init__(self, x=None, covfuncnames=None): - if x is None: - raise ValueError("N x D data matrix must be supplied as input") - if covfuncnames is None: - raise ValueError("A list of covariance functions is required") - self.covfuncs = [] - self.n_params = 0 - for cname in covfuncnames: - covfunc = eval(cname + '(x)') - self.n_params += covfunc.get_n_params() - self.covfuncs.append(covfunc) - - if len(x.shape) == 1: - self.N = len(x) - self.D = 1 - else: - self.N, self.D = x.shape - - def cov(self, theta, x, z=None): - theta_offset = 0 - for ci, covfunc in enumerate(self.covfuncs): - try: - n_params_c = covfunc.get_n_params() - theta_c = [theta[c] for c in - range(theta_offset, theta_offset + n_params_c)] - theta_offset += n_params_c - except Exception as e: - print(e) - - if ci == 0: - K = covfunc.cov(theta_c, x, z) - else: - K += covfunc.cov(theta_c, x, z) - return K - - def dcov(self, theta, x, i): - theta_offset = 0 - for covfunc in self.covfuncs: - n_params_c = covfunc.get_n_params() - theta_c = [theta[c] for c in - range(theta_offset, theta_offset + n_params_c)] - theta_offset += n_params_c - - if theta_c: # does the variable have any hyperparameters? - if 'dK' not in locals(): - dK = covfunc.dcov(theta_c, x, i) - else: - dK += covfunc.dcov(theta_c, x, i) - return dK - -# ----------------------- -# Gaussian process models -# ----------------------- - - -class GPR: - """Gaussian process regression - - Estimation and prediction of Gaussian process regression models - - Basic usage:: - - G = GPR() - hyp = B.estimate(hyp0, cov, X, y) - ys, ys2 = B.predict(hyp, cov, X, y, Xs) - - where the variables are - - :param hyp: vector of hyperparmaters - :param cov: covariance function - :param X: N x D data array - :param y: 1D Array of targets (length N) - :param Xs: Nte x D array of test cases - :param hyp0: starting estimates for hyperparameter optimisation - - :returns: * ys - predictive mean - * ys2 - predictive variance - - The hyperparameters are:: - - hyp = ( log(sn), (cov function params) ) # hyp is a list or array - - The implementation and notation follows Rasmussen and Williams (2006). - As in the gpml toolbox, these parameters are estimated using conjugate - gradient optimisation of the marginal likelihood. Note that there is no - explicit mean function, thus the gpr routines are limited to modelling - zero-mean processes. - - Reference: - C. Rasmussen and C. Williams (2006) Gaussian Processes for Machine Learning - - Written by A. Marquand - """ - - def __init__(self, hyp=None, covfunc=None, X=None, y=None, n_iter=100, - tol=1e-3, verbose=False, warp=None): - - self.hyp = np.nan - self.nlZ = np.nan - self.tol = tol # not used at present - self.n_iter = n_iter - self.verbose = verbose - - # set up warped likelihood - if warp is None: - self.warp = None - self.n_warp_param = 0 - else: - self.warp = warp - self.n_warp_param = warp.get_n_params() - - self.gamma = None - - def _updatepost(self, hyp, covfunc): - - hypeq = np.asarray(hyp == self.hyp) - if hypeq.all() and hasattr(self, 'alpha') and \ - (hasattr(self, 'covfunc') and covfunc == self.covfunc): - return False - else: - return True - - def post(self, hyp, covfunc, X, y): - """ Generic function to compute posterior distribution. - """ - - if len(hyp.shape) > 1: # force 1d hyperparameter array - hyp = hyp.flatten() - - if len(X.shape) == 1: - X = X[:, np.newaxis] - self.N, self.D = X.shape - - # hyperparameters - sn2 = np.exp(2*hyp[0]) # noise variance - if self.warp is not None: # parameters for warping the likelhood - n_lik_param = self.n_warp_param+1 - else: - n_lik_param = 1 - theta = hyp[n_lik_param:] # (generic) covariance hyperparameters - - if self.verbose: - print("estimating posterior ... | hyp=", hyp) - - self.K = covfunc.cov(theta, X) - self.L = chol(self.K + sn2*np.eye(self.N)) - self.alpha = solve(self.L.T, solve(self.L, y)) - self.hyp = hyp - self.covfunc = covfunc - - def loglik(self, hyp, covfunc, X, y): - """ Function to compute compute log (marginal) likelihood - """ - - # load or recompute posterior - if self.verbose: - print("computing likelihood ... | hyp=", hyp) - - # parameters for warping the likelhood function - if self.warp is not None: - gamma = hyp[1:(self.n_warp_param+1)] - y = self.warp.f(y, gamma) - y_unwarped = y - - if len(hyp.shape) > 1: # force 1d hyperparameter array - hyp = hyp.flatten() - if self._updatepost(hyp, covfunc): - try: - self.post(hyp, covfunc, X, y) - except (ValueError, LinAlgError): - print("Warning: Estimation of posterior distribution failed") - self.nlZ = 1/np.finfo(float).eps - return self.nlZ - - self.nlZ = 0.5*y.T.dot(self.alpha) + sum(np.log(np.diag(self.L))) + \ - 0.5*self.N*np.log(2*np.pi) - - if self.warp is not None: - # add in the Jacobian - self.nlZ = self.nlZ - sum(np.log(self.warp.df(y_unwarped, gamma))) - - # make sure the output is finite to stop the minimizer getting upset - if not np.isfinite(self.nlZ): - self.nlZ = 1/np.finfo(float).eps - - if self.verbose: - print("nlZ= ", self.nlZ, " | hyp=", hyp) - - return self.nlZ - - def dloglik(self, hyp, covfunc, X, y): - """ Function to compute derivatives - """ - - if len(hyp.shape) > 1: # force 1d hyperparameter array - hyp = hyp.flatten() - - if self.warp is not None: - raise ValueError('optimization with derivatives is not yet ' + \ - 'supported for warped liklihood') - - # hyperparameters - sn2 = np.exp(2*hyp[0]) # noise variance - theta = hyp[1:] # (generic) covariance hyperparameters - - # load posterior and prior covariance - if self._updatepost(hyp, covfunc): - try: - self.post(hyp, covfunc, X, y) - except (ValueError, LinAlgError): - print("Warning: Estimation of posterior distribution failed") - dnlZ = np.sign(self.dnlZ) / np.finfo(float).eps - return dnlZ - - # compute Q = alpha*alpha' - inv(K) - Q = np.outer(self.alpha, self.alpha) - \ - solve(self.L.T, solve(self.L, np.eye(self.N))) - - # initialise derivatives - self.dnlZ = np.zeros(len(hyp)) - - # noise variance - self.dnlZ[0] = -sn2*np.trace(Q) - - # covariance parameter(s) - for par in range(0, len(theta)): - # compute -0.5*trace(Q.dot(dK/d[theta_i])) efficiently - dK = covfunc.dcov(theta, X, i=par) - self.dnlZ[par+1] = -0.5*np.sum(np.sum(Q*dK.T)) - - # make sure the gradient is finite to stop the minimizer getting upset - if not all(np.isfinite(self.dnlZ)): - bad = np.where(np.logical_not(np.isfinite(self.dnlZ))) - for b in bad: - self.dnlZ[b] = np.sign(self.dnlZ[b]) / np.finfo(float).eps - - if self.verbose: - print("dnlZ= ", self.dnlZ, " | hyp=", hyp) - - return self.dnlZ - - # model estimation (optimization) - def estimate(self, hyp0, covfunc, X, y, optimizer='cg'): - """ Function to estimate the model - """ - if len(X.shape) == 1: - X = X[:, np.newaxis] - - self.hyp0 = hyp0 - - if optimizer.lower() == 'cg': # conjugate gradients - out = optimize.fmin_cg(self.loglik, hyp0, self.dloglik, - (covfunc, X, y), disp=True, gtol=self.tol, - maxiter=self.n_iter, full_output=1) - - elif optimizer.lower() == 'powell': # Powell's method - out = optimize.fmin_powell(self.loglik, hyp0, (covfunc, X, y), - full_output=1) - else: - raise ValueError("unknown optimizer") - - # Always return a 1d array. The optimizer sometimes changes dimesnions - if len(out[0].shape) > 1: - self.hyp = out[0].flatten() - else: - self.hyp = out[0] - self.nlZ = out[1] - self.optimizer = optimizer - - return self.hyp - - def predict(self, hyp, X, y, Xs): - """ Function to make predictions from the model - """ - if len(hyp.shape) > 1: # force 1d hyperparameter array - hyp = hyp.flatten() - - # ensure X and Xs are multi-dimensional arrays - if len(Xs.shape) == 1: - Xs = Xs[:, np.newaxis] - if len(X.shape) == 1: - X = X[:, np.newaxis] - - # parameters for warping the likelhood function - if self.warp is not None: - gamma = hyp[1:(self.n_warp_param+1)] - y = self.warp.f(y, gamma) - - # reestimate posterior (avoids numerical problems with optimizer) - self.post(hyp, self.covfunc, X, y) - - # hyperparameters - sn2 = np.exp(2*hyp[0]) # noise variance - theta = hyp[(self.n_warp_param + 1):] # (generic) covariance hyperparameters - - Ks = self.covfunc.cov(theta, Xs, X) - kss = self.covfunc.cov(theta, Xs) - - # predictive mean - ymu = Ks.dot(self.alpha) - - # predictive variance (for a noisy test input) - v = solve(self.L, Ks.T) - ys2 = kss - v.T.dot(v) + sn2 - - return ymu, ys2 diff --git a/build/lib/pcntoolkit/model/hbr.py b/build/lib/pcntoolkit/model/hbr.py deleted file mode 100644 index 18ed67e2..00000000 --- a/build/lib/pcntoolkit/model/hbr.py +++ /dev/null @@ -1,923 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Thu Jul 25 13:23:15 2019 - -@author: seykia -@author: augub -""" - -from __future__ import print_function -from __future__ import division -from ast import Param -from tkinter.font import names - -import numpy as np -import pymc3 as pm -import theano -from itertools import product -from functools import reduce - -import theano -from pymc3 import Metropolis, NUTS, Slice, HamiltonianMC -from scipy import stats -import bspline -from bspline import splinelab - -from model.SHASH import SHASHo2, SHASHb, SHASHo -from util.utils import create_poly_basis -from util.utils import expand_all -from pcntoolkit.util.utils import cartesian_product - -from theano import printing, function - -def bspline_fit(X, order, nknots): - feature_num = X.shape[1] - bsp_basis = [] - - for i in range(feature_num): - minx = np.min(X[:,i]) - maxx = np.max(X[:,i]) - delta = maxx-minx - # Expand range by 20% (10% on both sides) - splinemin = minx-0.1*delta - splinemax = maxx+0.1*delta - knots = np.linspace(splinemin, splinemax, nknots) - k = splinelab.augknt(knots, order) - bsp_basis.append(bspline.Bspline(k, order)) - - return bsp_basis - -def bspline_transform(X, bsp_basis): - if type(bsp_basis) != list: - temp = [] - temp.append(bsp_basis) - bsp_basis = temp - - feature_num = len(bsp_basis) - X_transformed = [] - for f in range(feature_num): - X_transformed.append(np.array([bsp_basis[f](i) for i in X[:, f]])) - X_transformed = np.concatenate(X_transformed, axis=1) - - return X_transformed - -def create_poly_basis(X, order): - """ compute a polynomial basis expansion of the specified order""" - - if len(X.shape) == 1: - X = X[:, np.newaxis] - D = X.shape[1] - Phi = np.zeros((X.shape[0], D * order)) - colid = np.arange(0, D) - for d in range(1, order + 1): - Phi[:, colid] = X ** d - colid += D - - return Phi - - -def from_posterior(param, samples, distribution=None, half=False, freedom=1): - if len(samples.shape) > 1: - shape = samples.shape[1:] - else: - shape = None - - if (distribution is None): - smin, smax = np.min(samples), np.max(samples) - width = smax - smin - x = np.linspace(smin, smax, 1000) - y = stats.gaussian_kde(np.ravel(samples))(x) - if half: - x = np.concatenate([x, [x[-1] + 0.1 * width]]) - y = np.concatenate([y, [0]]) - else: - x = np.concatenate([[x[0] - 0.1 * width], x, [x[-1] + 0.1 * width]]) - y = np.concatenate([[0], y, [0]]) - if shape is None: - return pm.distributions.Interpolated(param, x, y) - else: - return pm.distributions.Interpolated(param, x, y, shape=shape) - elif (distribution == 'normal'): - temp = stats.norm.fit(samples) - if shape is None: - return pm.Normal(param, mu=temp[0], sigma=freedom * temp[1]) - else: - return pm.Normal(param, mu=temp[0], sigma=freedom * temp[1], shape=shape) - elif (distribution == 'hnormal'): - temp = stats.halfnorm.fit(samples) - if shape is None: - return pm.HalfNormal(param, sigma=freedom * temp[1]) - else: - return pm.HalfNormal(param, sigma=freedom * temp[1], shape=shape) - elif (distribution == 'hcauchy'): - temp = stats.halfcauchy.fit(samples) - if shape is None: - return pm.HalfCauchy(param, freedom * temp[1]) - else: - return pm.HalfCauchy(param, freedom * temp[1], shape=shape) - elif (distribution == 'uniform'): - upper_bound = np.percentile(samples, 95) - lower_bound = np.percentile(samples, 5) - r = np.abs(upper_bound - lower_bound) - if shape is None: - return pm.Uniform(param, lower=lower_bound - freedom * r, - upper=upper_bound + freedom * r) - else: - return pm.Uniform(param, lower=lower_bound - freedom * r, - upper=upper_bound + freedom * r, shape=shape) - elif (distribution == 'huniform'): - upper_bound = np.percentile(samples, 95) - lower_bound = np.percentile(samples, 5) - r = np.abs(upper_bound - lower_bound) - if shape is None: - return pm.Uniform(param, lower=0, upper=upper_bound + freedom * r) - else: - return pm.Uniform(param, lower=0, upper=upper_bound + freedom * r, shape=shape) - - elif (distribution == 'gamma'): - alpha_fit, loc_fit, invbeta_fit = stats.gamma.fit(samples) - if shape is None: - return pm.Gamma(param, alpha=freedom * alpha_fit, beta=freedom / invbeta_fit) - else: - return pm.Gamma(param, alpha=freedom * alpha_fit, beta=freedom / invbeta_fit, shape=shape) - - elif (distribution == 'igamma'): - alpha_fit, loc_fit, beta_fit = stats.gamma.fit(samples) - if shape is None: - return pm.InverseGamma(param, alpha=freedom * alpha_fit, beta=freedom * beta_fit) - else: - return pm.InverseGamma(param, alpha=freedom * alpha_fit, beta=freedom * beta_fit, shape=shape) - - -def hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): - """ - :param X: [N×P] array of clinical covariates - :param y: [N×1] array of neuroimaging measures - :param batch_effects: [N×M] array of batch effects - :param batch_effects_size: [b1, b2,...,bM] List of counts of unique values of batch effects - :param configs: - :param trace: - :param return_shared_variables: If true, returns references to the shared variables. The values of the shared variables can be set manually, allowing running the same model on different data without re-compiling it. - :return: - """ - X = theano.shared(X) - X = theano.tensor.cast(X,'floatX') - y = theano.shared(y) - y = theano.tensor.cast(y,'floatX') - - - with pm.Model() as model: - - # Make a param builder that will make the correct calls - pb = ParamBuilder(model, X, y, batch_effects, trace, configs) - - if configs['likelihood'] == 'Normal': - mu = pb.make_param("mu", mu_slope_mu_params = (0.,10.), sigma_slope_mu_params = (5.,), mu_intercept_mu_params=(0.,10.), sigma_intercept_mu_params = (5.,)).get_samples(pb) - sigma = pb.make_param("sigma", mu_sigma_params = (10., 5.), sigma_sigma_params = (5.,)).get_samples(pb) - sigma_plus = pm.math.log(1+pm.math.exp(sigma)) - y_like = pm.Normal('y_like',mu=mu, sigma=sigma_plus, observed=y) - - elif configs['likelihood'] in ['SHASHb','SHASHo','SHASHo2']: - """ - Comment 1 - The current parameterizations are tuned towards standardized in- and output data. - It is possible to adjust the priors through the XXX_dist and XXX_params kwargs, like here we do with epsilon_params. - Supported distributions are listed in the Prior class. - - Comment 2 - Any mapping that is applied here after sampling should also be applied in util.hbr_utils.forward in order for the functions there to properly work. - For example, the softplus applied to sigma here is also applied in util.hbr_utils.forward - """ - SHASH_map = {'SHASHb':SHASHb,'SHASHo':SHASHo,'SHASHo2':SHASHo2} - - mu = pb.make_param("mu", slope_mu_params = (0.,3.), mu_intercept_mu_params=(0.,1.), sigma_intercept_mu_params = (1.,)).get_samples(pb) - sigma = pb.make_param("sigma", sigma_params = (1.,2.), slope_sigma_params=(0.,1.), intercept_sigma_params = (1., 1.)).get_samples(pb) - sigma_plus = pm.math.log(1+pm.math.exp(sigma)) - epsilon = pb.make_param("epsilon", epsilon_params = (0.,1.), slope_epsilon_params=(0.,1.), intercept_epsilon_params=(0.,1)).get_samples(pb) - delta = pb.make_param("delta", delta_params=(1.5,2.), slope_delta_params=(0.,1), intercept_delta_params=(2., 1)).get_samples(pb) - delta_plus = pm.math.log(1+pm.math.exp(delta)) + 0.3 - y_like = SHASH_map[configs['likelihood']]('y_like', mu=mu, sigma=sigma_plus, epsilon=epsilon, delta=delta_plus, observed = y) - - return model - - - -class HBR: - - """Hierarchical Bayesian Regression for normative modeling - - Basic usage:: - - model = HBR(configs) - trace = model.estimate(X, y, batch_effects) - ys,s2 = model.predict(X, batch_effects) - - where the variables are - - :param configs: a dictionary of model configurations. - :param X: N-by-P input matrix of P features for N subjects - :param y: N-by-1 vector of outputs. - :param batch_effects: N-by-B matrix of B batch ids for N subjects. - - :returns: * ys - predictive mean - * s2 - predictive variance - - Written by S.M. Kia - """ - - def get_step_methods(self, m): - """ - This can be used to assign default step functions. However, the nuts initialization keyword doesnt work together with this... so better not use it. - - STEP_METHODS = ( - NUTS, - HamiltonianMC, - Metropolis, - BinaryMetropolis, - BinaryGibbsMetropolis, - Slice, - CategoricalGibbsMetropolis, - ) - :param m: a PyMC3 model - :return: - """ - samplermap = {'NUTS': NUTS, 'MH': Metropolis, 'Slice': Slice, 'HMC': HamiltonianMC} - fallbacks = [Metropolis] # We are using MH as a fallback method here - if self.configs['sampler'] == 'NUTS': - step_kwargs = {'nuts': {'target_accept': self.configs['target_accept']}} - else: - step_kwargs = None - return pm.sampling.assign_step_methods(m, methods=[samplermap[self.configs['sampler']]] + fallbacks, - step_kwargs=step_kwargs) - - def __init__(self, configs): - self.bsp = None - self.model_type = configs['type'] - self.configs = configs - - def get_modeler(self): - return {'nn': nn_hbr}.get(self.model_type, hbr) - - def transform_X(self, X): - if self.model_type == 'polynomial': - Phi = create_poly_basis(X, self.configs['order']) - elif self.model_type == 'bspline': - if self.bsp is None: - self.bsp = bspline_fit(X, self.configs['order'], self.configs['nknots']) - bspline = bspline_transform(X, self.bsp) - Phi = np.concatenate((X, bspline), axis = 1) - else: - Phi = X - return Phi - - - def find_map(self, X, y, batch_effects,method='L-BFGS-B'): - """ Function to estimate the model """ - X, y, batch_effects = expand_all(X, y, batch_effects) - - self.batch_effects_num = batch_effects.shape[1] - self.batch_effects_size = [] - for i in range(self.batch_effects_num): - self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) - - X = self.transform_X(X) - modeler = self.get_modeler() - with modeler(X, y, batch_effects, self.batch_effects_size, self.configs) as m: - self.MAP = pm.find_MAP(method=method) - return self.MAP - - def estimate(self, X, y, batch_effects): - - """ Function to estimate the model """ - X, y, batch_effects = expand_all(X, y, batch_effects) - - self.batch_effects_num = batch_effects.shape[1] - self.batch_effects_size = [] - for i in range(self.batch_effects_num): - self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) - - X = self.transform_X(X) - modeler = self.get_modeler() - with modeler(X, y, batch_effects, self.batch_effects_size, self.configs) as m: - self.trace = pm.sample(draws=self.configs['n_samples'], - tune=self.configs['n_tuning'], - chains=self.configs['n_chains'], - init=self.configs['init'], n_init=500000, - cores=self.configs['cores']) - return self.trace - - def predict(self, X, batch_effects, pred='single'): - """ Function to make predictions from the model """ - X, batch_effects = expand_all(X, batch_effects) - - samples = self.configs['n_samples'] - y = np.zeros([X.shape[0], 1]) - - if pred == 'single': - X = self.transform_X(X) - modeler = self.get_modeler() - with modeler(X, y, batch_effects, self.batch_effects_size, self.configs): - ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) - pred_mean = ppc['y_like'].mean(axis=0) - pred_var = ppc['y_like'].var(axis=0) - - return pred_mean, pred_var - - def estimate_on_new_site(self, X, y, batch_effects): - """ Function to adapt the model """ - X, y, batch_effects = expand_all(X, y, batch_effects) - - self.batch_effects_num = batch_effects.shape[1] - self.batch_effects_size = [] - for i in range(self.batch_effects_num): - self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) - - X = self.transform_X(X) - modeler = self.get_modeler() - with modeler(X, y, batch_effects, self.batch_effects_size, - self.configs, trace=self.trace) as m: - self.trace = pm.sample(self.configs['n_samples'], - tune=self.configs['n_tuning'], - chains=self.configs['n_chains'], - target_accept=self.configs['target_accept'], - init=self.configs['init'], n_init=50000, - cores=self.configs['cores']) - return self.trace - - def predict_on_new_site(self, X, batch_effects): - """ Function to make predictions from the model """ - X, batch_effects = expand_all(X, batch_effects) - - samples = self.configs['n_samples'] - y = np.zeros([X.shape[0], 1]) - - X = self.transform_X(X) - modeler = self.get_modeler() - with modeler(X, y, batch_effects, self.batch_effects_size, self.configs, trace=self.trace): - ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) - pred_mean = ppc['y_like'].mean(axis=0) - pred_var = ppc['y_like'].var(axis=0) - - return pred_mean, pred_var - - def generate(self, X, batch_effects, samples): - """ Function to generate samples from posterior predictive distribution """ - X, batch_effects = expand_all(X, batch_effects) - - y = np.zeros([X.shape[0], 1]) - - X = self.transform_X(X) - modeler = self.get_modeler() - with modeler(X, y, batch_effects, self.batch_effects_size, self.configs): - ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) - - generated_samples = np.reshape(ppc['y_like'].squeeze().T, [X.shape[0] * samples, 1]) - X = np.repeat(X, samples) - if len(X.shape) == 1: - X = np.expand_dims(X, axis=1) - batch_effects = np.repeat(batch_effects, samples, axis=0) - if len(batch_effects.shape) == 1: - batch_effects = np.expand_dims(batch_effects, axis=1) - - return X, batch_effects, generated_samples - - def sample_prior_predictive(self, X, batch_effects, samples, trace=None): - """ Function to sample from prior predictive distribution """ - - if len(X.shape) == 1: - X = np.expand_dims(X, axis=1) - if len(batch_effects.shape) == 1: - batch_effects = np.expand_dims(batch_effects, axis=1) - - self.batch_effects_num = batch_effects.shape[1] - self.batch_effects_size = [] - for i in range(self.batch_effects_num): - self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) - - y = np.zeros([X.shape[0], 1]) - - if self.model_type == 'linear': - with hbr(X, y, batch_effects, self.batch_effects_size, self.configs, - trace): - ppc = pm.sample_prior_predictive(samples=samples) - return ppc - - def get_model(self, X, y, batch_effects): - X, y, batch_effects = expand_all(X, y, batch_effects) - - self.batch_effects_num = batch_effects.shape[1] - self.batch_effects_size = [] - for i in range(self.batch_effects_num): - self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) - modeler = self.get_modeler() - X = self.transform_X(X) - return modeler(X, y, batch_effects, self.batch_effects_size, self.configs, self.trace) - - def create_dummy_inputs(self, covariate_ranges = [[0.1,0.9,0.01]]): - - arrays = [] - for i in range(len(covariate_ranges)): - arrays.append(np.arange(covariate_ranges[i][0],covariate_ranges[i][1], covariate_ranges[i][2])) - X = cartesian_product(arrays) - X_dummy = np.concatenate([X for i in range(np.prod(self.batch_effects_size))]) - - arrays = [] - for i in range(self.batch_effects_num): - arrays.append(np.arange(0, self.batch_effects_size[i])) - batch_effects = cartesian_product(arrays) - batch_effects_dummy = np.repeat(batch_effects, X.shape[0], axis=0) - - return X_dummy, batch_effects_dummy - -class Prior: - """ - A wrapper class for a PyMC3 distribution. - - creates a fitted distribution from the trace, if one is present - - overloads the __getitem__ function with something that switches between indexing or not, based on the shape - """ - def __init__(self, name, dist, params, pb, shape=(1,)) -> None: - self.dist = None - self.name = name - self.shape = shape - self.has_random_effect = True if len(shape)>1 else False - self.distmap = {'normal': pm.Normal, - 'hnormal': pm.HalfNormal, - 'gamma': pm.Gamma, - 'uniform': pm.Uniform, - 'igamma': pm.InverseGamma, - 'hcauchy': pm.HalfCauchy} - self.make_dist(dist, params, pb) - - def make_dist(self, dist, params, pb): - """This creates a pymc3 distribution. If there is a trace, the distribution is fitted to the trace. If there isn't a trace, the prior is parameterized by the values in (params)""" - with pb.model as m: - if (pb.trace is not None) and (not self.has_random_effect): - int_dist = from_posterior(param=self.name, - samples=pb.trace[self.name], - distribution=dist, - freedom=pb.configs['freedom']) - self.dist = int_dist.reshape(self.shape) - else: - shape_prod = np.product(np.array(self.shape)) - print(self.name) - print(f"dist={dist}") - print(f"params={params}") - int_dist = self.distmap[dist](self.name, *params, shape=shape_prod) - self.dist = int_dist.reshape(self.shape) - - def __getitem__(self, idx): - """The idx here is the index of the batch-effect. If the prior does not model batch effects, this should return the same value for each index""" - assert self.dist is not None, "Distribution not initialized" - if self.has_random_effect: - return self.dist[idx] - else: - return self.dist - - -class ParamBuilder: - """ - A class that simplifies the construction of parameterizations. - It has a lot of attributes necessary for creating the model, including the data, but it is never saved with the model. - It also contains a lot of decision logic for creating the parameterizations. - """ - - def __init__(self, model, X, y, batch_effects, trace, configs): - """ - - :param model: model to attach all the distributions to - :param X: Covariates - :param y: IDPs - :param batch_effects: I guess this speaks for itself - :param trace: idem - :param configs: idem - """ - self.model = model - self.X = X - self.y = y - self.batch_effects = batch_effects - self.trace = trace - self.configs = configs - - self.feature_num = X.shape[1].eval().item() - self.y_shape = y.shape.eval() - self.n_ys = y.shape[0].eval().item() - self.batch_effects_num = batch_effects.shape[1] - - self.batch_effects_size = [] - self.all_idx = [] - for i in range(self.batch_effects_num): - # Count the unique values for each batch effect - self.batch_effects_size.append(len(np.unique(self.batch_effects[:, i]))) - # Store the unique values for each batch effect - self.all_idx.append(np.int16(np.unique(self.batch_effects[:, i]))) - # Make a cartesian product of all the unique values of each batch effect - self.be_idx = list(product(*self.all_idx)) - - # Make tuples of batch effects ID's and indices of datapoints with that specific combination of batch effects - self.be_idx_tups = [] - for be in self.be_idx: - a = [] - for i, b in enumerate(be): - a.append(self.batch_effects[:, i] == b) - idx = reduce(np.logical_and, a).nonzero() - if idx[0].shape[0] != 0: - self.be_idx_tups.append((be, idx)) - - def make_param(self, name, dim = (1,), **kwargs): - if self.configs.get(f'linear_{name}', False): - # First make a slope and intercept, and use those to make a linear parameterization - slope_parameterization = self.make_param(f'slope_{name}', dim=[self.feature_num], **kwargs) - intercept_parameterization = self.make_param(f'intercept_{name}', **kwargs) - return LinearParameterization(name=name, dim=dim, - slope_parameterization=slope_parameterization, intercept_parameterization=intercept_parameterization, - pb=self, - **kwargs) - - elif self.configs.get(f'random_{name}', False): - if self.configs.get(f'centered_{name}', True): - return CentralRandomFixedParameterization(name=name, pb=self, dim=dim, **kwargs) - else: - return NonCentralRandomFixedParameterization(name=name, pb=self, dim=dim, **kwargs) - else: - return FixedParameterization(name=name, dim=dim, pb=self,**kwargs) - - -class Parameterization: - """ - This is the top-level parameterization class from which all the other parameterizations inherit. - """ - def __init__(self, name, dim): - self.name = name - self.dim = dim - print(name, type(self)) - - def get_samples(self, pb): - - with pb.model: - samples = theano.tensor.zeros([pb.n_ys, *self.dim]) - for be, idx in pb.be_idx_tups: - samples = theano.tensor.set_subtensor(samples[idx], self.dist[be]) - return samples - - -class FixedParameterization(Parameterization): - """ - A parameterization that takes a single value for all input. It does not depend on anything except its hyperparameters - """ - def __init__(self, name, dim, pb:ParamBuilder, **kwargs): - super().__init__(name, dim) - dist = kwargs.get(f'{name}_dist','normal') - params = kwargs.get(f'{name}_params',(0.,1.)) - self.dist = Prior(name, dist, params, pb, shape = dim) - - -class CentralRandomFixedParameterization(Parameterization): - """ - A parameterization that is fixed for each batch effect. This is sampled in a central fashion; - the values are sampled from normal distribution with a group mean and group variance - """ - def __init__(self, name, dim, pb:ParamBuilder, **kwargs): - super().__init__(name, dim) - - # Normal distribution is default for mean - mu_dist = kwargs.get(f'mu_{name}_dist','normal') - mu_params = kwargs.get(f'mu_{name}_params',(0.,1.)) - mu_prior = Prior(f'mu_{name}', mu_dist, mu_params, pb, shape = dim) - - # HalfCauchy is default for sigma - sigma_dist = kwargs.get(f'sigma_{name}_dist','hcauchy') - sigma_params = kwargs.get(f'sigma_{name}_params',(1.,)) - sigma_prior = Prior(f'sigma_{name}',sigma_dist, sigma_params, pb, shape = [*pb.batch_effects_size, *dim]) - - self.dist = pm.Normal(name=name, mu=mu_prior.dist, sigma=sigma_prior.dist, shape = [*pb.batch_effects_size, *dim]) - - -class NonCentralRandomFixedParameterization(Parameterization): - """ - A parameterization that is fixed for each batch effect. This is sampled in a non-central fashion; - the values are a sum of a group mean and noise values scaled with a group scaling factor - """ - def __init__(self, name,dim, pb:ParamBuilder, **kwargs): - super().__init__(name, dim) - - # Normal distribution is default for mean - mu_dist = kwargs.get(f'mu_{name}_dist','normal') - mu_params = kwargs.get(f'mu_{name}_params',(0.,1.)) - mu_prior = Prior(f'mu_{name}', mu_dist, mu_params, pb, shape = dim) - - # HalfCauchy is default for sigma - sigma_dist = kwargs.get(f'sigma_{name}_dist','hcauchy') - sigma_params = kwargs.get(f'sigma_{name}_params',(1.,)) - sigma_prior = Prior(f'sigma_{name}',sigma_dist, sigma_params, pb, shape = dim) - - # Normal is default for offset - offset_dist = kwargs.get(f'offset_{name}_dist','normal') - offset_params = kwargs.get(f'offset_{name}_params',(0.,1.)) - offset_prior = Prior(f'offset_{name}',offset_dist, offset_params, pb, shape = [*pb.batch_effects_size, *dim]) - - self.dist = pm.Deterministic(name=name, var=mu_prior.dist+sigma_prior.dist*offset_prior.dist) - - -class LinearParameterization(Parameterization): - """ - A parameterization that can model a linear dependence on X. - """ - def __init__(self, name, dim, slope_parameterization, intercept_parameterization, pb, **kwargs): - super().__init__( name, dim) - self.slope_parameterization = slope_parameterization - self.intercept_parameterization = intercept_parameterization - - def get_samples(self, pb:ParamBuilder): - with pb.model: - samples = theano.tensor.zeros([pb.n_ys, *self.dim]) - for be, idx in pb.be_idx_tups: - dot = theano.tensor.dot(pb.X[idx,:], self.slope_parameterization.dist[be]).T - intercept = self.intercept_parameterization.dist[be] - samples = theano.tensor.set_subtensor(samples[idx,:],dot+intercept) - return samples - - -def get_design_matrix(X, nm, basis="linear"): - if basis == "bspline": - Phi = bspline_transform(X, nm.hbr.bsp) - elif basis == "polynomial": - Phi = create_poly_basis(X, 3) - else: - Phi = X - return Phi - - - -def nn_hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): - n_hidden = configs['nn_hidden_neuron_num'] - n_layers = configs['nn_hidden_layers_num'] - feature_num = X.shape[1] - batch_effects_num = batch_effects.shape[1] - all_idx = [] - for i in range(batch_effects_num): - all_idx.append(np.int16(np.unique(batch_effects[:, i]))) - be_idx = list(product(*all_idx)) - - # Initialize random weights between each layer for the mu: - init_1 = pm.floatX(np.random.randn(feature_num, n_hidden) * np.sqrt(1 / feature_num)) - init_out = pm.floatX(np.random.randn(n_hidden) * np.sqrt(1 / n_hidden)) - - std_init_1 = pm.floatX(np.random.rand(feature_num, n_hidden)) - std_init_out = pm.floatX(np.random.rand(n_hidden)) - - # And initialize random weights between each layer for sigma_noise: - init_1_noise = pm.floatX(np.random.randn(feature_num, n_hidden) * np.sqrt(1 / feature_num)) - init_out_noise = pm.floatX(np.random.randn(n_hidden) * np.sqrt(1 / n_hidden)) - - std_init_1_noise = pm.floatX(np.random.rand(feature_num, n_hidden)) - std_init_out_noise = pm.floatX(np.random.rand(n_hidden)) - - # If there are two hidden layers, then initialize weights for the second layer: - if n_layers == 2: - init_2 = pm.floatX(np.random.randn(n_hidden, n_hidden) * np.sqrt(1 / n_hidden)) - std_init_2 = pm.floatX(np.random.rand(n_hidden, n_hidden)) - init_2_noise = pm.floatX(np.random.randn(n_hidden, n_hidden) * np.sqrt(1 / n_hidden)) - std_init_2_noise = pm.floatX(np.random.rand(n_hidden, n_hidden)) - - with pm.Model() as model: - - X = pm.Data('X', X) - y = pm.Data('y', y) - - if trace is not None: # Used when estimating/predicting on a new site - weights_in_1_grp = from_posterior('w_in_1_grp', trace['w_in_1_grp'], - distribution='normal', freedom=configs['freedom']) - - weights_in_1_grp_sd = from_posterior('w_in_1_grp_sd', trace['w_in_1_grp_sd'], - distribution='hcauchy', freedom=configs['freedom']) - - if n_layers == 2: - weights_1_2_grp = from_posterior('w_1_2_grp', trace['w_1_2_grp'], - distribution='normal', freedom=configs['freedom']) - - weights_1_2_grp_sd = from_posterior('w_1_2_grp_sd', trace['w_1_2_grp_sd'], - distribution='hcauchy', freedom=configs['freedom']) - - weights_2_out_grp = from_posterior('w_2_out_grp', trace['w_2_out_grp'], - distribution='normal', freedom=configs['freedom']) - - weights_2_out_grp_sd = from_posterior('w_2_out_grp_sd', trace['w_2_out_grp_sd'], - distribution='hcauchy', freedom=configs['freedom']) - - mu_prior_intercept = from_posterior('mu_prior_intercept', trace['mu_prior_intercept'], - distribution='normal', freedom=configs['freedom']) - sigma_prior_intercept = from_posterior('sigma_prior_intercept', trace['sigma_prior_intercept'], - distribution='hcauchy', freedom=configs['freedom']) - - else: - # Group the mean distribution for input to the hidden layer: - weights_in_1_grp = pm.Normal('w_in_1_grp', 0, sd=1, - shape=(feature_num, n_hidden), testval=init_1) - - # Group standard deviation: - weights_in_1_grp_sd = pm.HalfCauchy('w_in_1_grp_sd', 1., - shape=(feature_num, n_hidden), testval=std_init_1) - - if n_layers == 2: - # Group the mean distribution for hidden layer 1 to hidden layer 2: - weights_1_2_grp = pm.Normal('w_1_2_grp', 0, sd=1, - shape=(n_hidden, n_hidden), testval=init_2) - - # Group standard deviation: - weights_1_2_grp_sd = pm.HalfCauchy('w_1_2_grp_sd', 1., - shape=(n_hidden, n_hidden), testval=std_init_2) - - # Group the mean distribution for hidden to output: - weights_2_out_grp = pm.Normal('w_2_out_grp', 0, sd=1, - shape=(n_hidden,), testval=init_out) - - # Group standard deviation: - weights_2_out_grp_sd = pm.HalfCauchy('w_2_out_grp_sd', 1., - shape=(n_hidden,), testval=std_init_out) - - # mu_prior_intercept = pm.Uniform('mu_prior_intercept', lower=-100, upper=100) - mu_prior_intercept = pm.Normal('mu_prior_intercept', mu=0., sigma=1e3) - sigma_prior_intercept = pm.HalfCauchy('sigma_prior_intercept', 5) - - # Now create separate weights for each group, by doing - # weights * group_sd + group_mean, we make sure the new weights are - # coming from the (group_mean, group_sd) distribution. - weights_in_1_raw = pm.Normal('w_in_1', 0, sd=1, - shape=(batch_effects_size + [feature_num, n_hidden])) - weights_in_1 = weights_in_1_raw * weights_in_1_grp_sd + weights_in_1_grp - - if n_layers == 2: - weights_1_2_raw = pm.Normal('w_1_2', 0, sd=1, - shape=(batch_effects_size + [n_hidden, n_hidden])) - weights_1_2 = weights_1_2_raw * weights_1_2_grp_sd + weights_1_2_grp - - weights_2_out_raw = pm.Normal('w_2_out', 0, sd=1, - shape=(batch_effects_size + [n_hidden])) - weights_2_out = weights_2_out_raw * weights_2_out_grp_sd + weights_2_out_grp - - intercepts_offset = pm.Normal('intercepts_offset', mu=0, sd=1, - shape=(batch_effects_size)) - - intercepts = pm.Deterministic('intercepts', intercepts_offset + - mu_prior_intercept * sigma_prior_intercept) - - # Build the neural network and estimate y_hat: - y_hat = theano.tensor.zeros(y.shape) - for be in be_idx: - # Find the indices corresponding to 'group be': - a = [] - for i, b in enumerate(be): - a.append(batch_effects[:, i] == b) - idx = reduce(np.logical_and, a).nonzero() - if idx[0].shape[0] != 0: - act_1 = pm.math.tanh(theano.tensor.dot(X[idx, :], weights_in_1[be])) - if n_layers == 2: - act_2 = pm.math.tanh(theano.tensor.dot(act_1, weights_1_2[be])) - y_hat = theano.tensor.set_subtensor(y_hat[idx, 0], - intercepts[be] + theano.tensor.dot(act_2, weights_2_out[be])) - else: - y_hat = theano.tensor.set_subtensor(y_hat[idx, 0], - intercepts[be] + theano.tensor.dot(act_1, weights_2_out[be])) - - # If we want to estimate varying noise terms across groups: - if configs['random_noise']: - if configs['hetero_noise']: - if trace is not None: # # Used when estimating/predicting on a new site - weights_in_1_grp_noise = from_posterior('w_in_1_grp_noise', - trace['w_in_1_grp_noise'], - distribution='normal', freedom=configs['freedom']) - - weights_in_1_grp_sd_noise = from_posterior('w_in_1_grp_sd_noise', - trace['w_in_1_grp_sd_noise'], - distribution='hcauchy', freedom=configs['freedom']) - - if n_layers == 2: - weights_1_2_grp_noise = from_posterior('w_1_2_grp_noise', - trace['w_1_2_grp_noise'], - distribution='normal', freedom=configs['freedom']) - - weights_1_2_grp_sd_noise = from_posterior('w_1_2_grp_sd_noise', - trace['w_1_2_grp_sd_noise'], - distribution='hcauchy', freedom=configs['freedom']) - - weights_2_out_grp_noise = from_posterior('w_2_out_grp_noise', - trace['w_2_out_grp_noise'], - distribution='normal', freedom=configs['freedom']) - - weights_2_out_grp_sd_noise = from_posterior('w_2_out_grp_sd_noise', - trace['w_2_out_grp_sd_noise'], - distribution='hcauchy', freedom=configs['freedom']) - - else: - # The input layer to the first hidden layer: - weights_in_1_grp_noise = pm.Normal('w_in_1_grp_noise', 0, sd=1, - shape=(feature_num, n_hidden), - testval=init_1_noise) - weights_in_1_grp_sd_noise = pm.HalfCauchy('w_in_1_grp_sd_noise', 1, - shape=(feature_num, n_hidden), - testval=std_init_1_noise) - - # The first hidden layer to second hidden layer: - if n_layers == 2: - weights_1_2_grp_noise = pm.Normal('w_1_2_grp_noise', 0, sd=1, - shape=(n_hidden, n_hidden), - testval=init_2_noise) - weights_1_2_grp_sd_noise = pm.HalfCauchy('w_1_2_grp_sd_noise', 1, - shape=(n_hidden, n_hidden), - testval=std_init_2_noise) - - # The second hidden layer to output layer: - weights_2_out_grp_noise = pm.Normal('w_2_out_grp_noise', 0, sd=1, - shape=(n_hidden,), - testval=init_out_noise) - weights_2_out_grp_sd_noise = pm.HalfCauchy('w_2_out_grp_sd_noise', 1, - shape=(n_hidden,), - testval=std_init_out_noise) - - # mu_prior_intercept_noise = pm.HalfNormal('mu_prior_intercept_noise', sigma=1e3) - # sigma_prior_intercept_noise = pm.HalfCauchy('sigma_prior_intercept_noise', 5) - - # Now create separate weights for each group: - weights_in_1_raw_noise = pm.Normal('w_in_1_noise', 0, sd=1, - shape=(batch_effects_size + [feature_num, n_hidden])) - weights_in_1_noise = weights_in_1_raw_noise * weights_in_1_grp_sd_noise + weights_in_1_grp_noise - - if n_layers == 2: - weights_1_2_raw_noise = pm.Normal('w_1_2_noise', 0, sd=1, - shape=(batch_effects_size + [n_hidden, n_hidden])) - weights_1_2_noise = weights_1_2_raw_noise * weights_1_2_grp_sd_noise + weights_1_2_grp_noise - - weights_2_out_raw_noise = pm.Normal('w_2_out_noise', 0, sd=1, - shape=(batch_effects_size + [n_hidden])) - weights_2_out_noise = weights_2_out_raw_noise * weights_2_out_grp_sd_noise + weights_2_out_grp_noise - - # intercepts_offset_noise = pm.Normal('intercepts_offset_noise', mu=0, sd=1, - # shape=(batch_effects_size)) - - # intercepts_noise = pm.Deterministic('intercepts_noise', mu_prior_intercept_noise + - # intercepts_offset_noise * sigma_prior_intercept_noise) - - # Build the neural network and estimate the sigma_y: - sigma_y = theano.tensor.zeros(y.shape) - for be in be_idx: - a = [] - for i, b in enumerate(be): - a.append(batch_effects[:, i] == b) - idx = reduce(np.logical_and, a).nonzero() - if idx[0].shape[0] != 0: - act_1_noise = pm.math.sigmoid(theano.tensor.dot(X[idx, :], weights_in_1_noise[be])) - if n_layers == 2: - act_2_noise = pm.math.sigmoid(theano.tensor.dot(act_1_noise, weights_1_2_noise[be])) - temp = pm.math.log1pexp(theano.tensor.dot(act_2_noise, weights_2_out_noise[be])) + 1e-5 - else: - temp = pm.math.log1pexp(theano.tensor.dot(act_1_noise, weights_2_out_noise[be])) + 1e-5 - sigma_y = theano.tensor.set_subtensor(sigma_y[idx, 0], temp) - - else: # homoscedastic noise: - if trace is not None: # Used for transferring the priors - upper_bound = np.percentile(trace['sigma_noise'], 95) - sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=2 * upper_bound, shape=(batch_effects_size)) - else: - sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=100, shape=(batch_effects_size)) - - sigma_y = theano.tensor.zeros(y.shape) - for be in be_idx: - a = [] - for i, b in enumerate(be): - a.append(batch_effects[:, i] == b) - idx = reduce(np.logical_and, a).nonzero() - if idx[0].shape[0] != 0: - sigma_y = theano.tensor.set_subtensor(sigma_y[idx, 0], sigma_noise[be]) - - else: # do not allow for random noise terms across groups: - if trace is not None: # Used for transferring the priors - upper_bound = np.percentile(trace['sigma_noise'], 95) - sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=2 * upper_bound) - else: - sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=100) - sigma_y = theano.tensor.zeros(y.shape) - for be in be_idx: - a = [] - for i, b in enumerate(be): - a.append(batch_effects[:, i] == b) - idx = reduce(np.logical_and, a).nonzero() - if idx[0].shape[0] != 0: - sigma_y = theano.tensor.set_subtensor(sigma_y[idx, 0], sigma_noise) - - if configs['skewed_likelihood']: - skewness = pm.Uniform('skewness', lower=-10, upper=10, shape=(batch_effects_size)) - alpha = theano.tensor.zeros(y.shape) - for be in be_idx: - a = [] - for i, b in enumerate(be): - a.append(batch_effects[:, i] == b) - idx = reduce(np.logical_and, a).nonzero() - if idx[0].shape[0] != 0: - alpha = theano.tensor.set_subtensor(alpha[idx, 0], skewness[be]) - else: - alpha = 0 # symmetrical normal distribution - - y_like = pm.SkewNormal('y_like', mu=y_hat, sigma=sigma_y, alpha=alpha, observed=y) - - return model diff --git a/build/lib/pcntoolkit/model/rfa.py b/build/lib/pcntoolkit/model/rfa.py deleted file mode 100644 index 7e67169b..00000000 --- a/build/lib/pcntoolkit/model/rfa.py +++ /dev/null @@ -1,243 +0,0 @@ -from __future__ import print_function -from __future__ import division - -import numpy as np -import torch - -class GPRRFA: - """Random Feature Approximation for Gaussian Process Regression - - Estimation and prediction of Bayesian linear regression models - - Basic usage:: - - R = GPRRFA() - hyp = R.estimate(hyp0, X, y) - ys,s2 = R.predict(hyp, X, y, Xs) - - where the variables are - - :param hyp: vector of hyperparmaters. - :param X: N x D data array - :param y: 1D Array of targets (length N) - :param Xs: Nte x D array of test cases - :param hyp0: starting estimates for hyperparameter optimisation - - :returns: * ys - predictive mean - * s2 - predictive variance - - The hyperparameters are:: - - hyp = [ log(sn), log(ell), log(sf) ] # hyp is a numpy array - - where sn^2 is the noise variance, ell are lengthscale parameters and - sf^2 is the signal variance. This provides an approximation to the - covariance function:: - - k(x,z) = x'*z + sn2*exp(0.5*(x-z)'*Lambda*(x-z)) - - where Lambda = diag((ell_1^2, ... ell_D^2)) - - Written by A. Marquand - """ - - def __init__(self, hyp=None, X=None, y=None, n_feat=None, - n_iter=100, tol=1e-3, verbose=False): - - self.hyp = np.nan - self.nlZ = np.nan - self.tol = tol # not used at present - self.Nf = n_feat - self.n_iter = n_iter - self.verbose = verbose - self._n_restarts = 5 - - if (hyp is not None) and (X is not None) and (y is not None): - self.post(hyp, X, y) - - def _numpy2torch(self, X, y=None, hyp=None): - - if type(X) is torch.Tensor: - pass - elif type(X) is np.ndarray: - X = torch.from_numpy(X) - else: - raise(ValueError, 'Unknown data type (X)') - X = X.double() - - if y is not None: - if type(y) is torch.Tensor: - pass - elif type(y) is np.ndarray: - y = torch.from_numpy(y) - else: - raise(ValueError, 'Unknown data type (y)') - - if len(y.shape) == 1: - y.resize_(y.shape[0],1) - y = y.double() - - if hyp is not None: - if type(hyp) is torch.Tensor: - pass - else: - hyp = torch.tensor(hyp, requires_grad=True) - - return X, y, hyp - - def get_n_params(self, X): - - return X.shape[1] + 2 - - def post(self, hyp, X, y): - """ Generic function to compute posterior distribution. - - This function will save the posterior mean and precision matrix as - self.m and self.A and will also update internal parameters (e.g. - N, D and the prior covariance (Sigma) and precision (iSigma). - """ - - # make sure all variables are the right type - X, y, hyp = self._numpy2torch(X, y, hyp) - - self.N, self.Dx = X.shape - - # ensure the number of features is specified (use 75% as a default) - if self.Nf is None: - self.Nf = int(0.75 * self.N) - - self.Omega = torch.zeros((self.Dx, self.Nf), dtype=torch.double) - for f in range(self.Nf): - self.Omega[:,f] = torch.exp(hyp[1:-1]) * \ - torch.randn((self.Dx, 1), dtype=torch.double).squeeze() - - XO = torch.mm(X, self.Omega) - self.Phi = torch.exp(hyp[-1])/np.sqrt(self.Nf) * \ - torch.cat((torch.cos(XO), torch.sin(XO)), 1) - - # concatenate linear weights - self.Phi = torch.cat((self.Phi, X), 1) - self.D = self.Phi.shape[1] - - if self.verbose: - print("estimating posterior ... | hyp=", hyp) - - self.A = torch.mm(torch.t(self.Phi), self.Phi) / torch.exp(2*hyp[0]) + \ - torch.eye(self.D, dtype=torch.double) - self.m = torch.mm(torch.solve(torch.t(self.Phi), self.A)[0], y) / \ - torch.exp(2*hyp[0]) - - # save hyperparameters - self.hyp = hyp - - # update optimizer iteration count - if hasattr(self,'_iterations'): - self._iterations += 1 - - def loglik(self, hyp, X, y): - """ Function to compute compute log (marginal) likelihood """ - X, y, hyp = self._numpy2torch(X, y, hyp) - - # always recompute the posterior - self.post(hyp, X, y) - - #logdetA = 2*torch.sum(torch.log(torch.diag(torch.cholesky(self.A)))) - try: - # compute the log determinants in a numerically stable way - logdetA = 2*torch.sum(torch.log(torch.diag(torch.cholesky(self.A)))) - except Exception as e: - print("Warning: Estimation of posterior distribution failed") - print(e) - #nlZ = torch.tensor(1/np.finfo(float).eps) - nlZ = torch.tensor(np.nan) - self._optim_failed = True - return nlZ - - # compute negative marginal log likelihood - nlZ = -0.5 * (self.N*torch.log(1/torch.exp(2*hyp[0])) - - self.N*np.log(2*np.pi) - - torch.mm(torch.t(y - torch.mm(self.Phi,self.m)), - (y - torch.mm(self.Phi,self.m))) / - torch.exp(2*hyp[0]) - - torch.mm(torch.t(self.m), self.m) - logdetA) - - if self.verbose: - print("nlZ= ", nlZ, " | hyp=", hyp) - - # save marginal likelihood - self.nlZ = nlZ - return nlZ - - def dloglik(self, hyp, X, y): - """ Function to compute derivatives """ - - print("derivatives not available") - - return - - def estimate(self, hyp0, X, y, optimizer='lbfgs'): - """ Function to estimate the model """ - - if type(hyp0) is torch.Tensor: - hyp = hyp0 - hyp0.requires_grad_() - else: - hyp = torch.tensor(hyp0, requires_grad=True) - # save the starting values - self.hyp0 = hyp - - if optimizer.lower() == 'lbfgs': - opt = torch.optim.LBFGS([hyp]) - else: - raise(ValueError, "Optimizer " + " not implemented") - self._iterations = 0 - - def closure(): - opt.zero_grad() - nlZ = self.loglik(hyp, X, y) - if not torch.isnan(nlZ): - nlZ.backward() - return nlZ - - for r in range(self._n_restarts): - self._optim_failed = False - - nlZ = opt.step(closure) - - if self._optim_failed: - print("optimization failed. retrying (", r+1, "of", - self._n_restarts,")") - hyp = torch.randn_like(hyp, requires_grad=True) - self.hyp0 = hyp - else: - print("Optimzation complete after", self._iterations, - "evaluations. Function value =", - nlZ.detach().numpy().squeeze()) - break - - return self.hyp.detach().numpy() - - def predict(self, hyp, X, y, Xs): - """ Function to make predictions from the model """ - - X, y, hyp = self._numpy2torch(X, y, hyp) - Xs, *_ = self._numpy2torch(Xs) - - if (hyp != self.hyp).all() or not(hasattr(self, 'A')): - self.post(hyp, X, y) - - # generate prediction tensors - XsO = torch.mm(Xs, self.Omega) - Phis = torch.exp(hyp[-1])/np.sqrt(self.Nf) * \ - torch.cat((torch.cos(XsO), torch.sin(XsO)), 1) - # add linear component - Phis = torch.cat((Phis, Xs), 1) - - ys = torch.mm(Phis, self.m) - - # compute diag(Phis*(Phis'\A)) avoiding computing off-diagonal entries - s2 = torch.exp(2*hyp[0]) + \ - torch.sum(Phis * torch.t(torch.solve(torch.t(Phis), self.A)[0]), 1) - - # return output as numpy arrays - return ys.detach().numpy().squeeze(), s2.detach().numpy().squeeze() diff --git a/build/lib/pcntoolkit/normative.py b/build/lib/pcntoolkit/normative.py deleted file mode 100644 index bfd50553..00000000 --- a/build/lib/pcntoolkit/normative.py +++ /dev/null @@ -1,1434 +0,0 @@ -#!/Users/andre/sfw/anaconda3/bin/python - -# ------------------------------------------------------------------------------ -# Usage: -# python normative.py -m [maskfile] -k [number of CV folds] -c -# -t [test covariates] -r [test responses] -# -# Either the -k switch or -t switch should be specified, but not both. -# If -t is selected, a set of responses should be provided with the -r switch -# -# Written by A. Marquand -# ------------------------------------------------------------------------------ - -from __future__ import print_function -from __future__ import division - -import os -import sys -import numpy as np -import argparse -import pickle -import glob - -from sklearn.model_selection import KFold -from pathlib import Path - -try: # run as a package if installed - from pcntoolkit import configs - from pcntoolkit.dataio import fileio - 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 -except ImportError: - pass - - path = os.path.abspath(os.path.dirname(__file__)) - if path not in sys.path: - sys.path.append(path) - #sys.path.append(os.path.join(path,'normative_model')) - del path - - 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 - -PICKLE_PROTOCOL = configs.PICKLE_PROTOCOL - -def load_response_vars(datafile, maskfile=None, vol=True): - """ load response variables (of any data type)""" - - if fileio.file_type(datafile) == 'nifti': - dat = fileio.load_nifti(datafile, vol=vol) - volmask = fileio.create_mask(dat, mask=maskfile) - Y = fileio.vol2vec(dat, volmask).T - else: - Y = fileio.load(datafile) - volmask = None - if fileio.file_type(datafile) == 'cifti': - Y = Y.T - - return Y, volmask - - -def get_args(*args): - """ Parse command line arguments""" - - # 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("-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("-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() - - # Process required arguemnts - wdir = os.path.realpath(os.path.curdir) - respfile = os.path.join(wdir, args.responses) - if args.covfile is None: - raise(ValueError, "No covariates specified") - else: - covfile = args.covfile - - # Process optional arguments - if 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: - testcov = None - testresp = None - cvfolds = int(args.cvfolds) - print("Running under " + str(cvfolds) + " fold cross-validation.") - else: - print("Test covariates specified") - testcov = args.testcov - cvfolds = None - if args.testresp is None: - testresp = None - print("No test response variables specified") - else: - testresp = args.testresp - if 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: - 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 - - -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 - and a set of true response variables Y, namely: - - * Rho: Pearson correlation - * RMSE: root mean squared error - * SMSE: standardized mean squared error - * EXPV: explained variance - - If the predictive variance is also specified the log loss will be computed - (which also takes into account the predictive variance). If the mean and - standard deviation are also specified these will be used to standardize - this, yielding the mean standardized log loss - - :param Y: N x P array of true response variables - :param Yhat: N x P array of predicted response variables - :param S2: predictive variance - :param mY: mean of the training set - :param sY: standard deviation of the training set - - :returns metrics: evaluation metrics - - ''' - - feature_num = Y.shape[1] - - # Remove metrics that cannot be computed with only a single data point - if Y.shape[0] == 1: - if 'MSLL' in metrics: - metrics.remove('MSLL') - if 'SMSE' in metrics: - metrics.remove('SMSE') - - # find and remove bad variables from the response variables - nz = np.where(np.bitwise_and(np.isfinite(Y).any(axis=0), - np.var(Y, axis=0) != 0))[0] - - MSE = np.mean((Y - Yhat)**2, axis=0) - - results = dict() - - if 'RMSE' in metrics: - RMSE = np.sqrt(MSE) - results['RMSE'] = RMSE - - if 'Rho' in metrics: - Rho = np.zeros(feature_num) - pRho = np.ones(feature_num) - Rho[nz], pRho[nz] = compute_pearsonr(Y[:,nz], Yhat[:,nz]) - results['Rho'] = Rho - results['pRho'] = pRho - - if 'SMSE' in metrics: - SMSE = np.zeros_like(MSE) - SMSE[nz] = MSE[nz] / np.var(Y[:,nz], axis=0) - results['SMSE'] = SMSE - - if 'EXPV' in metrics: - EXPV = np.zeros(feature_num) - EXPV[nz] = explained_var(Y[:,nz], Yhat[:,nz]) - results['EXPV'] = EXPV - - if 'MSLL' in metrics: - if ((S2 is not None) and (mY is not None) and (sY is not None)): - MSLL = np.zeros(feature_num) - MSLL[nz] = compute_MSLL(Y[:,nz], Yhat[:,nz], S2[:,nz], - mY.reshape(-1,1).T, - (sY**2).reshape(-1,1).T) - results['MSLL'] = MSLL - - if 'NLL' in metrics: - results['NLL'] = nlZ - - if 'BIC' in metrics: - if hasattr(getattr(nm, alg), 'hyp'): - n = Xz_tr.shape[0] - k = len(getattr(nm, alg).hyp) - BIC = k * np.log(n) + 2 * nlZ - results['BIC'] = BIC - - return results - -def save_results(respfile, Yhat, S2, maskvol, Z=None, outputsuffix=None, - results=None, save_path=''): - - print("Writing outputs ...") - if respfile is None: - exfile = None - file_ext = '.pkl' - else: - if fileio.file_type(respfile) == 'cifti' or \ - fileio.file_type(respfile) == 'nifti': - exfile = respfile - else: - exfile = None - file_ext = fileio.file_extension(respfile) - - if outputsuffix is not None: - ext = str(outputsuffix) + file_ext - else: - ext = file_ext - - fileio.save(Yhat, os.path.join(save_path, 'yhat' + ext), example=exfile, - mask=maskvol) - fileio.save(S2, os.path.join(save_path, 'ys2' + ext), example=exfile, - mask=maskvol) - if Z is not None: - fileio.save(Z, os.path.join(save_path, 'Z' + ext), example=exfile, - mask=maskvol) - - if results is not None: - for metric in list(results.keys()): - if (metric == 'NLL' or metric == 'BIC') and file_ext == '.nii.gz': - fileio.save(results[metric], os.path.join(save_path, metric + str(outputsuffix) + '.pkl'), - example=exfile, mask=maskvol) - else: - fileio.save(results[metric], os.path.join(save_path, metric + ext), - example=exfile, mask=maskvol) - -def estimate(covfile, respfile, **kwargs): - """ Estimate a normative model - - This will estimate a model in one of two settings according to - theparticular parameters specified (see below) - - * under k-fold cross-validation. - requires respfile, covfile and cvfolds>=2 - * estimating a training dataset then applying to a second test dataset. - requires respfile, covfile, testcov and testresp. - * estimating on a training dataset ouput of forward maps mean and se. - requires respfile, covfile and testcov - - The models are estimated on the basis of data stored on disk in ascii or - neuroimaging data formats (nifti or cifti). Ascii data should be in - tab or space delimited format with the number of subjects in rows and the - number of variables in columns. Neuroimaging data will be reshaped - into the appropriate format - - Basic usage:: - - estimate(covfile, respfile, [extra_arguments]) - - where the variables are defined below. Note that either the cfolds - parameter or (testcov, testresp) should be specified, but not both. - - :param respfile: response variables for the normative model - :param covfile: covariates used to predict the response variable - :param maskfile: mask used to apply to the data (nifti only) - :param cvfolds: Number of cross-validation folds - :param testcov: Test covariates - :param testresp: Test responses - :param alg: Algorithm for normative model - :param configparam: Parameters controlling the estimation algorithm - :param saveoutput: Save the output to disk? Otherwise returned as arrays - :param outputsuffix: Text string to add to the output filenames - :param inscale: Scaling approach for input covariates, could be 'None' (Default), - 'standardize', 'minmax', or 'robminmax'. - :param outscale: Scaling approach for output responses, could be 'None' (Default), - 'standardize', 'minmax', or 'robminmax'. - - All outputs are written to disk in the same format as the input. These are: - - :outputs: * 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 - - The outputsuffix may be useful to estimate multiple normative models in the - same directory (e.g. for custom cross-validation schemes) - """ - - # parse keyword arguments - maskfile = kwargs.pop('maskfile',None) - cvfolds = kwargs.pop('cvfolds', None) - testcov = kwargs.pop('testcov', None) - testresp = kwargs.pop('testresp',None) - alg = kwargs.pop('alg','gpr') - outputsuffix = kwargs.pop('outputsuffix','estimate') - outputsuffix = "_" + outputsuffix.replace("_", "") # Making sure there is only one - # '_' is in the outputsuffix to - # avoid file name parsing problem. - inscaler = kwargs.pop('inscaler','None') - outscaler = kwargs.pop('outscaler','None') - warp = kwargs.get('warp', None) - - # convert from strings if necessary - saveoutput = kwargs.pop('saveoutput','True') - if type(saveoutput) is str: - saveoutput = saveoutput=='True' - savemodel = kwargs.pop('savemodel','False') - if type(savemodel) is str: - savemodel = savemodel=='True' - - if savemodel and not os.path.isdir('Models'): - os.mkdir('Models') - - # which output metrics to compute - metrics = ['Rho', 'RMSE', 'SMSE', 'EXPV', 'MSLL','NLL', 'BIC'] - - # load data - print("Processing data in " + respfile) - X = fileio.load(covfile) - Y, maskvol = load_response_vars(respfile, maskfile) - if len(Y.shape) == 1: - Y = Y[:, np.newaxis] - if len(X.shape) == 1: - X = X[:, np.newaxis] - Nmod = Y.shape[1] - - if (testcov is not None) and (cvfolds is None): # a separate test dataset - - run_cv = False - cvfolds = 1 - Xte = fileio.load(testcov) - if len(Xte.shape) == 1: - Xte = Xte[:, np.newaxis] - if testresp is not None: - Yte, testmask = load_response_vars(testresp, maskfile) - if len(Yte.shape) == 1: - Yte = Yte[:, np.newaxis] - else: - sub_te = Xte.shape[0] - Yte = np.zeros([sub_te, Nmod]) - - # treat as a single train-test split - testids = range(X.shape[0], X.shape[0]+Xte.shape[0]) - splits = CustomCV((range(0, X.shape[0]),), (testids,)) - - Y = np.concatenate((Y, Yte), axis=0) - X = np.concatenate((X, Xte), axis=0) - - else: - run_cv = True - # we are running under cross-validation - splits = KFold(n_splits=cvfolds, shuffle=True) - testids = range(0, X.shape[0]) - if alg=='hbr': - trbefile = kwargs.get('trbefile', None) - if trbefile is not None: - be = fileio.load(trbefile) - if len(be.shape) == 1: - be = be[:, np.newaxis] - else: - print('No batch-effects file! Initilizing all as zeros!') - be = np.zeros([X.shape[0],1]) - - # find and remove bad variables from the response variables - # note: the covariates are assumed to have already been checked - nz = np.where(np.bitwise_and(np.isfinite(Y).any(axis=0), - np.var(Y, axis=0) != 0))[0] - - # run cross-validation loop - Yhat = np.zeros_like(Y) - S2 = np.zeros_like(Y) - Z = np.zeros_like(Y) - nlZ = np.zeros((Nmod, cvfolds)) - - scaler_resp = [] - scaler_cov = [] - mean_resp = [] # this is just for computing MSLL - std_resp = [] # this is just for computing MSLL - - if warp is not None: - Ywarp = np.zeros_like(Yhat) - - # for warping we need to compute metrics separately for each fold - results_folds = dict() - for m in metrics: - results_folds[m]= np.zeros((Nmod, cvfolds)) - - for idx in enumerate(splits.split(X)): - - fold = idx[0] - tr = idx[1][0] - ts = idx[1][1] - - # standardize responses and covariates, ignoring invalid entries - iy_tr, jy_tr = np.ix_(tr, nz) - iy_ts, jy_ts = np.ix_(ts, nz) - mY = np.mean(Y[iy_tr, jy_tr], axis=0) - sY = np.std(Y[iy_tr, jy_tr], axis=0) - mean_resp.append(mY) - std_resp.append(sY) - - if inscaler in ['standardize', 'minmax', 'robminmax']: - X_scaler = scaler(inscaler) - Xz_tr = X_scaler.fit_transform(X[tr, :]) - Xz_ts = X_scaler.transform(X[ts, :]) - scaler_cov.append(X_scaler) - else: - Xz_tr = X[tr, :] - Xz_ts = X[ts, :] - - if outscaler in ['standardize', 'minmax', 'robminmax']: - Y_scaler = scaler(outscaler) - Yz_tr = Y_scaler.fit_transform(Y[iy_tr, jy_tr]) - scaler_resp.append(Y_scaler) - else: - Yz_tr = Y[iy_tr, jy_tr] - - if (run_cv==True and alg=='hbr'): - fileio.save(be[tr,:], 'be_kfold_tr_tempfile.pkl') - fileio.save(be[ts,:], 'be_kfold_ts_tempfile.pkl') - kwargs['trbefile'] = 'be_kfold_tr_tempfile.pkl' - kwargs['tsbefile'] = 'be_kfold_ts_tempfile.pkl' - - # estimate the models for all subjects - for i in range(0, len(nz)): - print("Estimating model ", i+1, "of", len(nz)) - nm = norm_init(Xz_tr, Yz_tr[:, i], alg=alg, **kwargs) - - try: - nm = nm.estimate(Xz_tr, Yz_tr[:, i], **kwargs) - yhat, s2 = nm.predict(Xz_ts, Xz_tr, Yz_tr[:, i], **kwargs) - - if savemodel: - nm.save('Models/NM_' + str(fold) + '_' + str(nz[i]) + - outputsuffix + '.pkl' ) - - if outscaler == 'standardize': - Yhat[ts, nz[i]] = Y_scaler.inverse_transform(yhat, index=i) - S2[ts, nz[i]] = s2 * sY[i]**2 - elif outscaler in ['minmax', 'robminmax']: - Yhat[ts, nz[i]] = Y_scaler.inverse_transform(yhat, index=i) - S2[ts, nz[i]] = s2 * (Y_scaler.max[i] - Y_scaler.min[i])**2 - else: - Yhat[ts, nz[i]] = yhat - S2[ts, nz[i]] = s2 - - nlZ[nz[i], fold] = nm.neg_log_lik - - if (run_cv or testresp is not None): - if warp is not None: - # TODO: Warping for scaled data - if outscaler is not None and outscaler != 'None': - raise(ValueError, "outscaler not yet supported warping") - warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] - Ywarp[ts, nz[i]] = nm.blr.warp.f(Y[ts, nz[i]], warp_param) - Ytest = Ywarp[ts, nz[i]] - - # Save warped mean of the training data (for MSLL) - yw = nm.blr.warp.f(Y[tr, nz[i]], warp_param) - - # create arrays for evaluation - Yhati = Yhat[ts, nz[i]] - Yhati = Yhati[:, np.newaxis] - S2i = S2[ts, nz[i]] - S2i = S2i[:, np.newaxis] - - # evaluate and save results - mf = evaluate(Ytest[:, np.newaxis], Yhati, S2=S2i, - mY=np.mean(yw), sY=np.std(yw), - nlZ=nm.neg_log_lik, nm=nm, Xz_tr=Xz_tr, - alg=alg, metrics = metrics) - for k in metrics: - results_folds[k][nz[i]][fold] = mf[k] - else: - Ytest = Y[ts, nz[i]] - - Z[ts, nz[i]] = (Ytest - Yhat[ts, nz[i]]) / \ - np.sqrt(S2[ts, nz[i]]) - - except Exception as e: - exc_type, exc_obj, exc_tb = sys.exc_info() - fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1] - print("Model ", i+1, "of", len(nz), - "FAILED!..skipping and writing NaN to outputs") - print("Exception:") - print(e) - print(exc_type, fname, exc_tb.tb_lineno) - - Yhat[ts, nz[i]] = float('nan') - S2[ts, nz[i]] = float('nan') - nlZ[nz[i], fold] = float('nan') - if testcov is None: - Z[ts, nz[i]] = float('nan') - else: - if testresp is not None: - Z[ts, nz[i]] = float('nan') - - - if savemodel: - print('Saving model meta-data...') - v = get_package_versions() - with open('Models/meta_data.md', 'wb') as file: - pickle.dump({'valid_voxels':nz, 'fold_num':cvfolds, - 'mean_resp':mean_resp, 'std_resp':std_resp, - 'scaler_cov':scaler_cov, 'scaler_resp':scaler_resp, - 'regressor':alg, 'inscaler':inscaler, - 'outscaler':outscaler, 'versions':v}, - file, protocol=PICKLE_PROTOCOL) - - # compute performance metrics - if (run_cv or testresp is not None): - print("Evaluating the model ...") - if warp is None: - results = evaluate(Y[testids, :], Yhat[testids, :], - S2=S2[testids, :], mY=mean_resp[0], - sY=std_resp[0], nlZ=nlZ, nm=nm, Xz_tr=Xz_tr, alg=alg, - metrics = metrics) - else: - # for warped data we just aggregate across folds - results = dict() - for m in ['Rho', 'RMSE', 'SMSE', 'EXPV', 'MSLL']: - results[m] = np.mean(results_folds[m], axis=1) - results['NLL'] = results_folds['NLL'] - results['BIC'] = results_folds['BIC'] - - # Set writing options - if saveoutput: - if (run_cv or testresp is not None): - save_results(respfile, Yhat[testids, :], S2[testids, :], maskvol, - Z=Z[testids, :], results=results, - outputsuffix=outputsuffix) - - else: - save_results(respfile, Yhat[testids, :], S2[testids, :], maskvol, - outputsuffix=outputsuffix) - - else: - if (run_cv or testresp is not None): - output = (Yhat[testids, :], S2[testids, :], nm, Z[testids, :], - results) - else: - output = (Yhat[testids, :], S2[testids, :], nm) - - return output - - -def fit(covfile, respfile, **kwargs): - - # parse keyword arguments - maskfile = kwargs.pop('maskfile',None) - alg = kwargs.pop('alg','gpr') - savemodel = kwargs.pop('savemodel','True')=='True' - outputsuffix = kwargs.pop('outputsuffix','fit') - outputsuffix = "_" + outputsuffix.replace("_", "") - inscaler = kwargs.pop('inscaler','None') - outscaler = kwargs.pop('outscaler','None') - - if savemodel and not os.path.isdir('Models'): - os.mkdir('Models') - - # load data - print("Processing data in " + respfile) - X = fileio.load(covfile) - Y, maskvol = load_response_vars(respfile, maskfile) - if len(Y.shape) == 1: - Y = Y[:, np.newaxis] - if len(X.shape) == 1: - X = X[:, np.newaxis] - - # find and remove bad variables from the response variables - # note: the covariates are assumed to have already been checked - nz = np.where(np.bitwise_and(np.isfinite(Y).any(axis=0), - np.var(Y, axis=0) != 0))[0] - - scaler_resp = [] - scaler_cov = [] - mean_resp = [] # this is just for computing MSLL - std_resp = [] # this is just for computing MSLL - - # standardize responses and covariates, ignoring invalid entries - mY = np.mean(Y[:, nz], axis=0) - sY = np.std(Y[:, nz], axis=0) - mean_resp.append(mY) - std_resp.append(sY) - - if inscaler in ['standardize', 'minmax', 'robminmax']: - X_scaler = scaler(inscaler) - Xz = X_scaler.fit_transform(X) - scaler_cov.append(X_scaler) - else: - Xz = X - - if outscaler in ['standardize', 'minmax', 'robminmax']: - Yz = np.zeros_like(Y) - Y_scaler = scaler(outscaler) - Yz[:, nz] = Y_scaler.fit_transform(Y[:, nz]) - scaler_resp.append(Y_scaler) - else: - Yz = Y - - # estimate the models for all subjects - for i in range(0, len(nz)): - print("Estimating model ", i+1, "of", len(nz)) - nm = norm_init(Xz, Yz[:, nz[i]], alg=alg, **kwargs) - nm = nm.estimate(Xz, Yz[:, nz[i]], **kwargs) - - if savemodel: - nm.save('Models/NM_' + str(0) + '_' + str(nz[i]) + outputsuffix + - '.pkl' ) - - if savemodel: - print('Saving model meta-data...') - v = get_package_versions() - with open('Models/meta_data.md', 'wb') as file: - pickle.dump({'valid_voxels':nz, - 'mean_resp':mean_resp, 'std_resp':std_resp, - 'scaler_cov':scaler_cov, 'scaler_resp':scaler_resp, - 'regressor':alg, 'inscaler':inscaler, - 'outscaler':outscaler, 'versions':v}, - file, protocol=PICKLE_PROTOCOL) - - return nm - - -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 - will be returned. If the test responses are also specified then quantities - That depend on those will also be returned (Z scores and error metrics) - - Basic usage:: - - predict(covfile, [extra_arguments]) - - where the variables are defined below. - - :param covfile: test covariates used to predict the response variable - :param respfile: test response variables for the normative model - :param maskfile: mask used to apply to the data (nifti only) - :param model_path: Directory containing the normative model and metadata. - When using parallel prediction, do not pass the model path. It will be - automatically decided. - :param outputsuffix: Text string to add to the output filenames - :param batch_size: batch size (for use with normative_parallel) - :param job_id: batch id - :param fold: which cross-validation fold to use (default = 0) - :param fold: list of model IDs to predict (if not specified all are computed) - - All outputs are written to disk in the same format as the input. These are: - - :outputs: * Yhat - predictive mean - * S2 - predictive variance - * Z - Z scores - ''' - - - model_path = kwargs.pop('model_path', 'Models') - job_id = kwargs.pop('job_id', None) - batch_size = kwargs.pop('batch_size', None) - outputsuffix = kwargs.pop('outputsuffix', 'predict') - outputsuffix = "_" + outputsuffix.replace("_", "") - inputsuffix = kwargs.pop('inputsuffix', 'estimate') - inputsuffix = "_" + inputsuffix.replace("_", "") - alg = kwargs.pop('alg') - fold = kwargs.pop('fold',0) - models = kwargs.pop('models', None) - - if alg == 'gpr': - raise(ValueError, "gpr is not supported with predict()") - - if respfile is not None and not os.path.exists(respfile): - print("Response file does not exist. Only returning predictions") - respfile = None - if not os.path.isdir(model_path): - print('Models directory does not exist!') - return - else: - if os.path.exists(os.path.join(model_path, 'meta_data.md')): - with open(os.path.join(model_path, 'meta_data.md'), 'rb') as file: - meta_data = pickle.load(file) - inscaler = meta_data['inscaler'] - outscaler = meta_data['outscaler'] - mY = meta_data['mean_resp'] - sY = meta_data['std_resp'] - scaler_cov = meta_data['scaler_cov'] - scaler_resp = meta_data['scaler_resp'] - meta_data = True - else: - print("No meta-data file is found!") - inscaler = 'None' - outscaler = 'None' - meta_data = False - - if batch_size is not None: - batch_size = int(batch_size) - job_id = int(job_id) - 1 - - - # load data - print("Loading data ...") - X = fileio.load(covfile) - if len(X.shape) == 1: - X = X[:, np.newaxis] - - sample_num = X.shape[0] - if models is not None: - feature_num = len(models) - else: - feature_num = len(glob.glob(os.path.join(model_path, 'NM_'+ str(fold) + - '_*' + inputsuffix + '.pkl'))) - models = range(feature_num) - - Yhat = np.zeros([sample_num, feature_num]) - S2 = np.zeros([sample_num, feature_num]) - Z = np.zeros([sample_num, feature_num]) - - if inscaler in ['standardize', 'minmax', 'robminmax']: - Xz = scaler_cov[fold].transform(X) - else: - Xz = X - - # estimate the models for all subjects - for i, m in enumerate(models): - print("Prediction by model ", i+1, "of", feature_num) - nm = norm_init(Xz) - nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + - str(m) + inputsuffix + '.pkl')) - if (alg!='hbr' or nm.configs['transferred']==False): - yhat, s2 = nm.predict(Xz, **kwargs) - else: - tsbefile = kwargs.get('tsbefile') - batch_effects_test = fileio.load(tsbefile) - yhat, s2 = nm.predict_on_new_sites(Xz, batch_effects_test) - - if outscaler == 'standardize': - Yhat[:, i] = scaler_resp[fold].inverse_transform(yhat, index=i) - S2[:, i] = s2.squeeze() * sY[fold][i]**2 - elif outscaler in ['minmax', 'robminmax']: - Yhat[:, i] = scaler_resp[fold].inverse_transform(yhat, index=i) - S2[:, i] = s2 * (scaler_resp[fold].max[i] - scaler_resp[fold].min[i])**2 - else: - Yhat[:, i] = yhat.squeeze() - S2[:, i] = s2.squeeze() - - if respfile is None: - save_results(None, Yhat, S2, None, outputsuffix=outputsuffix) - - return (Yhat, S2) - - else: - Y, maskvol = load_response_vars(respfile, maskfile) - if models is not None and len(Y.shape) > 1: - Y = Y[:, models] - if meta_data: - # are we using cross-validation? - if type(mY) is list: - mY = mY[fold][models] - else: - mY = mY[models] - if type(sY) is list: - sY = sY[fold][models] - else: - sY = sY[models] - - if len(Y.shape) == 1: - Y = Y[:, np.newaxis] - - # warp the targets? - if alg == 'blr' and nm.blr.warp is not None: - warp = True - Yw = np.zeros_like(Y) - for i,m in enumerate(models): - nm = norm_init(Xz) - nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + - str(m) + inputsuffix + '.pkl')) - - warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] - Yw[:,i] = nm.blr.warp.f(Y[:,i], warp_param) - Y = Yw; - else: - warp = False - - Z = (Y - Yhat) / np.sqrt(S2) - - print("Evaluating the model ...") - if meta_data and not warp: - - results = evaluate(Y, Yhat, S2=S2, mY=mY, sY=sY) - else: - results = evaluate(Y, Yhat, S2=S2, - metrics = ['Rho', 'RMSE', 'SMSE', 'EXPV']) - - print("Evaluations Writing outputs ...") - save_results(respfile, Yhat, S2, maskvol, Z=Z, - outputsuffix=outputsuffix, results=results) - - return (Yhat, S2, Z) - - -def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, - **kwargs): - ''' - Transfer learning on the basis of a pre-estimated normative model by using - the posterior distribution over the parameters as an informed prior for - new data. currently only supported for HBR. - - Basic usage:: - - transfer(covfile, respfile [extra_arguments]) - - where the variables are defined below. - - :param covfile: transfer covariates used to predict the response variable - :param respfile: transfer response variables for the normative model - :param maskfile: mask used to apply to the data (nifti only) - :param testcov: Test covariates - :param testresp: Test responses - :param model_path: Directory containing the normative model and metadata - :param trbefile: Training batch effects file - :param batch_size: batch size (for use with normative_parallel) - :param job_id: batch id - - All outputs are written to disk in the same format as the input. These are: - - :outputs: * Yhat - predictive mean - * S2 - predictive variance - * Z - Z scores - ''' - alg = kwargs.pop('alg').lower() - - if alg != 'hbr' and alg != 'blr': - print('Model transfer function is only possible for HBR and BLR models.') - return - # testing should not be obligatory for HBR, - # but should be for BLR (since it doesn't produce transfer models) - elif (not 'model_path' in list(kwargs.keys())) or \ - (not 'trbefile' in list(kwargs.keys())): - print(f'{kwargs=}') - print('InputError: Some general mandatory arguments are missing.') - return - # hbr has one additional mandatory arguments - elif alg =='hbr': - if (not 'output_path' in list(kwargs.keys())): - print('InputError: Some mandatory arguments for hbr are missing.') - return - else: - output_path = kwargs.pop('output_path',None) - if not os.path.isdir(output_path): - os.mkdir(output_path) - - # for hbr, testing is not mandatory, for blr's predict/transfer it is. This will be an architectural choice. - #or (testresp==None) - elif alg =='blr': - if (testcov==None) or \ - (not 'tsbefile' in list(kwargs.keys())): - print('InputError: Some mandatory arguments for blr are missing.') - return - # general arguments - log_path = kwargs.pop('log_path', None) - model_path = kwargs.pop('model_path') - outputsuffix = kwargs.pop('outputsuffix', 'transfer') - outputsuffix = "_" + outputsuffix.replace("_", "") - inputsuffix = kwargs.pop('inputsuffix', 'estimate') - inputsuffix = "_" + inputsuffix.replace("_", "") - tsbefile = kwargs.pop('tsbefile', None) - trbefile = kwargs.pop('trbefile', None) - job_id = kwargs.pop('job_id', None) - batch_size = kwargs.pop('batch_size', None) - fold = kwargs.pop('fold',0) - - # for PCNonline automated parallel jobs loop - count_jobsdone = kwargs.pop('count_jobsdone','False') - if type(count_jobsdone) is str: - count_jobsdone = count_jobsdone=='True' - - if batch_size is not None: - batch_size = int(batch_size) - job_id = int(job_id) - 1 - - if not os.path.isdir(model_path): - print('Models directory does not exist!') - return - else: - if os.path.exists(os.path.join(model_path, 'meta_data.md')): - with open(os.path.join(model_path, 'meta_data.md'), 'rb') as file: - meta_data = pickle.load(file) - inscaler = meta_data['inscaler'] - outscaler = meta_data['outscaler'] - scaler_cov = meta_data['scaler_cov'] - scaler_resp = meta_data['scaler_resp'] - meta_data = True - else: - print("No meta-data file is found!") - inscaler = 'None' - outscaler = 'None' - meta_data = False - - # load adaptation data - print("Loading data ...") - X = fileio.load(covfile) - Y, maskvol = load_response_vars(respfile, maskfile) - if len(Y.shape) == 1: - Y = Y[:, np.newaxis] - if len(X.shape) == 1: - X = X[:, np.newaxis] - - if inscaler in ['standardize', 'minmax', 'robminmax']: - X = scaler_cov[0].transform(X) - - feature_num = Y.shape[1] - mY = np.mean(Y, axis=0) - sY = np.std(Y, axis=0) - - if outscaler in ['standardize', 'minmax', 'robminmax']: - Y = scaler_resp[0].transform(Y) - - batch_effects_train = fileio.load(trbefile) - - # load test data - if testcov is not None: - # we have a separate test dataset - Xte = fileio.load(testcov) - if len(Xte.shape) == 1: - Xte = Xte[:, np.newaxis] - ts_sample_num = Xte.shape[0] - if inscaler in ['standardize', 'minmax', 'robminmax']: - Xte = scaler_cov[0].transform(Xte) - - if testresp is not None: - Yte, testmask = load_response_vars(testresp, maskfile) - if len(Yte.shape) == 1: - Yte = Yte[:, np.newaxis] - else: - Yte = np.zeros([ts_sample_num, feature_num]) - - if tsbefile is not None: - batch_effects_test = fileio.load(tsbefile) - else: - batch_effects_test = np.zeros([Xte.shape[0],2]) - else: - ts_sample_num = 0 - - Yhat = np.zeros([ts_sample_num, feature_num]) - S2 = np.zeros([ts_sample_num, feature_num]) - Z = np.zeros([ts_sample_num, feature_num]) - - # estimate the models for all subjects - for i in range(feature_num): - - if alg == 'hbr': - print("Using HBR transform...") - nm = norm_init(X) - if batch_size is not None: # when using normative_parallel - print("Transferring model ", job_id*batch_size+i) - nm = nm.load(os.path.join(model_path, 'NM_0_' + - str(job_id*batch_size+i) + inputsuffix + - '.pkl')) - else: - print("Transferring model ", i+1, "of", feature_num) - nm = nm.load(os.path.join(model_path, 'NM_0_' + str(i) + - inputsuffix + '.pkl')) - - nm = nm.estimate_on_new_sites(X, Y[:,i], batch_effects_train) - if batch_size is not None: - nm.save(os.path.join(output_path, 'NM_0_' + - str(job_id*batch_size+i) + outputsuffix + '.pkl')) - else: - nm.save(os.path.join(output_path, 'NM_0_' + - str(i) + outputsuffix + '.pkl')) - - if testcov is not None: - yhat, s2 = nm.predict_on_new_sites(Xte, batch_effects_test) - - # We basically use normative.predict script here. - if alg == 'blr': - print("Using BLR transform...") - print("Transferring model ", i+1, "of", feature_num) - nm = norm_init(X) - nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + - str(i) + inputsuffix + '.pkl')) - - # translate the syntax to what blr understands - # first strip existing blr keyword arguments to avoid redundancy - adapt_cov = kwargs.pop('adaptcovfile', None) - adapt_res = kwargs.pop('adaptrespfile', None) - adapt_vg = kwargs.pop('adaptvargroupfile', None) - test_vg = kwargs.pop('testvargroupfile', None) - if adapt_cov is not None or adapt_res is not None \ - or adapt_vg is not None or test_vg is not None: - print("Warning: redundant batch effect parameterisation. Using HBR syntax") - - yhat, s2 = nm.predict(Xte, X, Y[:, i], - adaptcov = X, - adaptresp = Y[:, i], - adaptvargroup = batch_effects_train, - testvargroup = batch_effects_test, - **kwargs) - - if testcov is not None: - if outscaler == 'standardize': - Yhat[:, i] = scaler_resp[0].inverse_transform(yhat.squeeze(), index=i) - S2[:, i] = s2.squeeze() * sY[i]**2 - elif outscaler in ['minmax', 'robminmax']: - Yhat[:, i] = scaler_resp[0].inverse_transform(yhat, index=i) - S2[:, i] = s2 * (scaler_resp[0].max[i] - scaler_resp[0].min[i])**2 - else: - Yhat[:, i] = yhat.squeeze() - S2[:, i] = s2.squeeze() - - # Creates a file for every job succesfully completed (for tracking failed jobs). - if count_jobsdone==True: - done_path = os.path.join(log_path, str(job_id)+".jobsdone") - Path(done_path).touch() - - - if testresp is None: - save_results(respfile, Yhat, S2, maskvol, outputsuffix=outputsuffix) - return (Yhat, S2) - else: - # warp the targets? - if alg == 'blr' and nm.blr.warp is not None: - warp = True - Yw = np.zeros_like(Yte) - for i in range(feature_num): - nm = norm_init(Xte) - nm = nm.load(os.path.join(model_path, 'NM_' + str(fold) + '_' + - str(i) + inputsuffix + '.pkl')) - - warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] - Yw[:,i] = nm.blr.warp.f(Yte[:,i], warp_param) - Yte = Yw; - else: - warp = False - - Z = (Yte - Yhat) / np.sqrt(S2) - - print("Evaluating the model ...") - if meta_data and not warp: - results = evaluate(Yte, Yhat, S2=S2, mY=mY, sY=sY) - else: - results = evaluate(Yte, Yhat, S2=S2, - metrics = ['Rho', 'RMSE', 'SMSE', 'EXPV']) - - save_results(respfile, Yhat, S2, maskvol, Z=Z, results=results, - outputsuffix=outputsuffix) - - return (Yhat, S2, Z) - - -def extend(covfile, respfile, maskfile=None, **kwargs): - - ''' - This function extends an existing HBR model with data from new sites/scanners. - - Basic usage:: - - extend(covfile, respfile [extra_arguments]) - - where the variables are defined below. - - :param covfile: covariates for new data - :param respfile: response variables for new data - :param maskfile: mask used to apply to the data (nifti only) - :param model_path: Directory containing the normative model and metadata - :param trbefile: file address to batch effects file for new data - :param batch_size: batch size (for use with normative_parallel) - :param job_id: batch id - :param output_path: the path for saving the the extended model - :param informative_prior: use initial model prior or learn from scratch (default is False). - :param generation_factor: see below - - generation factor refers to the number of samples generated for each - combination of covariates and batch effects. Default is 10. - - - All outputs are written to disk in the same format as the input. - - ''' - - alg = kwargs.pop('alg') - if alg != 'hbr': - print('Model extention is only possible for HBR models.') - return - elif (not 'model_path' in list(kwargs.keys())) or \ - (not 'output_path' in list(kwargs.keys())) or \ - (not 'trbefile' in list(kwargs.keys())): - print('InputError: Some mandatory arguments are missing.') - return - else: - model_path = kwargs.pop('model_path') - output_path = kwargs.pop('output_path') - trbefile = kwargs.pop('trbefile') - - outputsuffix = kwargs.pop('outputsuffix', 'extend') - outputsuffix = "_" + outputsuffix.replace("_", "") - inputsuffix = kwargs.pop('inputsuffix', 'estimate') - inputsuffix = "_" + inputsuffix.replace("_", "") - informative_prior = kwargs.pop('informative_prior', 'False') == 'True' - generation_factor = int(kwargs.pop('generation_factor', '10')) - job_id = kwargs.pop('job_id', None) - batch_size = kwargs.pop('batch_size', None) - if batch_size is not None: - batch_size = int(batch_size) - job_id = int(job_id) - 1 - - if not os.path.isdir(model_path): - print('Models directory does not exist!') - return - else: - if os.path.exists(os.path.join(model_path, 'meta_data.md')): - with open(os.path.join(model_path, 'meta_data.md'), 'rb') as file: - meta_data = pickle.load(file) - if (meta_data['inscaler'] != 'None' or - meta_data['outscaler'] != 'None'): - print('Models extention on scaled data is not possible!') - return - - if not os.path.isdir(output_path): - os.mkdir(output_path) - - # load data - print("Loading data ...") - X = fileio.load(covfile) - Y, maskvol = load_response_vars(respfile, maskfile) - batch_effects_train = fileio.load(trbefile) - - if len(Y.shape) == 1: - Y = Y[:, np.newaxis] - if len(X.shape) == 1: - X = X[:, np.newaxis] - feature_num = Y.shape[1] - - # estimate the models for all subjects - for i in range(feature_num): - - nm = norm_init(X) - if batch_size is not None: # when using nirmative_parallel - print("Extending model ", job_id*batch_size+i) - nm = nm.load(os.path.join(model_path, 'NM_0_' + - str(job_id*batch_size+i) + inputsuffix + - '.pkl')) - else: - print("Extending model ", i+1, "of", feature_num) - nm = nm.load(os.path.join(model_path, 'NM_0_' + str(i) + - inputsuffix +'.pkl')) - - nm = nm.extend(X, Y[:,i:i+1], batch_effects_train, - samples=generation_factor, - informative_prior=informative_prior) - - if batch_size is not None: - nm.save(os.path.join(output_path, 'NM_0_' + - str(job_id*batch_size+i) + outputsuffix + '.pkl')) - nm.save(os.path.join('Models', 'NM_0_' + - str(i) + outputsuffix + '.pkl')) - else: - nm.save(os.path.join(output_path, 'NM_0_' + - str(i) + outputsuffix + '.pkl')) - - - -def tune(covfile, respfile, maskfile=None, **kwargs): - - ''' - This function tunes an existing HBR model with real data. - - Basic usage:: - - tune(covfile, respfile [extra_arguments]) - - where the variables are defined below. - - :param covfile: covariates for new data - :param respfile: response variables for new data - :param maskfile: mask used to apply to the data (nifti only) - :param model_path: Directory containing the normative model and metadata - :param trbefile: file address to batch effects file for new data - :param batch_size: batch size (for use with normative_parallel) - :param job_id: batch id - :param output_path: the path for saving the the extended model - :param informative_prior: use initial model prior or learn from scracth (default is False). - :param generation_factor: see below - - - generation factor refers to the number of samples generated for each - combination of covariates and batch effects. Default is 10. - - - All outputs are written to disk in the same format as the input. - - ''' - - alg = kwargs.pop('alg') - if alg != 'hbr': - print('Model extention is only possible for HBR models.') - return - elif (not 'model_path' in list(kwargs.keys())) or \ - (not 'output_path' in list(kwargs.keys())) or \ - (not 'trbefile' in list(kwargs.keys())): - print('InputError: Some mandatory arguments are missing.') - return - else: - model_path = kwargs.pop('model_path') - output_path = kwargs.pop('output_path') - trbefile = kwargs.pop('trbefile') - - outputsuffix = kwargs.pop('outputsuffix', 'tuned') - outputsuffix = "_" + outputsuffix.replace("_", "") - inputsuffix = kwargs.pop('inputsuffix', 'estimate') - inputsuffix = "_" + inputsuffix.replace("_", "") - informative_prior = kwargs.pop('informative_prior', 'False') == 'True' - generation_factor = int(kwargs.pop('generation_factor', '10')) - job_id = kwargs.pop('job_id', None) - batch_size = kwargs.pop('batch_size', None) - if batch_size is not None: - batch_size = int(batch_size) - job_id = int(job_id) - 1 - - if not os.path.isdir(model_path): - print('Models directory does not exist!') - return - else: - if os.path.exists(os.path.join(model_path, 'meta_data.md')): - with open(os.path.join(model_path, 'meta_data.md'), 'rb') as file: - meta_data = pickle.load(file) - if (meta_data['inscaler'] != 'None' or - meta_data['outscaler'] != 'None'): - print('Models extention on scaled data is not possible!') - return - - if not os.path.isdir(output_path): - os.mkdir(output_path) - - # load data - print("Loading data ...") - X = fileio.load(covfile) - Y, maskvol = load_response_vars(respfile, maskfile) - batch_effects_train = fileio.load(trbefile) - - if len(Y.shape) == 1: - Y = Y[:, np.newaxis] - if len(X.shape) == 1: - X = X[:, np.newaxis] - feature_num = Y.shape[1] - - # estimate the models for all subjects - for i in range(feature_num): - - nm = norm_init(X) - if batch_size is not None: # when using nirmative_parallel - print("Tuning model ", job_id*batch_size+i) - nm = nm.load(os.path.join(model_path, 'NM_0_' + - str(job_id*batch_size+i) + inputsuffix + - '.pkl')) - else: - print("Tuning model ", i+1, "of", feature_num) - nm = nm.load(os.path.join(model_path, 'NM_0_' + str(i) + - inputsuffix +'.pkl')) - - nm = nm.tune(X, Y[:,i:i+1], batch_effects_train, - samples=generation_factor, - informative_prior=informative_prior) - - if batch_size is not None: - nm.save(os.path.join(output_path, 'NM_0_' + - str(job_id*batch_size+i) + outputsuffix + '.pkl')) - nm.save(os.path.join('Models', 'NM_0_' + - str(i) + outputsuffix + '.pkl')) - else: - nm.save(os.path.join(output_path, 'NM_0_' + - str(i) + outputsuffix + '.pkl')) - - -def merge(covfile=None, respfile=None, **kwargs): - - ''' - This function extends an existing HBR model with data from new sites/scanners. - - Basic usage:: - - merge(model_path1, model_path2 [extra_arguments]) - - where the variables are defined below. - - :param covfile: Not required. Always set to None. - :param respfile: Not required. Always set to None. - :param model_path1: Directory containing the model and metadata (1st model) - :param model_path2: Directory containing the model and metadata (2nd model) - :param batch_size: batch size (for use with normative_parallel) - :param job_id: batch id - :param output_path: the path for saving the the extended model - :param generation_factor: see below - - The generation factor refers tothe number of samples generated for each - combination of covariates and batch effects. Default is 10. - - - All outputs are written to disk in the same format as the input. - - ''' - - alg = kwargs.pop('alg') - if alg != 'hbr': - print('Merging models is only possible for HBR models.') - return - elif (not 'model_path1' in list(kwargs.keys())) or \ - (not 'model_path2' in list(kwargs.keys())) or \ - (not 'output_path' in list(kwargs.keys())): - print('InputError: Some mandatory arguments are missing.') - return - else: - model_path1 = kwargs.pop('model_path1') - model_path2 = kwargs.pop('model_path2') - output_path = kwargs.pop('output_path') - - outputsuffix = kwargs.pop('outputsuffix', 'merge') - outputsuffix = "_" + outputsuffix.replace("_", "") - inputsuffix = kwargs.pop('inputsuffix', 'estimate') - inputsuffix = "_" + inputsuffix.replace("_", "") - generation_factor = int(kwargs.pop('generation_factor', '10')) - job_id = kwargs.pop('job_id', None) - batch_size = kwargs.pop('batch_size', None) - if batch_size is not None: - batch_size = int(batch_size) - job_id = int(job_id) - 1 - - if (not os.path.isdir(model_path1)) or (not os.path.isdir(model_path2)): - print('Models directory does not exist!') - return - else: - if batch_size is None: - with open(os.path.join(model_path1, 'meta_data.md'), 'rb') as file: - meta_data1 = pickle.load(file) - with open(os.path.join(model_path2, 'meta_data.md'), 'rb') as file: - meta_data2 = pickle.load(file) - if meta_data1['valid_voxels'].shape[0] != meta_data2['valid_voxels'].shape[0]: - print('Two models are trained on different features!') - return - else: - feature_num = meta_data1['valid_voxels'].shape[0] - else: - feature_num = batch_size - - - if not os.path.isdir(output_path): - os.mkdir(output_path) - - # mergeing the models - for i in range(feature_num): - - nm1 = norm_init(np.random.rand(100,10)) - nm2 = norm_init(np.random.rand(100,10)) - if batch_size is not None: # when using nirmative_parallel - print("Merging model ", job_id*batch_size+i) - nm1 = nm1.load(os.path.join(model_path1, 'NM_0_' + - str(job_id*batch_size+i) + inputsuffix + - '.pkl')) - nm2 = nm2.load(os.path.join(model_path2, 'NM_0_' + - str(job_id*batch_size+i) + inputsuffix + - '.pkl')) - else: - print("Merging model ", i+1, "of", feature_num) - nm1 = nm1.load(os.path.join(model_path1, 'NM_0_' + str(i) + - inputsuffix +'.pkl')) - nm2 = nm1.load(os.path.join(model_path2, 'NM_0_' + str(i) + - inputsuffix +'.pkl')) - - nm_merged = nm1.merge(nm2, samples=generation_factor) - - if batch_size is not None: - nm_merged.save(os.path.join(output_path, 'NM_0_' + - str(job_id*batch_size+i) + outputsuffix + '.pkl')) - nm_merged.save(os.path.join('Models', 'NM_0_' + - str(i) + outputsuffix + '.pkl')) - else: - nm_merged.save(os.path.join(output_path, 'NM_0_' + - str(i) + outputsuffix + '.pkl')) - - -def main(*args): - """ Parse arguments and estimate model - """ - - np.seterr(invalid='ignore') - - rfile, mfile, cfile, cv, tcfile, trfile, func, alg, cfg, kw = get_args(args) - - # collect required arguments - pos_args = ['cfile', 'rfile'] - - # collect basic keyword arguments controlling model estimation - kw_args = ['maskfile=mfile', - 'cvfolds=cv', - 'testcov=tcfile', - 'testresp=trfile', - 'alg=alg', - 'configparam=cfg'] - - # add additional keyword arguments - for k in kw: - kw_args.append(k + '=' + "'" + kw[k] + "'") - all_args = ', '.join(pos_args + kw_args) - - # Executing the target function - exec(func + '(' + all_args + ')') - -# For running from the command line: -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/build/lib/pcntoolkit/normative_NP.py b/build/lib/pcntoolkit/normative_NP.py deleted file mode 100644 index 3694e146..00000000 --- a/build/lib/pcntoolkit/normative_NP.py +++ /dev/null @@ -1,270 +0,0 @@ -#!/usr/bin/env python3 - -# -*- coding: utf-8 -*- -""" -Created on Tue Jun 18 09:47:01 2019 - -@author: seykia -""" -# ------------------------------------------------------------------------------ -# Usage: -# python normative_NP.py -r /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/responses.nii.gz -# -c /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/covariates.pickle -# --tr /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/test_responses.nii.gz -# --tc /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/test_covariates.pickle -# -o /home/preclineu/andmar/data/seykia/ds000030_R1.0.5/Results -# -# -# Written by S. M. Kia -# ------------------------------------------------------------------------------ - -from __future__ import print_function -from __future__ import division - -import sys -import argparse -import torch -from torch import optim -import numpy as np -import pickle -from pcntoolkit.model.NP import NP, apply_dropout_test, np_loss -from sklearn.preprocessing import MinMaxScaler, StandardScaler -from sklearn.linear_model import LinearRegression, MultiTaskLasso -from pcntoolkit.model.architecture import Encoder, Decoder -from pcntoolkit.util.utils import compute_pearsonr, explained_var, compute_MSLL -from pcntoolkit.util.utils import extreme_value_prob, extreme_value_prob_fit, ravel_2D, unravel_2D -from pcntoolkit.dataio import fileio -import os - -try: # run as a package if installed - from pcntoolkit import configs -except ImportError: - pass - - path = os.path.abspath(os.path.dirname(__file__)) - if path not in sys.path: - sys.path.append(path) - del path - import configs - -def get_args(*args): - """ Parse command line arguments""" - - ############################ Parsing inputs ############################### - - parser = argparse.ArgumentParser(description='Neural Processes (NP) for Deep Normative Modeling') - parser.add_argument("-r", help="Training response nifti file address", - required=True, dest="respfile", default=None) - parser.add_argument("-c", help="Training covariates pickle file address", - required=True, dest="covfile", default=None) - parser.add_argument("--tc", help="Test covariates pickle file address", - required=True, dest="testcovfile", default=None) - parser.add_argument("--tr", help="Test response nifti file address", - dest="testrespfile", default=None) - parser.add_argument("--mask", help="Mask nifti file address", - dest="mask", default=None) - parser.add_argument("-o", help="Output directory address", dest="outdir", default=None) - parser.add_argument('-m', type=int, default=10, dest='m', - help='number of fixed-effect estimations') - parser.add_argument('--batchnum', type=int, default=10, dest='batchnum', - help='input batch size for training') - parser.add_argument('--epochs', type=int, default=100, dest='epochs', - help='number of epochs to train') - parser.add_argument('--device', type=str, default='cuda', dest='device', - help='Either cpu or cuda') - parser.add_argument('--fxestimator', type=str, default='ST', dest='estimator', - help='Fixed-effect estimator type.') - - args = parser.parse_args() - - if (args.respfile == None or args.covfile == None or args.testcovfile == None): - raise(ValueError, "Training response nifti file, Training covariates pickle file, and \ - Test covariates pickle file must be specified.") - if (args.outdir == None): - args.outdir = os.getcwd() - - cuda = args.device=='cuda' and torch.cuda.is_available() - args.device = torch.device("cuda" if cuda else "cpu") - args.kwargs = {'num_workers': 1, 'pin_memory': True} if cuda else {} - args.type= 'MT' - - return args - -def estimate(args): - torch.set_default_dtype(torch.float32) - args.type = 'MT' - print('Loading the input Data ...') - responses = fileio.load_nifti(args.respfile, vol=True).transpose([3,0,1,2]) - response_shape = responses.shape - with open(args.covfile, 'rb') as handle: - covariates = pickle.load(handle)['covariates'] - with open(args.testcovfile, 'rb') as handle: - test_covariates = pickle.load(handle)['test_covariates'] - if args.mask is not None: - mask = fileio.load_nifti(args.mask, vol=True) - mask = fileio.create_mask(mask, mask=None) - else: - mask = fileio.create_mask(responses[0,:,:,:], mask=None) - if args.testrespfile is not None: - test_responses = fileio.load_nifti(args.testrespfile, vol=True).transpose([3,0,1,2]) - test_responses_shape = test_responses.shape - - print('Normalizing the input Data ...') - covariates_scaler = StandardScaler() - covariates = covariates_scaler.fit_transform(covariates) - test_covariates = covariates_scaler.transform(test_covariates) - response_scaler = MinMaxScaler() - responses = unravel_2D(response_scaler.fit_transform(ravel_2D(responses)), response_shape) - if args.testrespfile is not None: - test_responses = unravel_2D(response_scaler.transform(ravel_2D(test_responses)), test_responses_shape) - test_responses = np.expand_dims(test_responses, axis=1) - - factor = args.m - - x_context = np.zeros([covariates.shape[0], factor, covariates.shape[1]], dtype=np.float32) - y_context = np.zeros([responses.shape[0], factor, responses.shape[1], - responses.shape[2], responses.shape[3]], dtype=np.float32) - x_all = np.zeros([covariates.shape[0], factor, covariates.shape[1]], dtype=np.float32) - x_context_test = np.zeros([test_covariates.shape[0], factor, test_covariates.shape[1]], dtype=np.float32) - y_context_test = np.zeros([test_covariates.shape[0], factor, responses.shape[1], - responses.shape[2], responses.shape[3]], dtype=np.float32) - - print('Estimating the fixed-effects ...') - for i in range(factor): - x_context[:,i,:] = covariates[:,:] - x_context_test[:,i,:] = test_covariates[:,:] - idx = np.random.randint(0,covariates.shape[0], covariates.shape[0]) - if args.estimator=='ST': - for j in range(responses.shape[1]): - for k in range(responses.shape[2]): - for l in range(responses.shape[3]): - reg = LinearRegression() - reg.fit(x_context[idx,i,:], responses[idx,j,k,l]) - y_context[:,i,j,k,l] = reg.predict(x_context[:,i,:]) - y_context_test[:,i,j,k,l] = reg.predict(x_context_test[:,i,:]) - elif args.estimator=='MT': - reg = MultiTaskLasso(alpha=0.1) - reg.fit(x_context[idx,i,:], np.reshape(responses[idx,:,:,:], [covariates.shape[0],np.prod(responses.shape[1:])])) - y_context[:,i,:,:,:] = np.reshape(reg.predict(x_context[:,i,:]), - [x_context.shape[0],responses.shape[1],responses.shape[2],responses.shape[3]]) - y_context_test[:,i,:,:,:] = np.reshape(reg.predict(x_context_test[:,i,:]), - [x_context_test.shape[0],responses.shape[1],responses.shape[2],responses.shape[3]]) - print('Fixed-effect %d of %d is computed!' %(i+1, factor)) - - x_all = x_context - responses = np.expand_dims(responses, axis=1).repeat(factor, axis=1) - - ################################## TRAINING ################################# - - encoder = Encoder(x_context, y_context, args).to(args.device) - args.cnn_feature_num = encoder.cnn_feature_num - decoder = Decoder(x_context, y_context, args).to(args.device) - model = NP(encoder, decoder, args).to(args.device) - - print('Estimating the Random-effect ...') - k = 1 - epochs = [int(args.epochs/4),int(args.epochs/2),int(args.epochs/5),int(args.epochs-args.epochs/4-args.epochs/2-args.epochs/5)] - mini_batch_num = args.batchnum - batch_size = int(x_context.shape[0]/mini_batch_num) - model.train() - for e in range(len(epochs)): - optimizer = optim.Adam(model.parameters(), lr=10**(-e-2)) - for j in range(epochs[e]): - train_loss = 0 - rand_idx = np.random.permutation(x_context.shape[0]) - for i in range(mini_batch_num): - optimizer.zero_grad() - idx = rand_idx[i*batch_size:(i+1)*batch_size] - y_hat, z_all, z_context, dummy = model(torch.tensor(x_context[idx,:,:], device = args.device), - torch.tensor(y_context[idx,:,:,:,:], device = args.device), - torch.tensor(x_all[idx,:,:], device = args.device), - torch.tensor(responses[idx,:,:,:,:], device = args.device)) - loss = np_loss(y_hat, torch.tensor(responses[idx,:,:,:,:], device = args.device), z_all, z_context) - loss.backward() - train_loss += loss.item() - optimizer.step() - print('Epoch: %d, Loss:%f, Average Loss:%f' %(k, train_loss, train_loss/responses.shape[0])) - k += 1 - - ################################## Evaluation ################################# - - print('Predicting on Test Data ...') - model.eval() - model.apply(apply_dropout_test) - with torch.no_grad(): - y_hat, z_all, z_context, y_sigma = model(torch.tensor(x_context_test, device = args.device), - torch.tensor(y_context_test, device = args.device), n = 15) - if args.testrespfile is not None: - test_loss = np_loss(y_hat[0:test_responses_shape[0],:], - torch.tensor(test_responses, device = args.device), - z_all, z_context).item() - print('Average Test Loss:%f' %(test_loss/test_responses_shape[0])) - - RMSE = np.sqrt(np.mean((test_responses - y_hat[0:test_responses_shape[0],:].cpu().numpy())**2, axis = 0)).squeeze() * mask - SMSE = RMSE ** 2 / np.var(test_responses, axis=0).squeeze() - Rho, pRho = compute_pearsonr(test_responses.squeeze(), y_hat[0:test_responses_shape[0],:].cpu().numpy().squeeze()) - EXPV = explained_var(test_responses.squeeze(), y_hat[0:test_responses_shape[0],:].cpu().numpy().squeeze()) * mask - MSLL = compute_MSLL(test_responses.squeeze(), y_hat[0:test_responses_shape[0],:].cpu().numpy().squeeze(), - y_sigma[0:test_responses_shape[0],:].cpu().numpy().squeeze()**2, train_mean = test_responses.mean(0), - train_var = test_responses.var(0)).squeeze() * mask - - NPMs = (test_responses - y_hat[0:test_responses_shape[0],:].cpu().numpy()) / (y_sigma[0:test_responses_shape[0],:].cpu().numpy()) - NPMs = NPMs.squeeze() - NPMs = NPMs * mask - NPMs = np.nan_to_num(NPMs) - - temp=NPMs.reshape([NPMs.shape[0],NPMs.shape[1]*NPMs.shape[2]*NPMs.shape[3]]) - EVD_params = extreme_value_prob_fit(temp, 0.01) - abnormal_probs = extreme_value_prob(EVD_params, temp, 0.01) - - ############################## SAVING RESULTS ################################# - - print('Saving Results to: %s' %(args.outdir)) - exfile = args.respfile - y_hat = y_hat.squeeze().cpu().numpy() - y_hat = response_scaler.inverse_transform(ravel_2D(y_hat)) - y_hat = y_hat[:,mask.flatten()] - fileio.save(y_hat.T, args.outdir + - '/yhat.nii.gz', example=exfile, mask=mask) - ys2 = y_sigma.squeeze().cpu().numpy() - ys2 = ravel_2D(ys2) * (response_scaler.data_max_ - response_scaler.data_min_) - ys2 = ys2**2 - ys2 = ys2[:,mask.flatten()] - fileio.save(ys2.T, args.outdir + - '/ys2.nii.gz', example=exfile, mask=mask) - if args.testrespfile is not None: - NPMs = ravel_2D(NPMs)[:,mask.flatten()] - fileio.save(NPMs.T, args.outdir + - '/Z.nii.gz', example=exfile, mask=mask) - fileio.save(Rho.flatten()[mask.flatten()], args.outdir + - '/Rho.nii.gz', example=exfile, mask=mask) - fileio.save(pRho.flatten()[mask.flatten()], args.outdir + - '/pRho.nii.gz', example=exfile, mask=mask) - fileio.save(RMSE.flatten()[mask.flatten()], args.outdir + - '/rmse.nii.gz', example=exfile, mask=mask) - fileio.save(SMSE.flatten()[mask.flatten()], args.outdir + - '/smse.nii.gz', example=exfile, mask=mask) - fileio.save(EXPV.flatten()[mask.flatten()], args.outdir + - '/expv.nii.gz', example=exfile, mask=mask) - fileio.save(MSLL.flatten()[mask.flatten()], args.outdir + - '/msll.nii.gz', example=exfile, mask=mask) - - with open(args.outdir +'model.pkl', 'wb') as handle: - pickle.dump({'model':model, 'covariates_scaler':covariates_scaler, - 'response_scaler': response_scaler, 'EVD_params':EVD_params, - 'abnormal_probs':abnormal_probs}, handle, protocol=configs.PICKLE_PROTOCOL) - -############################################################################### - print('DONE!') - - -def main(*args): - """ Parse arguments and estimate model - """ - - np.seterr(invalid='ignore') - args = get_args(args) - estimate(args) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/build/lib/pcntoolkit/normative_model/__init__.py b/build/lib/pcntoolkit/normative_model/__init__.py deleted file mode 100644 index 772a3653..00000000 --- a/build/lib/pcntoolkit/normative_model/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from . import norm_gpr -from . import norm_base -from . import norm_blr -from . import norm_rfa -from . import norm_hbr -from . import norm_utils \ No newline at end of file diff --git a/build/lib/pcntoolkit/normative_model/norm_base.py b/build/lib/pcntoolkit/normative_model/norm_base.py deleted file mode 100644 index 3e46ef93..00000000 --- a/build/lib/pcntoolkit/normative_model/norm_base.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -import sys -from six import with_metaclass -from abc import ABCMeta, abstractmethod -import pickle - -try: # run as a package if installed - from pcntoolkit import configs -except ImportError: - pass - - path = os.path.abspath(os.path.dirname(__file__)) - if path not in sys.path: - sys.path.append(path) - del path - import configs - - -class NormBase(with_metaclass(ABCMeta)): - """ Base class for normative model back-end. - - All normative modelling approaches must define the following methods:: - - NormativeModel.estimate() - NormativeModel.predict() - """ - - def __init__(self, x=None): - pass - - @abstractmethod - def estimate(self, X, y): - """ Estimate the normative model """ - - @abstractmethod - def predict(self, Xs, X, y): - """ Make predictions for new data """ - - @property - @abstractmethod - def n_params(self): - """ Report the number of parameters required by the model """ - - def save(self, save_path): - try: - with open(save_path, 'wb') as handle: - pickle.dump(self, handle, protocol=configs.PICKLE_PROTOCOL) - return True - except Exception as err: - print('Error:', err) - raise - - def load(self, load_path): - try: - with open(load_path, 'rb') as handle: - nm = pickle.load(handle) - return nm - except Exception as err: - print('Error:', err) - raise diff --git a/build/lib/pcntoolkit/normative_model/norm_blr.py b/build/lib/pcntoolkit/normative_model/norm_blr.py deleted file mode 100644 index 814e0b08..00000000 --- a/build/lib/pcntoolkit/normative_model/norm_blr.py +++ /dev/null @@ -1,252 +0,0 @@ -from __future__ import print_function -from __future__ import division - -import os -import sys -import numpy as np -import pandas as pd -from ast import literal_eval - -try: # run as a package if installed - from pcntoolkit.model.bayesreg import BLR - from pcntoolkit.normative_model.norm_base import NormBase - from pcntoolkit.dataio import fileio - from pcntoolkit.util.utils import create_poly_basis, WarpBoxCox, \ - WarpAffine, WarpCompose, WarpSinArcsinh -except ImportError: - pass - - path = os.path.abspath(os.path.dirname(__file__)) - if path not in sys.path: - sys.path.append(path) - del path - - from model.bayesreg import BLR - from norm_base import NormBase - from dataio import fileio - from util.utils import create_poly_basis, WarpBoxCox, \ - WarpAffine, WarpCompose, WarpSinArcsinh - -class NormBLR(NormBase): - """ Normative modelling based on Bayesian Linear Regression - """ - - def __init__(self, **kwargs): - X = kwargs.pop('X', None) - y = kwargs.pop('y', None) - theta = kwargs.pop('theta', None) - if isinstance(theta, str): - theta = np.array(literal_eval(theta)) - self.optim_alg = kwargs.get('optimizer','powell') - - if X is None: - raise(ValueError, "Data matrix must be specified") - - if len(X.shape) == 1: - self.D = 1 - else: - self.D = X.shape[1] - - # Parse model order - if kwargs is None: - model_order = 1 - elif 'configparam' in kwargs: # deprecated syntax - model_order = kwargs.pop('configparam') - elif 'model_order' in kwargs: - model_order = kwargs.pop('model_order') - else: - model_order = 1 - - # Force a default model order and check datatype - if model_order is None: - model_order = 1 - if type(model_order) is not int: - model_order = int(model_order) - - # configure heteroskedastic noise - if 'varcovfile' in kwargs: - var_cov_file = kwargs.get('varcovfile') - if var_cov_file.endswith('.pkl'): - self.var_covariates = pd.read_pickle(var_cov_file) - else: - self.var_covariates = np.loadtxt(var_cov_file) - if len(self.var_covariates.shape) == 1: - self.var_covariates = self.var_covariates[:, np.newaxis] - n_beta = self.var_covariates.shape[1] - self.var_groups = None - elif 'vargroupfile' in kwargs: - # configure variance groups (e.g. site specific variance) - var_groups_file = kwargs.pop('vargroupfile') - if var_groups_file.endswith('.pkl'): - self.var_groups = pd.read_pickle(var_groups_file) - else: - self.var_groups = np.loadtxt(var_groups_file) - var_ids = set(self.var_groups) - var_ids = sorted(list(var_ids)) - n_beta = len(var_ids) - else: - self.var_groups = None - self.var_covariates = None - n_beta = 1 - - # are we using ARD? - if 'use_ard' in kwargs: - self.use_ard = kwargs.pop('use_ard') - else: - self.use_ard = False - if self.use_ard: - n_alpha = self.D * model_order - else: - n_alpha = 1 - - # Configure warped likelihood - if 'warp' in kwargs: - warp_str = kwargs.pop('warp') - if warp_str is None: - self.warp = None - n_gamma = 0 - else: - # set up warp - exec('self.warp =' + warp_str + '()') - n_gamma = self.warp.get_n_params() - else: - self.warp = None - n_gamma = 0 - - self._n_params = n_alpha + n_beta + n_gamma - self._model_order = model_order - - print("configuring BLR ( order", model_order, ")") - if (theta is None) or (len(theta) != self._n_params): - print("Using default hyperparameters") - self.theta0 = np.zeros(self._n_params) - else: - self.theta0 = theta - self.theta = self.theta0 - - # initialise the BLR object if the required parameters are present - if (theta is not None) and (y is not None): - Phi = create_poly_basis(X, self._model_order) - self.blr = BLR(theta=theta, X=Phi, y=y, - warp=self.warp, **kwargs) - else: - self.blr = BLR(**kwargs) - - @property - def n_params(self): - return self._n_params - - @property - def neg_log_lik(self): - return self.blr.nlZ - - def estimate(self, X, y, **kwargs): - theta = kwargs.pop('theta', None) - if isinstance(theta, str): - theta = np.array(literal_eval(theta)) - - # remove warp string to prevent it being passed to the blr object - kwargs.pop('warp',None) - - Phi = create_poly_basis(X, self._model_order) - if len(y.shape) > 1: - y = y.ravel() - - if theta is None: - theta = self.theta0 - - # (re-)initialize BLR object because parameters were not specified - self.blr = BLR(theta=theta, X=Phi, y=y, - var_groups=self.var_groups, - warp=self.warp, **kwargs) - - self.theta = self.blr.estimate(theta, Phi, y, - var_covariates=self.var_covariates, **kwargs) - - return self - - def predict(self, Xs, X=None, y=None, **kwargs): - - theta = self.theta # always use the estimated coefficients - # remove from kwargs to avoid downstream problems - kwargs.pop('theta', None) - - Phis = create_poly_basis(Xs, self._model_order) - - if X is None: - Phi = None - else: - Phi = create_poly_basis(X, self._model_order) - - # process variance groups for the test data - if 'testvargroup' in kwargs: - var_groups_te = kwargs.pop('testvargroup') - else: - if 'testvargroupfile' in kwargs: - var_groups_test_file = kwargs.pop('testvargroupfile') - if var_groups_test_file.endswith('.pkl'): - var_groups_te = pd.read_pickle(var_groups_test_file) - else: - var_groups_te = np.loadtxt(var_groups_test_file) - else: - var_groups_te = None - - # process test variance covariates - if 'testvarcov' in kwargs: - var_cov_te = kwargs.pop('testvarcov') - else: - if 'testvarcovfile' in kwargs: - var_cov_test_file = kwargs.get('testvarcovfile') - if var_cov_test_file.endswith('.pkl'): - var_cov_te = pd.read_pickle(var_cov_test_file) - else: - var_cov_te = np.loadtxt(var_cov_test_file) - else: - var_cov_te = None - - # do we want to adjust the responses? - if 'adaptresp' in kwargs: - y_adapt = kwargs.pop('adaptresp') - else: - if 'adaptrespfile' in kwargs: - y_adapt = fileio.load(kwargs.pop('adaptrespfile')) - if len(y_adapt.shape) == 1: - y_adapt = y_adapt[:, np.newaxis] - else: - y_adapt = None - - if 'adaptcov' in kwargs: - X_adapt = kwargs.pop('adaptcov') - Phi_adapt = create_poly_basis(X_adapt, self._model_order) - else: - if 'adaptcovfile' in kwargs: - X_adapt = fileio.load(kwargs.pop('adaptcovfile')) - Phi_adapt = create_poly_basis(X_adapt, self._model_order) - else: - Phi_adapt = None - - if 'adaptvargroup' in kwargs: - var_groups_ad = kwargs.pop('adaptvargroup') - else: - if 'adaptvargroupfile' in kwargs: - var_groups_adapt_file = kwargs.pop('adaptvargroupfile') - if var_groups_adapt_file.endswith('.pkl'): - var_groups_ad = pd.read_pickle(var_groups_adapt_file) - else: - var_groups_ad = np.loadtxt(var_groups_adapt_file) - else: - var_groups_ad = None - - if y_adapt is None: - yhat, s2 = self.blr.predict(theta, Phi, y, Phis, - var_groups_test=var_groups_te, - var_covariates_test=var_cov_te, - **kwargs) - else: - yhat, s2 = self.blr.predict_and_adjust(theta, Phi_adapt, y_adapt, Phis, - var_groups_test=var_groups_te, - var_groups_adapt=var_groups_ad, - **kwargs) - - return yhat, s2 - \ No newline at end of file diff --git a/build/lib/pcntoolkit/normative_model/norm_gpr.py b/build/lib/pcntoolkit/normative_model/norm_gpr.py deleted file mode 100644 index a74cc95b..00000000 --- a/build/lib/pcntoolkit/normative_model/norm_gpr.py +++ /dev/null @@ -1,72 +0,0 @@ -from __future__ import print_function -from __future__ import division - -import os -import sys -import numpy as np - -try: # run as a package if installed - from pcntoolkit.model.gp import GPR, CovSum - from pcntoolkit.normative_model.norm_base import NormBase -except ImportError: - pass - - path = os.path.abspath(os.path.dirname(__file__)) - if path not in sys.path: - sys.path.append(path) - del path - - from model.gp import GPR, CovSum - from norm_base import NormBase - -class NormGPR(NormBase): - """ Classical GPR-based normative modelling approach - """ - - def __init__(self, **kwargs): #X=None, y=None, theta=None, - X = kwargs.pop('X', None) - y = kwargs.pop('y', None) - theta = kwargs.pop('theta', None) - - self.covfunc = CovSum(X, ('CovLin', 'CovSqExpARD')) - self.theta0 = np.zeros(self.covfunc.get_n_params() + 1) - self.theta = self.theta0 - - if (theta is not None) and (X is not None) and (y is not None): - self.gpr = GPR(theta, self.covfunc, X, y) - self._n_params = self.covfunc.get_n_params() + 1 - else: - self.gpr = GPR() - - @property - def n_params(self): - if not hasattr(self,'_n_params'): - self._n_params = self.covfunc.get_n_params() + 1 - - return self._n_params - - @property - def neg_log_lik(self): - return self.gpr.nlZ - - def estimate(self, X, y, **kwargs): - theta = kwargs.pop('theta', None) - if theta is None: - theta = self.theta0 - self.gpr = GPR(theta, self.covfunc, X, y) - self.theta = self.gpr.estimate(theta, self.covfunc, X, y) - - return self - - def predict(self, Xs, X, y, **kwargs): - theta = kwargs.pop('theta', None) - if theta is None: - theta = self.theta - yhat, s2 = self.gpr.predict(theta, X, y, Xs) - - # only return the marginal variances - if len(s2.shape) == 2: - s2 = np.diag(s2) - - return yhat, s2 - \ No newline at end of file diff --git a/build/lib/pcntoolkit/normative_model/norm_hbr.py b/build/lib/pcntoolkit/normative_model/norm_hbr.py deleted file mode 100644 index 16ec21ce..00000000 --- a/build/lib/pcntoolkit/normative_model/norm_hbr.py +++ /dev/null @@ -1,235 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Thu Jul 25 17:01:24 2019 - -@author: seykia -@author: augub -""" - -from __future__ import print_function -from __future__ import division - - -import os -import warnings -import sys -import numpy as np -from ast import literal_eval as make_tuple - -try: - from pcntoolkit.dataio import fileio - from pcntoolkit.normative_model.norm_base import NormBase - from pcntoolkit.model.hbr import HBR -except ImportError: - pass - - path = os.path.abspath(os.path.dirname(__file__)) - if path not in sys.path: - sys.path.append(path) - del path - import dataio.fileio as fileio - from model.hbr import HBR - from norm_base import NormBase - - -class NormHBR(NormBase): - """ Classical GPR-based normative modelling approach - """ - - def __init__(self, **kwargs): - - self.configs = dict() - self.configs['transferred'] = False - self.configs['trbefile'] = kwargs.pop('trbefile', None) - self.configs['tsbefile'] = kwargs.pop('tsbefile', None) - self.configs['type'] = kwargs.pop('model_type', 'linear') - self.configs['skewed_likelihood'] = kwargs.pop('skewed_likelihood', 'False') == 'True' - self.configs['pred_type'] = kwargs.pop('pred_type', 'single') - self.configs['random_noise'] = kwargs.pop('random_noise', 'True') == 'True' - self.configs['n_samples'] = int(kwargs.pop('n_samples', '1000')) - self.configs['n_tuning'] = int(kwargs.pop('n_tuning', '500')) - self.configs['n_chains'] = int(kwargs.pop('n_chains', '1')) - self.configs['likelihood'] = kwargs.pop('likelihood', 'Normal') - self.configs['sampler'] = kwargs.pop('sampler', 'NUTS') - self.configs['target_accept'] = float(kwargs.pop('target_accept', '0.8')) - self.configs['init'] = kwargs.pop('init', 'jitter+adapt_diag') - self.configs['cores'] = int(kwargs.pop('cores', '1')) - self.configs['freedom'] = int(kwargs.pop('freedom', '1')) - - if self.configs['type'] == 'bspline': - self.configs['order'] = int(kwargs.pop('order', '3')) - self.configs['nknots'] = int(kwargs.pop('nknots', '5')) - elif self.configs['type'] == 'polynomial': - self.configs['order'] = int(kwargs.pop('order', '3')) - elif self.configs['type'] == 'nn': - self.configs['nn_hidden_neuron_num'] = int(kwargs.pop('nn_hidden_neuron_num', '2')) - self.configs['nn_hidden_layers_num'] = int(kwargs.pop('nn_hidden_layers_num', '2')) - if self.configs['nn_hidden_layers_num'] > 2: - raise ValueError("Using " + str(self.configs['nn_hidden_layers_num']) \ - + " layers was not implemented. The number of " \ - + " layers has to be less than 3.") - elif self.configs['type'] == 'linear': - pass - else: - raise ValueError("Unknown model type, please specify from 'linear', \ - 'polynomial', 'bspline', or 'nn'.") - - if self.configs['type'] in ['bspline', 'polynomial', 'linear']: - - for p in ['mu', 'sigma', 'epsilon', 'delta']: - self.configs[f'linear_{p}'] = kwargs.pop(f'linear_{p}', 'False') == 'True' - - ######## Deprecations (remove in later version) - if f'{p}_linear' in kwargs.keys(): - print(f'The keyword \'{p}_linear\' is deprecated. It is now automatically replaced with \'linear_{p}\'') - self.configs[f'linear_{p}'] = kwargs.pop(f'{p}_linear', 'False') == 'True' - ##### End Deprecations - - for c in ['centered','random']: - self.configs[f'{c}_{p}'] = kwargs.pop(f'{c}_{p}', 'False') == 'True' - for sp in ['slope','intercept']: - self.configs[f'{c}_{sp}_{p}'] = kwargs.pop(f'{c}_{sp}_{p}', 'False') == 'True' - - ######## Deprecations (remove in later version) - if self.configs['linear_sigma']: - if 'random_noise' in kwargs.keys(): - print("The keyword \'random_noise\' is deprecated. It is now automatically replaced with \'random_intercept_sigma\', because sigma is linear") - self.configs['random_intercept_sigma'] = kwargs.pop('random_noise','False') == 'True' - elif 'random_noise' in kwargs.keys(): - print("The keyword \'random_noise\' is deprecated. It is now automatically replaced with \'random_sigma\', because sigma is fixed") - self.configs['random_sigma'] = kwargs.pop('random_noise','False') == 'True' - if 'random_slope' in kwargs.keys(): - print("The keyword \'random_slope\' is deprecated. It is now automatically replaced with \'random_intercept_mu\'") - self.configs['random_intercept_mu'] = kwargs.pop('random_slope','False') == 'True' - ##### End Deprecations - - self.configs['linear_mu'] = kwargs.pop('linear_mu','True') == 'True' - self.configs['random_intercept_mu'] = kwargs.pop('random_intercept_mu','True') == 'True' - self.configs['random_slope_mu'] = kwargs.pop('random_slope_mu','True') == 'True' - self.configs['random_sigma'] = kwargs.pop('random_sigma','True') == 'True' - - self.hbr = HBR(self.configs) - - @property - def n_params(self): - return 1 - - @property - def neg_log_lik(self): - return -1 - - def estimate(self, X, y, **kwargs): - - trbefile = kwargs.pop('trbefile', None) - if trbefile is not None: - batch_effects_train = fileio.load(trbefile) - else: - print('Could not find batch-effects file! Initilizing all as zeros ...') - batch_effects_train = np.zeros([X.shape[0], 1]) - - self.hbr.estimate(X, y, batch_effects_train) - - return self - - def predict(self, Xs, X=None, Y=None, **kwargs): - - tsbefile = kwargs.pop('tsbefile', None) - if tsbefile is not None: - batch_effects_test = fileio.load(tsbefile) - else: - print('Could not find batch-effects file! Initilizing all as zeros ...') - batch_effects_test = np.zeros([Xs.shape[0], 1]) - - pred_type = self.configs['pred_type'] - - if self.configs['transferred'] == False: - yhat, s2 = self.hbr.predict(Xs, batch_effects_test, pred=pred_type) - else: - raise ValueError("This is a transferred model. Please use predict_on_new_sites function.") - - return yhat.squeeze(), s2.squeeze() - - def estimate_on_new_sites(self, X, y, batch_effects): - self.hbr.estimate_on_new_site(X, y, batch_effects) - self.configs['transferred'] = True - return self - - def predict_on_new_sites(self, X, batch_effects): - - yhat, s2 = self.hbr.predict_on_new_site(X, batch_effects) - return yhat, s2 - - def extend(self, X, y, batch_effects, X_dummy_ranges=[[0.1, 0.9, 0.01]], - merge_batch_dim=0, samples=10, informative_prior=False): - - X_dummy, batch_effects_dummy = self.hbr.create_dummy_inputs(X_dummy_ranges) - - X_dummy, batch_effects_dummy, Y_dummy = self.hbr.generate(X_dummy, - batch_effects_dummy, samples) - - batch_effects[:, merge_batch_dim] = batch_effects[:, merge_batch_dim] + \ - np.max(batch_effects_dummy[:, merge_batch_dim]) + 1 - - if informative_prior: - self.hbr.adapt(np.concatenate((X_dummy, X)), - np.concatenate((Y_dummy, y)), - np.concatenate((batch_effects_dummy, batch_effects))) - else: - self.hbr.estimate(np.concatenate((X_dummy, X)), - np.concatenate((Y_dummy, y)), - np.concatenate((batch_effects_dummy, batch_effects))) - - return self - - def tune(self, X, y, batch_effects, X_dummy_ranges=[[0.1, 0.9, 0.01]], - merge_batch_dim=0, samples=10, informative_prior=False): - - tune_ids = list(np.unique(batch_effects[:, merge_batch_dim])) - - X_dummy, batch_effects_dummy = self.hbr.create_dummy_inputs(X_dummy_ranges) - - for idx in tune_ids: - X_dummy = X_dummy[batch_effects_dummy[:, merge_batch_dim] != idx, :] - batch_effects_dummy = batch_effects_dummy[batch_effects_dummy[:, merge_batch_dim] != idx, :] - - X_dummy, batch_effects_dummy, Y_dummy = self.hbr.generate(X_dummy, - batch_effects_dummy, samples) - - if informative_prior: - self.hbr.adapt(np.concatenate((X_dummy, X)), - np.concatenate((Y_dummy, y)), - np.concatenate((batch_effects_dummy, batch_effects))) - else: - self.hbr.estimate(np.concatenate((X_dummy, X)), - np.concatenate((Y_dummy, y)), - np.concatenate((batch_effects_dummy, batch_effects))) - - return self - - def merge(self, nm, X_dummy_ranges=[[0.1, 0.9, 0.01]], merge_batch_dim=0, - samples=10): - - X_dummy1, batch_effects_dummy1 = self.hbr.create_dummy_inputs(X_dummy_ranges) - X_dummy2, batch_effects_dummy2 = nm.hbr.create_dummy_inputs(X_dummy_ranges) - - X_dummy1, batch_effects_dummy1, Y_dummy1 = self.hbr.generate(X_dummy1, - batch_effects_dummy1, samples) - X_dummy2, batch_effects_dummy2, Y_dummy2 = nm.hbr.generate(X_dummy2, - batch_effects_dummy2, samples) - - batch_effects_dummy2[:, merge_batch_dim] = batch_effects_dummy2[:, merge_batch_dim] + \ - np.max(batch_effects_dummy1[:, merge_batch_dim]) + 1 - - self.hbr.estimate(np.concatenate((X_dummy1, X_dummy2)), - np.concatenate((Y_dummy1, Y_dummy2)), - np.concatenate((batch_effects_dummy1, - batch_effects_dummy2))) - - return self - - def generate(self, X, batch_effects, samples=10): - - X, batch_effects, generated_samples = self.hbr.generate(X, batch_effects, - samples) - return X, batch_effects, generated_samples diff --git a/build/lib/pcntoolkit/normative_model/norm_np.py b/build/lib/pcntoolkit/normative_model/norm_np.py deleted file mode 100644 index 7b632522..00000000 --- a/build/lib/pcntoolkit/normative_model/norm_np.py +++ /dev/null @@ -1,229 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Fri Nov 22 14:41:07 2019 - -@author: seykia -""" - -from __future__ import print_function -from __future__ import division - -import os -import sys -import numpy as np -import torch -from torch import nn, optim -from torch.nn import functional as F -from sklearn.linear_model import LinearRegression -from sklearn.preprocessing import MinMaxScaler -import pickle - -try: # run as a package if installed - from pcntoolkit.normative_model.normbase import NormBase - from pcntoolkit.model.NPR import NPR, np_loss -except ImportError: - pass - - path = os.path.abspath(os.path.dirname(__file__)) - if path not in sys.path: - sys.path.append(path) - del path - - from model.NPR import NPR, np_loss - from norm_base import NormBase - -class struct(object): - pass - -class Encoder(nn.Module): - def __init__(self, x, y, args): - super(Encoder, self).__init__() - self.r_dim = args.r_dim - self.z_dim = args.z_dim - self.hidden_neuron_num = args.hidden_neuron_num - self.h_1 = nn.Linear(x.shape[1] + y.shape[1], self.hidden_neuron_num) - self.h_2 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) - self.h_3 = nn.Linear(self.hidden_neuron_num, self.r_dim) - - def forward(self, x, y): - x_y = torch.cat([x, y], dim=2) - x_y = F.relu(self.h_1(x_y)) - x_y = F.relu(self.h_2(x_y)) - x_y = F.relu(self.h_3(x_y)) - r = torch.mean(x_y, dim=1) - return r - - -class Decoder(nn.Module): - def __init__(self, x, y, args): - super(Decoder, self).__init__() - self.r_dim = args.r_dim - self.z_dim = args.z_dim - self.hidden_neuron_num = args.hidden_neuron_num - - self.g_1 = nn.Linear(self.z_dim, self.hidden_neuron_num) - self.g_2 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) - self.g_3 = nn.Linear(self.hidden_neuron_num, y.shape[1]) - - self.g_1_84 = nn.Linear(self.z_dim, self.hidden_neuron_num) - self.g_2_84 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) - self.g_3_84 = nn.Linear(self.hidden_neuron_num, y.shape[1]) - - def forward(self, z_sample): - z_hat = F.relu(self.g_1(z_sample)) - z_hat = F.relu(self.g_2(z_hat)) - y_hat = torch.sigmoid(self.g_3(z_hat)) - - z_hat_84 = F.relu(self.g_1(z_sample)) - z_hat_84 = F.relu(self.g_2_84(z_hat_84)) - y_hat_84 = torch.sigmoid(self.g_3_84(z_hat_84)) - - return y_hat, y_hat_84 - - - - -class NormNP(NormBase): - """ Classical GPR-based normative modelling approach - """ - - def __init__(self, X, y, configparam=None): - self.configparam = configparam - if configparam is not None: - with open(configparam, 'rb') as handle: - config = pickle.load(handle) - args = struct() - if 'batch_size' in config: - args.batch_size = config['batch_size'] - else: - args.batch_size = 10 - if 'epochs' in config: - args.epochs = config['epochs'] - else: - args.epochs = 100 - if 'device' in config: - args.device = config['device'] - else: - args.device = torch.device('cpu') - if 'm' in config: - args.m = config['m'] - else: - args.m = 200 - if 'hidden_neuron_num' in config: - args.hidden_neuron_num = config['hidden_neuron_num'] - else: - args.hidden_neuron_num = 10 - if 'r_dim' in config: - args.r_dim = config['r_dim'] - else: - args.r_dim = 5 - if 'z_dim' in config: - args.z_dim = config['z_dim'] - else: - args.z_dim = 3 - if 'nv' in config: - args.nv = config['nv'] - else: - args.nv = 0.01 - else: - args = struct() - args.batch_size = 10 - args.epochs = 100 - args.device = torch.device('cpu') - args.m = 200 - args.hidden_neuron_num = 10 - args.r_dim = 5 - args.z_dim = 3 - args.nv = 0.01 - - if y is not None: - if y.ndim == 1: - y = y.reshape(-1,1) - self.args = args - self.encoder = Encoder(X, y, args) - self.decoder = Decoder(X, y, args) - self.model = NPR(self.encoder, self.decoder, args) - - - @property - def n_params(self): - return 1 - - @property - def neg_log_lik(self): - return -1 - - def estimate(self, X, y): - if y.ndim == 1: - y = y.reshape(-1,1) - sample_num = X.shape[0] - batch_size = self.args.batch_size - factor_num = self.args.m - mini_batch_num = int(np.floor(sample_num/batch_size)) - device = self.args.device - - self.scaler = MinMaxScaler() - y = self.scaler.fit_transform(y) - - self.reg = [] - for i in range(factor_num): - self.reg.append(LinearRegression()) - idx = np.random.randint(0, sample_num, sample_num)#int(sample_num/10)) - self.reg[i].fit(X[idx,:],y[idx,:]) - - x_context = np.zeros([sample_num, factor_num, X.shape[1]]) - y_context = np.zeros([sample_num, factor_num, 1]) - - s = X.std(axis=0) - for j in range(factor_num): - x_context[:,j,:] = X + np.sqrt(self.args.nv) * s * np.random.randn(X.shape[0], X.shape[1]) - y_context[:,j,:] = self.reg[j].predict(x_context[:,j,:]) - - x_context = torch.tensor(x_context, device=device, dtype = torch.float) - y_context = torch.tensor(y_context, device=device, dtype = torch.float) - - x_all = torch.tensor(np.expand_dims(X,axis=1), device=device, dtype = torch.float) - y_all = torch.tensor(y.reshape(-1, 1, y.shape[1]), device=device, dtype = torch.float) - - self.model.train() - epochs = [int(self.args.epochs/4),int(self.args.epochs/2),int(self.args.epochs/5), - int(self.args.epochs-self.args.epochs/4-self.args.epochs/2-self.args.epochs/5)] - k = 1 - for e in range(len(epochs)): - optimizer = optim.Adam(self.model.parameters(), lr=10**(-e-2)) - for j in range(epochs[e]): - train_loss = 0 - for i in range(mini_batch_num): - optimizer.zero_grad() - idx = np.arange(i*batch_size,(i+1)*batch_size) - y_hat, y_hat_84, z_all, z_context, dummy, dummy = self.model(x_context[idx,:,:], y_context[idx,:,:], x_all[idx,:,:], y_all[idx,:,:]) - loss = np_loss(y_hat, y_hat_84, y_all[idx,0,:], z_all, z_context) - loss.backward() - train_loss += loss.item() - optimizer.step() - print('Epoch: %d, Loss:%f' %( k, train_loss)) - k += 1 - return self - - def predict(self, Xs, X=None, Y=None, theta=None): - sample_num = Xs.shape[0] - factor_num = self.args.m - x_context_test = np.zeros([sample_num, factor_num, Xs.shape[1]]) - y_context_test = np.zeros([sample_num, factor_num, 1]) - for j in range(factor_num): - x_context_test[:,j,:] = Xs - y_context_test[:,j,:] = self.reg[j].predict(x_context_test[:,j,:]) - x_context_test = torch.tensor(x_context_test, device=self.args.device, dtype = torch.float) - y_context_test = torch.tensor(y_context_test, device=self.args.device, dtype = torch.float) - self.model.eval() - with torch.no_grad(): - y_hat, y_hat_84, z_all, z_context, y_sigma, y_sigma_84 = self.model(x_context_test, y_context_test, n = 100) - - y_hat = self.scaler.inverse_transform(y_hat.cpu().numpy()) - y_hat_84 = self.scaler.inverse_transform(y_hat_84.cpu().numpy()) - y_sigma = y_sigma.cpu().numpy() * (self.scaler.data_max_ - self.scaler.data_min_) - y_sigma_84 = y_sigma_84.cpu().numpy() * (self.scaler.data_max_ - self.scaler.data_min_) - sigma_al = y_hat - y_hat_84 - return y_hat.squeeze(), (y_sigma**2 + sigma_al**2).squeeze() #, z_context[0].cpu().numpy(), z_context[1].cpu().numpy() - \ No newline at end of file diff --git a/build/lib/pcntoolkit/normative_model/norm_rfa.py b/build/lib/pcntoolkit/normative_model/norm_rfa.py deleted file mode 100644 index f60e731a..00000000 --- a/build/lib/pcntoolkit/normative_model/norm_rfa.py +++ /dev/null @@ -1,72 +0,0 @@ -from __future__ import print_function -from __future__ import division - -import os -import sys -import numpy as np - -try: # run as a package if installed - from pcntoolkit.normative_model.norm_base import NormBase - from pcntoolkit.model.rfa import GPRRFA -except ImportError: - pass - - path = os.path.abspath(os.path.dirname(__file__)) - if path not in sys.path: - sys.path.append(path) - del path - - from model.rfa import GPRRFA - from norm_base import NormBase - -class NormRFA(NormBase): - """ Classical GPR-based normative modelling approach - """ - - def __init__(self, X, y=None, theta=None, n_feat=None): - - if (X is not None): - if n_feat is None: - print("initialising RFA") - else: - print("initialising RFA with", n_feat, "random features") - self.gprrfa = GPRRFA(theta, X, n_feat=n_feat) - self._n_params = self.gprrfa.get_n_params(X) - else: - raise(ValueError, 'please specify covariates') - return - - if theta is None: - self.theta0 = np.zeros(self._n_params) - else: - if len(theta) == self._n_params: - self.theta0 = theta - else: - raise(ValueError, 'hyperparameter vector has incorrect size') - - self.theta = self.theta0 - - @property - def n_params(self): - - return self._n_params - - @property - def neg_log_lik(self): - return self.gprrfa.nlZ - - def estimate(self, X, y, theta=None): - if theta is None: - theta = self.theta0 - self.gprrfa = GPRRFA(theta, X, y) - self.theta = self.gprrfa.estimate(theta, X, y) - - return self - - def predict(self, Xs, X, y, theta=None): - if theta is None: - theta = self.theta - yhat, s2 = self.gprrfa.predict(theta, X, y, Xs) - - return yhat, s2 - \ No newline at end of file diff --git a/build/lib/pcntoolkit/normative_model/norm_utils.py b/build/lib/pcntoolkit/normative_model/norm_utils.py deleted file mode 100644 index 48e79980..00000000 --- a/build/lib/pcntoolkit/normative_model/norm_utils.py +++ /dev/null @@ -1,28 +0,0 @@ -try: # run as a package if installed - from pcntoolkit.normative_model.norm_blr import NormBLR - from pcntoolkit.normative_model.norm_gpr import NormGPR - from pcntoolkit.normative_model.norm_rfa import NormRFA - from pcntoolkit.normative_model.norm_hbr import NormHBR - from pcntoolkit.normative_model.norm_np import NormNP -except: - from norm_blr import NormBLR - from norm_gpr import NormGPR - from norm_rfa import NormRFA - from norm_hbr import NormHBR - from norm_np import NormNP - -def norm_init(X, y=None, theta=None, alg='gpr', **kwargs): - if alg == 'gpr': - nm = NormGPR(X=X, y=y, theta=theta, **kwargs) - elif alg =='blr': - nm = NormBLR(X=X, y=y, theta=theta, **kwargs) - elif alg == 'rfa': - nm = NormRFA(X=X, y=y, theta=theta, **kwargs) - elif alg == 'hbr': - nm = NormHBR(**kwargs) - elif alg == 'np': - nm = NormNP(X=X, y=y, **kwargs) - else: - raise(ValueError, "Algorithm " + alg + " not known.") - - return nm \ No newline at end of file diff --git a/build/lib/pcntoolkit/normative_parallel.py b/build/lib/pcntoolkit/normative_parallel.py deleted file mode 100644 index 8c549b67..00000000 --- a/build/lib/pcntoolkit/normative_parallel.py +++ /dev/null @@ -1,1275 +0,0 @@ -#!/.../anaconda/bin/python/ - -# ----------------------------------------------------------------------------- -# Run parallel normative modelling. -# All processing takes place in the processing directory (processing_dir) -# All inputs should be text files or binaries and space seperated -# -# It is possible to run these functions using... -# -# * k-fold cross-validation -# * estimating a training dataset then applying to a second test dataset -# -# First,the data is split for parallel processing. -# Second, the splits are submitted to the cluster. -# Third, the output is collected and combined. -# -# witten by (primarily) T Wolfers, (adaptated) SM Kia, H Huijsdens, L Parks, -# S Rutherford, AF Marquand -# ----------------------------------------------------------------------------- - -from __future__ import print_function -from __future__ import division - -import os -import sys -import glob -import shutil -import pickle -import fileinput -import time -import numpy as np -import pandas as pd -from subprocess import call, check_output - - -try: - import pcntoolkit as ptk - import pcntoolkit.dataio.fileio as fileio - from pcntoolkit import configs - from pcntoolkit.util.utils import yes_or_no - ptkpath = ptk.__path__[0] -except ImportError: - pass - ptkpath = os.path.abspath(os.path.dirname(__file__)) - if ptkpath not in sys.path: - sys.path.append(ptkpath) - import dataio.fileio as fileio - import configs - from util.utils import yes_or_no - - -PICKLE_PROTOCOL = configs.PICKLE_PROTOCOL - - -def execute_nm(processing_dir, - python_path, - job_name, - covfile_path, - respfile_path, - batch_size, - memory, - duration, - normative_path=None, - func='estimate', - interactive=False, - **kwargs): - - ''' Execute parallel normative models - This function is a mother function that executes all parallel normative - modelling routines. Different specifications are possible using the sub- - functions. - - Basic usage:: - - execute_nm(processing_dir, python_path, job_name, covfile_path, respfile_path, batch_size, memory, duration) - - :param processing_dir: Full path to the processing dir - :param python_path: Full path to the python distribution - :param normative_path: Full path to the normative.py. If None (default) then it will automatically retrieves the path from the installed packeage. - :param job_name: Name for the bash script that is the output of this function - :param covfile_path: Full path to a .txt file that contains all covariats (subjects x covariates) for the responsefile - :param respfile_path: Full path to a .txt that contains all features (subjects x features) - :param batch_size: Number of features in each batch - :param memory: Memory requirements written as string for example 4gb or 500mb - :param duation: The approximate duration of the job, a string with HH:MM:SS for example 01:01:01 - :param cv_folds: Number of cross validations - :param testcovfile_path: Full path to a .txt file that contains all covariates (subjects x covariates) for the test response file - :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 - - written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. - ''' - - if normative_path is None: - normative_path = ptkpath + '/normative.py' - - cv_folds = kwargs.get('cv_folds', None) - testcovfile_path = kwargs.get('testcovfile_path', None) - testrespfile_path= kwargs.get('testrespfile_path', None) - outputsuffix = kwargs.get('outputsuffix', 'estimate') - cluster_spec = kwargs.pop('cluster_spec', 'torque') - log_path = kwargs.get('log_path', None) - binary = kwargs.pop('binary', False) - - split_nm(processing_dir, - respfile_path, - batch_size, - binary, - **kwargs) - - batch_dir = glob.glob(processing_dir + 'batch_*') - # print(batch_dir) - number_of_batches = len(batch_dir) - # print(number_of_batches) - - if binary: - file_extentions = '.pkl' - else: - file_extentions = '.txt' - - kwargs.update({'batch_size':str(batch_size)}) - job_ids = [] - for n in range(1, number_of_batches+1): - kwargs.update({'job_id':str(n)}) - if testrespfile_path is not None: - if cv_folds is not None: - raise(ValueError, """If the response file is specified - cv_folds must be equal to None""") - else: - # specified train/test split - batch_processing_dir = processing_dir + 'batch_' + str(n) + '/' - batch_job_name = job_name + '_' + str(n) + '.sh' - batch_respfile_path = (batch_processing_dir + 'resp_batch_' + - str(n) + file_extentions) - batch_testrespfile_path = (batch_processing_dir + - 'testresp_batch_' + - str(n) + file_extentions) - batch_job_path = batch_processing_dir + batch_job_name - if cluster_spec == 'torque': - - # update the response file - kwargs.update({'testrespfile_path': \ - batch_testrespfile_path}) - bashwrap_nm(batch_processing_dir, - python_path, - normative_path, - batch_job_name, - covfile_path, - batch_respfile_path, - func=func, - **kwargs) - job_id = qsub_nm(job_path=batch_job_path, - log_path=log_path, - memory=memory, - duration=duration) - job_ids.append(job_id) - elif cluster_spec == 'sbatch': - # update the response file - kwargs.update({'testrespfile_path': \ - batch_testrespfile_path}) - sbatchwrap_nm(batch_processing_dir, - python_path, - normative_path, - batch_job_name, - covfile_path, - batch_respfile_path, - func=func, - memory=memory, - duration=duration, - **kwargs) - sbatch_nm(job_path=batch_job_path, - log_path=log_path) - elif cluster_spec == 'new': - # this part requires addition in different envioronment [ - sbatchwrap_nm(processing_dir=batch_processing_dir, - func=func, **kwargs) - sbatch_nm(processing_dir=batch_processing_dir) - # ] - if testrespfile_path is None: - if testcovfile_path is not None: - # forward model - batch_processing_dir = processing_dir + 'batch_' + str(n) + '/' - batch_job_name = job_name + '_' + str(n) + '.sh' - batch_respfile_path = (batch_processing_dir + 'resp_batch_' + - str(n) + file_extentions) - batch_job_path = batch_processing_dir + batch_job_name - if cluster_spec == 'torque': - bashwrap_nm(batch_processing_dir, - python_path, - normative_path, - batch_job_name, - covfile_path, - batch_respfile_path, - func=func, - **kwargs) - job_id = qsub_nm(job_path=batch_job_path, - log_path=log_path, - memory=memory, - duration=duration) - job_ids.append(job_id) - elif cluster_spec == 'sbatch': - sbatchwrap_nm(batch_processing_dir, - python_path, - normative_path, - batch_job_name, - covfile_path, - batch_respfile_path, - func=func, - memory=memory, - duration=duration, - **kwargs) - sbatch_nm(job_path=batch_job_path, - log_path=log_path) - elif cluster_spec == 'new': - # this part requires addition in different envioronment [ - bashwrap_nm(processing_dir=batch_processing_dir, func=func, - **kwargs) - qsub_nm(processing_dir=batch_processing_dir) - # ] - else: - # cross-validation - batch_processing_dir = (processing_dir + 'batch_' + - str(n) + '/') - batch_job_name = job_name + '_' + str(n) + '.sh' - batch_respfile_path = (batch_processing_dir + - 'resp_batch_' + str(n) + - file_extentions) - batch_job_path = batch_processing_dir + batch_job_name - if cluster_spec == 'torque': - bashwrap_nm(batch_processing_dir, - python_path, - normative_path, - batch_job_name, - covfile_path, - batch_respfile_path, - func=func, - **kwargs) - job_id = qsub_nm(job_path=batch_job_path, - log_path=log_path, - memory=memory, - duration=duration) - job_ids.append(job_id) - elif cluster_spec == 'sbatch': - sbatchwrap_nm(batch_processing_dir, - python_path, - normative_path, - batch_job_name, - covfile_path, - batch_respfile_path, - func=func, - memory=memory, - duration=duration, - **kwargs) - sbatch_nm(job_path=batch_job_path, - log_path=log_path) - elif cluster_spec == 'new': - # this part requires addition in different envioronment [ - bashwrap_nm(processing_dir=batch_processing_dir, func=func, - **kwargs) - qsub_nm(processing_dir=batch_processing_dir) - # ] - - if interactive: - - check_jobs(job_ids, delay=60) - - success = False - while (not success): - success = collect_nm(processing_dir, - job_name, - func=func, - collect=False, - binary=binary, - batch_size=batch_size, - outputsuffix=outputsuffix) - if success: - break - else: - if interactive == 'query': - response = yes_or_no('Rerun the failed jobs?') - if response: - rerun_nm(processing_dir, log_path=log_path, memory=memory, - duration=duration, binary=binary, - 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 interactive == 'query': - response = yes_or_no('Collect the results?') - if response: - success = collect_nm(processing_dir, - job_name, - func=func, - collect=True, - binary=binary, - batch_size=batch_size, - outputsuffix=outputsuffix) - else: - print('Collecting the results ...') - success = collect_nm(processing_dir, - job_name, - func=func, - collect=True, - binary=binary, - batch_size=batch_size, - outputsuffix=outputsuffix) - - -"""routines that are environment independent""" - -def split_nm(processing_dir, - respfile_path, - batch_size, - binary, - **kwargs): - - ''' This function prepares the input files for normative_parallel. - - Basic usage:: - - split_nm(processing_dir, respfile_path, batch_size, binary, testrespfile_path) - - :param processing_dir: Full path to the processing dir - :param respfile_path: Full path to the responsefile.txt (subjects x features) - :param batch_size: Number of features in each batch - :param testrespfile_path: Full path to the test responsefile.txt (subjects x features) - :param binary: If True binary file - - :outputs: The creation of a folder struture for batch-wise processing. - - witten by (primarily) T Wolfers (adapted) SM Kia, (adapted) S Rutherford. - ''' - - testrespfile_path = kwargs.pop('testrespfile_path', None) - - dummy, respfile_extension = os.path.splitext(respfile_path) - if (binary and respfile_extension != '.pkl'): - raise(ValueError, """If binary is True the file format for the - testrespfile file must be .pkl""") - elif (binary==False and respfile_extension != '.txt'): - raise(ValueError, """If binary is False the file format for the - testrespfile file must be .txt""") - - # splits response into batches - if testrespfile_path is None: - if (binary==False): - respfile = fileio.load_ascii(respfile_path) - else: - respfile = pd.read_pickle(respfile_path) - - respfile = pd.DataFrame(respfile) - - numsub = respfile.shape[1] - batch_vec = np.arange(0, - numsub, - batch_size) - batch_vec = np.append(batch_vec, - numsub) - - for n in range(0, (len(batch_vec) - 1)): - resp_batch = respfile.iloc[:, (batch_vec[n]): batch_vec[n + 1]] - os.chdir(processing_dir) - resp = str('resp_batch_' + str(n+1)) - batch = str('batch_' + str(n+1)) - if not os.path.exists(processing_dir + batch): - os.makedirs(processing_dir + batch) - os.makedirs(processing_dir + batch + '/Models/') - if (binary==False): - fileio.save_pd(resp_batch, - processing_dir + batch + '/' + - resp + '.txt') - else: - resp_batch.to_pickle(processing_dir + batch + '/' + - resp + '.pkl', protocol=PICKLE_PROTOCOL) - - # splits response and test responsefile into batches - else: - dummy, testrespfile_extension = os.path.splitext(testrespfile_path) - if (binary and testrespfile_extension != '.pkl'): - raise(ValueError, """If binary is True the file format for the - testrespfile file must be .pkl""") - elif(binary==False and testrespfile_extension != '.txt'): - raise(ValueError, """If binary is False the file format for the - testrespfile file must be .txt""") - - if (binary==False): - respfile = fileio.load_ascii(respfile_path) - testrespfile = fileio.load_ascii(testrespfile_path) - else: - respfile = pd.read_pickle(respfile_path) - testrespfile = pd.read_pickle(testrespfile_path) - - respfile = pd.DataFrame(respfile) - testrespfile = pd.DataFrame(testrespfile) - - numsub = respfile.shape[1] - batch_vec = np.arange(0, numsub, - batch_size) - batch_vec = np.append(batch_vec, - numsub) - for n in range(0, (len(batch_vec) - 1)): - resp_batch = respfile.iloc[:, (batch_vec[n]): batch_vec[n + 1]] - testresp_batch = testrespfile.iloc[:, (batch_vec[n]): batch_vec[n + - 1]] - os.chdir(processing_dir) - resp = str('resp_batch_' + str(n+1)) - testresp = str('testresp_batch_' + str(n+1)) - batch = str('batch_' + str(n+1)) - if not os.path.exists(processing_dir + batch): - os.makedirs(processing_dir + batch) - os.makedirs(processing_dir + batch + '/Models/') - if (binary==False): - fileio.save_pd(resp_batch, - processing_dir + batch + '/' + - resp + '.txt') - fileio.save_pd(testresp_batch, - processing_dir + batch + '/' + testresp + - '.txt') - else: - resp_batch.to_pickle(processing_dir + batch + '/' + - resp + '.pkl', protocol=PICKLE_PROTOCOL) - testresp_batch.to_pickle(processing_dir + batch + '/' + - testresp + '.pkl', - protocol=PICKLE_PROTOCOL) - - -def collect_nm(processing_dir, - job_name, - func='estimate', - collect=False, - binary=False, - batch_size=None, - outputsuffix='_estimate'): - - '''Function to checks and collects all batches. - - Basic usage:: - - collect_nm(processing_dir, job_name) - - - :param processing_dir: Full path to the processing directory - :param collect: If True data is checked for failed batches and collected; if False data is just checked - :param binary: Results in pkl format - - :outputs: Text files containing all results accross all batches the combined output (written to disk). - - :returns 0: if batches fail - :returns 1: if bathches complete successfully - - written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. - ''' - - if binary: - file_extentions = '.pkl' - else: - file_extentions = '.txt' - - # detect number of subjects, batches, hyperparameters and CV - batches = glob.glob(processing_dir + 'batch_*/') - - count = 0 - batch_fail = [] - - if (func!='fit' and func!='extend' and func!='merge' and func!='tune'): - 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 - + file_extentions) - else: - break - if binary is False: - file_example = fileio.load(file_example[0]) - else: - file_example = pd.read_pickle(file_example[0]) - numsubjects = file_example.shape[0] - batch_size = file_example.shape[1] - - # artificially creates files for batches that were not executed - batch_dirs = glob.glob(processing_dir + 'batch_*/') - batch_dirs = fileio.sort_nicely(batch_dirs) - for batch in batch_dirs: - filepath = glob.glob(batch + 'yhat' + outputsuffix + '*') - if filepath == []: - count = count+1 - batch1 = glob.glob(batch + '/' + job_name + '*.sh') - print(batch1) - batch_fail.append(batch1) - if collect is True: - pRho = np.ones(batch_size) - pRho = pRho.transpose() - pRho = pd.Series(pRho) - fileio.save(pRho, batch + 'pRho' + outputsuffix + - file_extentions) - - Rho = np.zeros(batch_size) - Rho = Rho.transpose() - Rho = pd.Series(Rho) - fileio.save(Rho, batch + 'Rho' + outputsuffix + - file_extentions) - - rmse = np.zeros(batch_size) - rmse = rmse.transpose() - rmse = pd.Series(rmse) - fileio.save(rmse, batch + 'RMSE' + outputsuffix + - file_extentions) - - smse = np.zeros(batch_size) - smse = smse.transpose() - smse = pd.Series(smse) - fileio.save(smse, batch + 'SMSE' + outputsuffix + - file_extentions) - - expv = np.zeros(batch_size) - expv = expv.transpose() - expv = pd.Series(expv) - fileio.save(expv, batch + 'EXPV' + outputsuffix + - file_extentions) - - msll = np.zeros(batch_size) - msll = msll.transpose() - msll = pd.Series(msll) - fileio.save(msll, batch + 'MSLL' + outputsuffix + - file_extentions) - - yhat = np.zeros([numsubjects, batch_size]) - yhat = pd.DataFrame(yhat) - fileio.save(yhat, batch + 'yhat' + outputsuffix + - file_extentions) - - ys2 = np.zeros([numsubjects, batch_size]) - ys2 = pd.DataFrame(ys2) - fileio.save(ys2, batch + 'ys2' + outputsuffix + - file_extentions) - - Z = np.zeros([numsubjects, batch_size]) - Z = pd.DataFrame(Z) - fileio.save(Z, batch + 'Z' + outputsuffix + - file_extentions) - - nll = np.zeros(batch_size) - nll = nll.transpose() - nll = pd.Series(nll) - fileio.save(nll, batch + 'NLL' + outputsuffix + - file_extentions) - - bic = np.zeros(batch_size) - bic = bic.transpose() - bic = pd.Series(bic) - fileio.save(bic, batch + 'BIC' + outputsuffix + - file_extentions) - - if not os.path.isdir(batch + 'Models'): - os.mkdir('Models') - - - else: # if more than 10% of yhat is nan then it is a failed batch - yhat = fileio.load(filepath[0]) - if np.count_nonzero(~np.isnan(yhat))/(np.prod(yhat.shape))<0.9: - count = count+1 - batch1 = glob.glob(batch + '/' + job_name + '*.sh') - print('More than 10% nans in '+ batch1[0]) - batch_fail.append(batch1) - - else: - batch_dirs = glob.glob(processing_dir + 'batch_*/') - batch_dirs = fileio.sort_nicely(batch_dirs) - for batch in batch_dirs: - filepath = glob.glob(batch + 'Models/' + 'NM_' + '*' + outputsuffix - + '*') - if len(filepath) < batch_size: - count = count+1 - batch1 = glob.glob(batch + '/' + job_name + '*.sh') - print(batch1) - batch_fail.append(batch1) - - # combines all output files across batches - if collect is True: - pRho_filenames = glob.glob(processing_dir + 'batch_*/' + 'pRho' + - outputsuffix + '*') - if pRho_filenames: - pRho_filenames = fileio.sort_nicely(pRho_filenames) - pRho_dfs = [] - for pRho_filename in pRho_filenames: - pRho_dfs.append(pd.DataFrame(fileio.load(pRho_filename))) - pRho_dfs = pd.concat(pRho_dfs, ignore_index=True, axis=0) - fileio.save(pRho_dfs, processing_dir + 'pRho' + outputsuffix + - file_extentions) - del pRho_dfs - - Rho_filenames = glob.glob(processing_dir + 'batch_*/' + 'Rho' + - outputsuffix + '*') - if Rho_filenames: - Rho_filenames = fileio.sort_nicely(Rho_filenames) - Rho_dfs = [] - for Rho_filename in Rho_filenames: - Rho_dfs.append(pd.DataFrame(fileio.load(Rho_filename))) - Rho_dfs = pd.concat(Rho_dfs, ignore_index=True, axis=0) - fileio.save(Rho_dfs, processing_dir + 'Rho' + outputsuffix + - file_extentions) - del Rho_dfs - - Z_filenames = glob.glob(processing_dir + 'batch_*/' + 'Z' + - outputsuffix + '*') - if Z_filenames: - Z_filenames = fileio.sort_nicely(Z_filenames) - Z_dfs = [] - for Z_filename in Z_filenames: - Z_dfs.append(pd.DataFrame(fileio.load(Z_filename))) - Z_dfs = pd.concat(Z_dfs, ignore_index=True, axis=1) - fileio.save(Z_dfs, processing_dir + 'Z' + outputsuffix + - file_extentions) - del Z_dfs - - yhat_filenames = glob.glob(processing_dir + 'batch_*/' + 'yhat' + - outputsuffix + '*') - if yhat_filenames: - yhat_filenames = fileio.sort_nicely(yhat_filenames) - yhat_dfs = [] - for yhat_filename in yhat_filenames: - yhat_dfs.append(pd.DataFrame(fileio.load(yhat_filename))) - yhat_dfs = pd.concat(yhat_dfs, ignore_index=True, axis=1) - fileio.save(yhat_dfs, processing_dir + 'yhat' + outputsuffix + - file_extentions) - del yhat_dfs - - ys2_filenames = glob.glob(processing_dir + 'batch_*/' + 'ys2' + - outputsuffix + '*') - if ys2_filenames: - ys2_filenames = fileio.sort_nicely(ys2_filenames) - ys2_dfs = [] - for ys2_filename in ys2_filenames: - ys2_dfs.append(pd.DataFrame(fileio.load(ys2_filename))) - ys2_dfs = pd.concat(ys2_dfs, ignore_index=True, axis=1) - fileio.save(ys2_dfs, processing_dir + 'ys2' + outputsuffix + - file_extentions) - del ys2_dfs - - rmse_filenames = glob.glob(processing_dir + 'batch_*/' + 'RMSE' + - outputsuffix + '*') - if rmse_filenames: - rmse_filenames = fileio.sort_nicely(rmse_filenames) - rmse_dfs = [] - for rmse_filename in rmse_filenames: - rmse_dfs.append(pd.DataFrame(fileio.load(rmse_filename))) - rmse_dfs = pd.concat(rmse_dfs, ignore_index=True, axis=0) - fileio.save(rmse_dfs, processing_dir + 'RMSE' + outputsuffix + - file_extentions) - del rmse_dfs - - smse_filenames = glob.glob(processing_dir + 'batch_*/' + 'SMSE' + - outputsuffix + '*') - if smse_filenames: - smse_filenames = fileio.sort_nicely(smse_filenames) - smse_dfs = [] - for smse_filename in smse_filenames: - smse_dfs.append(pd.DataFrame(fileio.load(smse_filename))) - smse_dfs = pd.concat(smse_dfs, ignore_index=True, axis=0) - fileio.save(smse_dfs, processing_dir + 'SMSE' + outputsuffix + - file_extentions) - del smse_dfs - - expv_filenames = glob.glob(processing_dir + 'batch_*/' + 'EXPV' + - outputsuffix + '*') - if expv_filenames: - expv_filenames = fileio.sort_nicely(expv_filenames) - expv_dfs = [] - for expv_filename in expv_filenames: - expv_dfs.append(pd.DataFrame(fileio.load(expv_filename))) - expv_dfs = pd.concat(expv_dfs, ignore_index=True, axis=0) - fileio.save(expv_dfs, processing_dir + 'EXPV' + outputsuffix + - file_extentions) - del expv_dfs - - msll_filenames = glob.glob(processing_dir + 'batch_*/' + 'MSLL' + - outputsuffix + '*') - if msll_filenames: - msll_filenames = fileio.sort_nicely(msll_filenames) - msll_dfs = [] - for msll_filename in msll_filenames: - msll_dfs.append(pd.DataFrame(fileio.load(msll_filename))) - msll_dfs = pd.concat(msll_dfs, ignore_index=True, axis=0) - fileio.save(msll_dfs, processing_dir + 'MSLL' + outputsuffix + - file_extentions) - del msll_dfs - - nll_filenames = glob.glob(processing_dir + 'batch_*/' + 'NLL' + - outputsuffix + '*') - if nll_filenames: - nll_filenames = fileio.sort_nicely(nll_filenames) - nll_dfs = [] - for nll_filename in nll_filenames: - nll_dfs.append(pd.DataFrame(fileio.load(nll_filename))) - nll_dfs = pd.concat(nll_dfs, ignore_index=True, axis=0) - fileio.save(nll_dfs, processing_dir + 'NLL' + outputsuffix + - file_extentions) - del nll_dfs - - bic_filenames = glob.glob(processing_dir + 'batch_*/' + 'BIC' + - outputsuffix + '*') - if bic_filenames: - bic_filenames = fileio.sort_nicely(bic_filenames) - bic_dfs = [] - for bic_filename in bic_filenames: - bic_dfs.append(pd.DataFrame(fileio.load(bic_filename))) - bic_dfs = pd.concat(bic_dfs, ignore_index=True, axis=0) - fileio.save(bic_dfs, processing_dir + 'BIC' + outputsuffix + - file_extentions) - del bic_dfs - - if (func!='predict' and func!='extend' and func!='merge' and func!='tune'): - if not os.path.isdir(processing_dir + 'Models') and \ - os.path.exists(os.path.join(batches[0], 'Models')): - os.mkdir(processing_dir + 'Models') - - meta_filenames = glob.glob(processing_dir + 'batch_*/Models/' + - 'meta_data.md') - mY = [] - sY = [] - X_scalers = [] - Y_scalers = [] - if meta_filenames: - meta_filenames = fileio.sort_nicely(meta_filenames) - with open(meta_filenames[0], 'rb') as file: - meta_data = pickle.load(file) - - for meta_filename in meta_filenames: - with open(meta_filename, 'rb') as file: - meta_data = pickle.load(file) - mY.append(meta_data['mean_resp']) - sY.append(meta_data['std_resp']) - if meta_data['inscaler'] in ['standardize', 'minmax', - 'robminmax']: - X_scalers.append(meta_data['scaler_cov']) - if meta_data['outscaler'] in ['standardize', 'minmax', - 'robminmax']: - Y_scalers.append(meta_data['scaler_resp']) - meta_data['mean_resp'] = np.squeeze(np.column_stack(mY)) - meta_data['std_resp'] = np.squeeze(np.column_stack(sY)) - meta_data['scaler_cov'] = X_scalers - meta_data['scaler_resp'] = Y_scalers - - with open(os.path.join(processing_dir, 'Models', - 'meta_data.md'), 'wb') as file: - pickle.dump(meta_data, file, protocol=PICKLE_PROTOCOL) - - batch_dirs = glob.glob(processing_dir + 'batch_*/') - if batch_dirs: - batch_dirs = fileio.sort_nicely(batch_dirs) - for b, batch_dir in enumerate(batch_dirs): - src_files = glob.glob(batch_dir + 'Models/NM*' + - outputsuffix + '.pkl') - if src_files: - src_files = fileio.sort_nicely(src_files) - for f, full_file_name in enumerate(src_files): - if os.path.isfile(full_file_name): - file_name = full_file_name.split('/')[-1] - n = file_name.split('_') - n[-2] = str(b * batch_size + f) - n = '_'.join(n) - shutil.copy(full_file_name, processing_dir + - 'Models/' + n) - elif func=='fit': - count = count+1 - batch1 = glob.glob(batch_dir + '/' + job_name + '*.sh') - print('Failed batch: ' + batch1[0]) - batch_fail.append(batch1) - - # list batches that were not executed - print('Number of batches that failed:' + str(count)) - batch_fail_df = pd.DataFrame(batch_fail) - if file_extentions == '.txt': - fileio.save_pd(batch_fail_df, processing_dir + 'failed_batches'+ - file_extentions) - else: - fileio.save(batch_fail_df, processing_dir + - 'failed_batches' + - file_extentions) - - if not batch_fail: - return True - else: - return False - -def delete_nm(processing_dir, - binary=False): - '''This function deletes all processing for normative modelling and just keeps the combined output. - - Basic usage:: - - collect_nm(processing_dir) - - :param processing_dir: Full path to the processing directory. - :param binary: Results in pkl format. - - written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. - ''' - - if binary: - file_extentions = '.pkl' - else: - file_extentions = '.txt' - for file in glob.glob(processing_dir + 'batch_*/'): - shutil.rmtree(file) - if os.path.exists(processing_dir + 'failed_batches' + file_extentions): - 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 - -def bashwrap_nm(processing_dir, - python_path, - normative_path, - job_name, - covfile_path, - respfile_path, - func='estimate', - **kwargs): - - ''' This function wraps normative modelling into a bash script to run it - on a torque cluster system. - - Basic usage:: - - bashwrap_nm(processing_dir, python_path, normative_path, job_name, covfile_path, respfile_path) - - :param processing_dir: Full path to the processing dir - :param python_path: Full path to the python distribution - :param normative_path: Full path to the normative.py - :param job_name: Name for the bash script that is the output of this function - :param covfile_path: Full path to a .txt file that contains all covariates (subjects x covariates) for the responsefile - :param respfile_path: Full path to a .txt that contains all features (subjects x features) - :param cv_folds: Number of cross validations - :param testcovfile_path: Full path to a .txt file that contains all covariates (subjects x covariates) for the testresponse file - :param testrespfile_path: Full path to a .txt file that contains all test features - :param alg: which algorithm to use - :param configparam: configuration parameters for this algorithm - - :outputs: A bash.sh file containing the commands for normative modelling saved to the processing directory (written to disk). - - written by (primarily) T Wolfers, (adapted) S Rutherford. - ''' - - # here we use pop not get to remove the arguments as they used - cv_folds = kwargs.pop('cv_folds',None) - testcovfile_path = kwargs.pop('testcovfile_path', None) - testrespfile_path = kwargs.pop('testrespfile_path', None) - alg = kwargs.pop('alg', None) - configparam = kwargs.pop('configparam', None) - # change to processing dir - os.chdir(processing_dir) - output_changedir = ['cd ' + processing_dir + '\n'] - - bash_lines = '#!/bin/bash\n' - bash_cores = 'export OMP_NUM_THREADS=1\n' - bash_environment = [bash_lines + bash_cores] - - # creates call of function for normative modelling - if (testrespfile_path is not None) and (testcovfile_path is not None): - job_call = [python_path + ' ' + normative_path + ' -c ' + - covfile_path + ' -t ' + testcovfile_path + ' -r ' + - testrespfile_path + ' -f ' + func] - elif (testrespfile_path is None) and (testcovfile_path is not None): - job_call = [python_path + ' ' + normative_path + ' -c ' + - covfile_path + ' -t ' + testcovfile_path + ' -f ' + func] - elif cv_folds is not None: - job_call = [python_path + ' ' + normative_path + ' -c ' + - covfile_path + ' -k ' + str(cv_folds) + ' -f ' + func] - elif func != 'estimate': - job_call = [python_path + ' ' + normative_path + ' -c ' + - covfile_path + ' -f ' + func] - else: - raise(ValueError, """For 'estimate' function either testcov or cvfold - must be specified.""") - - # add algorithm-specific parameters - if alg is not None: - job_call = [job_call[0] + ' -a ' + alg] - if configparam is not None: - job_call = [job_call[0] + ' -x ' + str(configparam)] - - # add standardization flag if it is false - # if not standardize: - # job_call = [job_call[0] + ' -s'] - - # add responses file - job_call = [job_call[0] + ' ' + respfile_path] - - # add in optional arguments. - for k in kwargs: - job_call = [job_call[0] + ' ' + k + '=' + kwargs[k]] - - # writes bash file into processing dir - with open(processing_dir+job_name, 'w') as bash_file: - bash_file.writelines(bash_environment + output_changedir + \ - job_call + ["\n"]) - - # changes permissoins for bash.sh file - os.chmod(processing_dir + job_name, 0o700) - - -def qsub_nm(job_path, - log_path, - memory, - duration): - - '''This function submits a job.sh scipt to the torque custer using the qsub command. - - Basic usage:: - - - qsub_nm(job_path, log_path, memory, duration) - - :param job_path: Full path to the job.sh file. - :param memory: Memory requirements written as string for example 4gb or 500mb. - :param duation: The approximate duration of the job, a string with HH:MM:SS for example 01:01:01. - - :outputs: Submission of the job to the (torque) cluster. - - written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. - ''' - - # created qsub command - if log_path is None: - qsub_call = ['echo ' + job_path + ' | qsub -N ' + job_path + ' -l ' + - 'procs=1' + ',mem=' + memory + ',walltime=' + duration] - else: - qsub_call = ['echo ' + job_path + ' | qsub -N ' + job_path + - ' -l ' + 'procs=1' + ',mem=' + memory + ',walltime=' + - duration + ' -o ' + log_path + ' -e ' + log_path] - - # submits job to cluster - #call(qsub_call, shell=True) - job_id = check_output(qsub_call, shell=True).decode(sys.stdout.encoding).replace("\n", "") - - return job_id - - -def rerun_nm(processing_dir, - log_path, - memory, - duration, - binary=False, - interactive=False): - '''This function reruns all failed batched in processing_dir after collect_nm has identified the failed batches. - Basic usage:: - - rerun_nm(processing_dir, log_path, memory, duration) - - :param processing_dir: Full path to the processing directory - :param memory: Memory requirements written as string for example 4gb or 500mb. - :param duration: The approximate duration of the job, a string with HH:MM:SS for example 01:01:01. - - written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. - ''' - - job_ids = [] - - - if binary: - file_extentions = '.pkl' - failed_batches = fileio.load(processing_dir + - 'failed_batches' + file_extentions) - shape = failed_batches.shape - for n in range(0, shape[0]): - jobpath = failed_batches[n, 0] - print(jobpath) - job_id = qsub_nm(job_path=jobpath, - log_path=log_path, - memory=memory, - duration=duration) - job_ids.append(job_id) - else: - file_extentions = '.txt' - failed_batches = fileio.load_pd(processing_dir + - 'failed_batches' + file_extentions) - shape = failed_batches.shape - for n in range(0, shape[0]): - jobpath = failed_batches.iloc[n, 0] - print(jobpath) - job_id = qsub_nm(job_path=jobpath, - log_path=log_path, - memory=memory, - duration=duration) - job_ids.append(job_id) - - if interactive: - check_jobs(job_ids, delay=60) - - -# COPY the rotines above here and aadapt those to your cluster -# bashwarp_nm; qsub_nm; rerun_nm - -def sbatchwrap_nm(processing_dir, - python_path, - normative_path, - job_name, - covfile_path, - respfile_path, - memory, - duration, - func='estimate', - **kwargs): - - '''This function wraps normative modelling into a bash script to run it - on a torque cluster system. - - Basic usage:: - - sbatchwrap_nm(processing_dir, python_path, normative_path, job_name, covfile_path, respfile_path, memory, duration) - - :param processing_dir: Full path to the processing dir - :param python_path: Full path to the python distribution - :param normative_path: Full path to the normative.py - :param job_name: Name for the bash script that is the output of this function - :param covfile_path: Full path to a .txt file that contains all covariates (subjects x covariates) for the responsefile - :param respfile_path: Full path to a .txt that contains all features (subjects x features) - :param cv_folds: Number of cross validations - :param testcovfile_path: Full path to a .txt file that contains all covariates (subjects x covariates) for the testresponse file - :param testrespfile_path: Full path to a .txt file that contains all test features - :param alg: which algorithm to use - :param configparam: configuration parameters for this algorithm - - :outputs: A bash.sh file containing the commands for normative modelling saved to the processing directory (written to disk). - - written by (primarily) T Wolfers, (adapted) S Rutherford - ''' - - # here we use pop not get to remove the arguments as they used - cv_folds = kwargs.pop('cv_folds',None) - testcovfile_path = kwargs.pop('testcovfile_path', None) - testrespfile_path = kwargs.pop('testrespfile_path', None) - alg = kwargs.pop('alg', None) - configparam = kwargs.pop('configparam', None) - - # change to processing dir - os.chdir(processing_dir) - output_changedir = ['cd ' + processing_dir + '\n'] - - sbatch_init='#!/bin/bash\n' - sbatch_jobname='#SBATCH --job-name=' + processing_dir + '\n' - sbatch_account='#SBATCH --account=p33_norment\n' - sbatch_nodes='#SBATCH --nodes=1\n' - sbatch_tasks='#SBATCH --ntasks=1\n' - sbatch_time='#SBATCH --time=' + str(duration) + '\n' - sbatch_memory='#SBATCH --mem-per-cpu=' + str(memory) + '\n' - sbatch_module='module purge\n' - sbatch_anaconda='module load anaconda3\n' - sbatch_exit='set -o errexit\n' - - #echo -n "This script is running on " - #hostname - - bash_environment = [sbatch_init + - sbatch_jobname + - sbatch_account + - sbatch_nodes + - sbatch_tasks + - sbatch_time + - sbatch_module + - sbatch_anaconda] - - # creates call of function for normative modelling - if (testrespfile_path is not None) and (testcovfile_path is not None): - job_call = [python_path + ' ' + normative_path + ' -c ' + - covfile_path + ' -t ' + testcovfile_path + ' -r ' + - testrespfile_path + ' -f ' + func] - elif (testrespfile_path is None) and (testcovfile_path is not None): - job_call = [python_path + ' ' + normative_path + ' -c ' + - covfile_path + ' -t ' + testcovfile_path + ' -f ' + func] - elif cv_folds is not None: - job_call = [python_path + ' ' + normative_path + ' -c ' + - covfile_path + ' -k ' + str(cv_folds) + ' -f ' + func] - elif func != 'estimate': - job_call = [python_path + ' ' + normative_path + ' -c ' + - covfile_path + ' -f ' + func] - else: - raise(ValueError, """For 'estimate' function either testcov or cvfold - must be specified.""") - - # add algorithm-specific parameters - if alg is not None: - job_call = [job_call[0] + ' -a ' + alg] - if configparam is not None: - job_call = [job_call[0] + ' -x ' + str(configparam)] - - # add standardization flag if it is false - # if not standardize: - # job_call = [job_call[0] + ' -s'] - - # add responses file - job_call = [job_call[0] + ' ' + respfile_path] - - # add in optional arguments. - for k in kwargs: - job_call = [job_call[0] + ' ' + k + '=' + kwargs[k]] - - # writes bash file into processing dir - with open(processing_dir+job_name, 'w') as bash_file: - bash_file.writelines(bash_environment + output_changedir + \ - job_call + ["\n"] + [sbatch_exit]) - - # changes permissoins for bash.sh file - os.chmod(processing_dir + job_name, 0o700) - -def sbatch_nm(job_path, - log_path): - - '''This function submits a job.sh scipt to the torque custer using the qsub - command. - - Basic usage:: - - sbatch_nm(job_path, log_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. - - written by (primarily) T Wolfers, (adapted) S Rutherford. - ''' - - # created qsub command - sbatch_call = ['sbatch ' + job_path] - - # submits job to cluster - call(sbatch_call, shell=True) - -def sbatchrerun_nm(processing_dir, - memory, - duration, - new_memory=False, - new_duration=False, - binary=False, - **kwargs): - - '''This function reruns all failed batched in processing_dir after collect_nm has identified he failed batches. - - Basic usage:: - - rerun_nm(processing_dir, memory, duration) - - :param processing_dir: Full path to the processing directory. - :param memory: Memory requirements written as string, for example 4gb or 500mb. - :param duration: The approximate duration of the job, a string with HH:MM:SS for example 01:01:01. - :param new_memory: If you want to change the memory you have to indicate it here. - :param new_duration: If you want to change the duration you have to indicate it here. - - :outputs: Re-runs failed batches. - - written by (primarily) T Wolfers, (adapted) S Rutherford. - ''' - log_path = kwargs.pop('log_path', None) - - if binary: - file_extentions = '.pkl' - failed_batches = fileio.load(processing_dir + 'failed_batches' + file_extentions) - shape = failed_batches.shape - for n in range(0, shape[0]): - jobpath = failed_batches[n, 0] - print(jobpath) - if new_duration != False: - 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) - - else: - file_extentions = '.txt' - failed_batches = fileio.load_pd(processing_dir + 'failed_batches' + file_extentions) - shape = failed_batches.shape - for n in range(0, shape[0]): - jobpath = failed_batches.iloc[n, 0] - print(jobpath) - if new_duration != False: - 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) - - -def retrieve_jobs(): - """ - A utility function to retrieve task status from the outputs of qstat. - - :return: a dictionary of jobs. - - """ - - output = check_output('qstat', shell=True).decode(sys.stdout.encoding) - output = output.split('\n') - jobs = dict() - 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 - - return jobs - - -def check_job_status(jobs): - """ - A utility function to count the tasks with different status. - - :param jobs: List of job ids. - :return: returns the number of taks athat are queued, running, completed etc - - """ - running_jobs = retrieve_jobs() - - r = 0 - c = 0 - q = 0 - u = 0 - 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 - continue - - print('Total Jobs:%d, Queued:%d, Running:%d, Completed:%d, Unknown:%d' - %(len(jobs), q, r, c, u)) - return q,r,c,u - - -def check_jobs(jobs, delay=60): - """ - A utility function for chacking the status of submitted jobs. - - :param jobs: list of job ids. - :param delay: the delay (in sec) between two consequative checks, defaults to 60. - - """ - - n = len(jobs) - - while(True): - q,r,c,u = check_job_status(jobs) - if c == n: - print('All jobs are completed!') - break - time.sleep(delay) - - diff --git a/build/lib/pcntoolkit/trendsurf.py b/build/lib/pcntoolkit/trendsurf.py deleted file mode 100644 index e0f0d6e5..00000000 --- a/build/lib/pcntoolkit/trendsurf.py +++ /dev/null @@ -1,253 +0,0 @@ -#!/Users/andre/sfw/anaconda3/bin/python - -# ------------------------------------------------------------------------------ -# Usage: -# python trendsurf.py -m [maskfile] -b [basis] -c [covariates] -# -# Written by A. Marquand -# ------------------------------------------------------------------------------ - -from __future__ import print_function -from __future__ import division - -import os -import sys -import numpy as np -import nibabel as nib -import argparse - -try: # Run as a package if installed - from pcntoolkit.dataio import fileio - from pcntoolkit.model.bayesreg import BLR -except ImportError: - pass - path = os.path.abspath(os.path.dirname(__file__)) - if path not in sys.path: - sys.path.append(path) - del path - - from dataio import fileio - from model.bayesreg import BLR - - - -def load_data(datafile, maskfile=None): - """ load 4d nifti data """ - if datafile.endswith("nii.gz") or datafile.endswith("nii"): - # we load the data this way rather than fileio.load() because we need - # access to the volumetric representation (to know the # coordinates) - dat = fileio.load_nifti(datafile, vol=True) - dim = dat.shape - if len(dim) <= 3: - dim = dim + (1,) - else: - raise ValueError("No routine to handle non-nifti data") - - mask = fileio.create_mask(dat, mask=maskfile) - - dat = fileio.vol2vec(dat, mask) - maskid = np.where(mask.ravel())[0] - - # generate voxel coordinates - i, j, k = np.meshgrid(np.linspace(0, dim[0]-1, dim[0]), - np.linspace(0, dim[1]-1, dim[1]), - np.linspace(0, dim[2]-1, dim[2]), indexing='ij') - - # voxel-to-world mapping - img = nib.load(datafile) - world = np.vstack((i.ravel(), j.ravel(), k.ravel(), - np.ones(np.prod(i.shape), float))).T - world = np.dot(world, img.affine.T)[maskid, 0:3] - - return dat, world, mask - - -def create_basis(X, basis, mask): - """ Create a (polynomial) basis set """ - - # check whether we are using a polynomial basis set - if type(basis) is int or (type(basis) is str and len(basis) == 1): - dimpoly = int(basis) - dimx = X.shape[1] - print('Generating polynomial basis set of degree', dimpoly, '...') - Phi = np.zeros((X.shape[0], X.shape[1]*dimpoly)) - colid = np.arange(0, dimx) - for d in range(1, dimpoly+1): - Phi[:, colid] = X ** d - colid += dimx - else: # custom basis set - if type(basis) is str: - print('Loading custom basis set from', basis) - - # Phi_vol = fileio.load_data(basis) - # we load the data this way instead so we can apply the same mask - Phi_vol = fileio.load_nifti(basis, vol=True) - Phi = fileio.vol2vec(Phi_vol, mask) - print('Basis set consists of', Phi.shape[1], 'basis functions.') - # maskid = np.where(mask.ravel())[0] - else: - raise ValueError("I don't know what to do with basis:", basis) - - return Phi - - -def write_nii(data, filename, examplenii, mask): - """ Write output to nifti """ - - # load example image - ex_img = nib.load(examplenii) - dim = ex_img.shape[0:3] - nvol = int(data.shape[1]) - - # write data - array_data = np.zeros((np.prod(dim), nvol)) - array_data[mask.flatten(), :] = data - array_data = np.reshape(array_data, dim+(nvol,)) - array_img = nib.Nifti1Image(array_data, - ex_img.get_affine(), - ex_img.get_header()) - nib.save(array_img, filename) - - -def get_args(*args): - # parse arguments - parser = argparse.ArgumentParser(description="Trend surface model") - parser.add_argument("filename") - parser.add_argument("-b", help="basis set", dest="basis", default=3) - 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("-a", help="use ARD", action='store_true') - parser.add_argument("-o", help="output all measures", action='store_true') - args = parser.parse_args() - wdir = os.path.realpath(os.path.curdir) - filename = os.path.join(wdir, args.filename) - if args.maskfile is None: - maskfile = None - else: - maskfile = os.path.join(wdir, args.maskfile) - basis = args.basis - if args.covfile is not None: - raise(NotImplementedError, "Covariates not implemented yet.") - - return filename, maskfile, basis, args.a, args.o - - -def estimate(filename, maskfile, basis, ard=False, outputall=False, - saveoutput=True): - """ Estimate a trend surface model - - This will estimate a trend surface model, independently for each subject. - This is currently fit using a polynomial model of a specified degree. - The models are estimated on the basis of data stored on disk in ascii or - neuroimaging data formats (currently nifti only). Ascii data should be in - tab or space delimited format with the number of voxels in rows and the - number of subjects in columns. Neuroimaging data will be reshaped - into the appropriate format - - Basic usage:: - - estimate(filename, maskfile, basis) - - where the variables are defined below. Note that either the cfolds - parameter or (testcov, testresp) should be specified, but not both. - - :param filename: 4-d nifti file containing the images to be estimated - :param maskfile: nifti mask used to apply to the data - :param basis: model order for the interpolating polynomial - - All outputs are written to disk in the same format as the input. These are: - - :outputs: * yhat - predictive mean - * ys2 - predictive variance - * trendcoeff - coefficients from the trend surface model - * negloglik - Negative log marginal likelihood - * hyp - hyperparameters - * explainedvar - explained variance - * rmse - standardised mean squared error - """ - - # load data - print("Processing data in", filename) - Y, X, mask = load_data(filename, maskfile) - Y = np.round(10000*Y)/10000 # truncate precision to avoid numerical probs - if len(Y.shape) == 1: - Y = Y[:, np.newaxis] - N = Y.shape[1] - - # standardize responses and covariates - mY = np.mean(Y, axis=0) - sY = np.std(Y, axis=0) - Yz = (Y - mY) / sY - mX = np.mean(X, axis=0) - sX = np.std(X, axis=0) - Xz = (X - mX) / sX - - # create basis set and set starting hyperparamters - Phi = create_basis(Xz, basis, mask) - if ard is True: - hyp0 = np.zeros(Phi.shape[1]+1) - else: - hyp0 = np.zeros(2) - - # estimate the models for all subjects - if ard: - print('ARD is enabled') - yhat = np.zeros_like(Yz) - ys2 = np.zeros_like(Yz) - nlZ = np.zeros(N) - hyp = np.zeros((N, len(hyp0))) - rmse = np.zeros(N) - ev = np.zeros(N) - m = np.zeros((N, Phi.shape[1])) - bs2 = np.zeros((N, Phi.shape[1])) - for i in range(0, N): - print("Estimating model ", i+1, "of", N) - breg = BLR() - hyp[i, :] = breg.estimate(hyp0, Phi, Yz[:, i]) - m[i, :] = breg.m - nlZ[i] = breg.nlZ - - # compute extra measures (e.g. marginal variances)? - if outputall: - bs2[i] = np.sqrt(np.diag(np.linalg.inv(breg.A))) - - # compute predictions and errors - yhat[:, i], ys2[:, i] = breg.predict(hyp[i, :], Phi, Yz[:, i], Phi) - yhat[:, i] = yhat[:, i]*sY[i] + mY[i] - rmse[i] = np.sqrt(np.mean((Y[:, i] - yhat[:, i]) ** 2)) - ev[i] = 100*(1 - (np.var(Y[:, i] - yhat[:, i]) / np.var(Y[:, i]))) - - print("Variance explained =", ev[i], "% RMSE =", rmse[i]) - - print("Mean (std) variance explained =", ev.mean(), "(", ev.std(), ")") - print("Mean (std) RMSE =", rmse.mean(), "(", rmse.std(), ")") - - # Write output - if saveoutput: - print("Writing output ...") - np.savetxt("trendcoeff.txt", m, delimiter='\t', fmt='%5.8f') - np.savetxt("negloglik.txt", nlZ, delimiter='\t', fmt='%5.8f') - np.savetxt("hyp.txt", hyp, delimiter='\t', fmt='%5.8f') - np.savetxt("explainedvar.txt", ev, delimiter='\t', fmt='%5.8f') - np.savetxt("rmse.txt", rmse, delimiter='\t', fmt='%5.8f') - fileio.save_nifti(yhat, 'yhat.nii.gz', filename, mask) - fileio.save_nifti(ys2, 'ys2.nii.gz', filename, mask) - - if outputall: - np.savetxt("trendcoeffvar.txt", bs2, delimiter='\t', fmt='%5.8f') - else: - out = [yhat, ys2, nlZ, hyp, rmse, ev, m] - if outputall: - out.append(bs2) - return out - -def main(*args): - np.seterr(invalid='ignore') - - filename, maskfile, basis, ard, outputall = get_args(args) - estimate(filename, maskfile, basis, ard, outputall) - -# For running from the command line: -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/build/lib/pcntoolkit/util/__init__.py b/build/lib/pcntoolkit/util/__init__.py deleted file mode 100644 index 9f9161bf..00000000 --- a/build/lib/pcntoolkit/util/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import utils \ No newline at end of file diff --git a/build/lib/pcntoolkit/util/hbr_utils.py b/build/lib/pcntoolkit/util/hbr_utils.py deleted file mode 100644 index db7a5ccd..00000000 --- a/build/lib/pcntoolkit/util/hbr_utils.py +++ /dev/null @@ -1,236 +0,0 @@ -from __future__ import print_function -import os -import sys -import numpy as np -from scipy import stats -import scipy.special as spp -import pickle -import matplotlib.pyplot as plt -import pandas as pd -import pymc3 as pm -from pcntoolkit.model.SHASH import * -from pcntoolkit.model.hbr import bspline_transform - -""" -@author: augub -""" - -def MCMC_estimate(f, trace): - """Get an MCMC estimate of f given a trace""" - out = np.zeros_like(f(trace.point(0))) - n=0 - for p in trace.points(): - out += f(p) - n+=1 - return out/n - - -def get_MCMC_zscores(X, Y, Z, model): - """Get an MCMC estimate of the z-scores of Y""" - def f(sample): - return get_single_zscores(X, Y, Z, model,sample) - return MCMC_estimate(f, model.hbr.trace) - - -def get_single_zscores(X, Y, Z, model, sample): - """Get the z-scores of y, given clinical covariates and a model""" - likelihood = model.configs['likelihood'] - params = forward(X,Z,model,sample) - return z_score(Y, params, likelihood = likelihood) - - -def z_score(Y, params, likelihood = "Normal"): - """Get the z-scores of Y, given likelihood parameters""" - if likelihood.startswith('SHASH'): - mu = params['mu'] - sigma = params['sigma'] - epsilon = params['epsilon'] - delta = params['delta'] - if likelihood == "SHASHo": - 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) - 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) - SHASH = SHASH_c * true_sigma + true_mu - Z = np.sinh(np.arcsinh(SHASH) * delta - epsilon) - elif likelihood == 'Normal': - Z = (Y-params['mu'])/params['sigma'] - else: - exit("Unsupported likelihood") - return Z - - -def get_MCMC_quantiles(synthetic_X, z_scores, model, be): - """Get an MCMC estimate of the quantiles""" - """This does not use the get_single_quantiles function, for memory efficiency""" - resolution = synthetic_X.shape[0] - synthetic_X_transformed = model.hbr.transform_X(synthetic_X) - be = np.reshape(np.array(be),(1,-1)) - synthetic_Z = np.repeat(be, resolution, axis = 0) - z_scores = np.reshape(np.array(z_scores),(1,-1)) - zs = np.repeat(z_scores, resolution, axis=0) - def f(sample): - params = forward(synthetic_X_transformed,synthetic_Z, model,sample) - q = quantile(zs, params, likelihood = model.configs['likelihood']) - return q - out = MCMC_estimate(f, model.hbr.trace) - return out - - -def get_single_quantiles(synthetic_X, z_scores, model, be, sample): - """Get the quantiles within a given range of covariates, given a model""" - resolution = synthetic_X.shape[0] - synthetic_X_transformed = model.hbr.transform_X(synthetic_X) - be = np.reshape(np.array(be),(1,-1)) - synthetic_Z = np.repeat(be, resolution, axis = 0) - z_scores = np.reshape(np.array(z_scores),(1,-1)) - zs = np.repeat(z_scores, resolution, axis=0) - params = forward(synthetic_X_transformed,synthetic_Z, model,sample) - q = quantile(zs, params, likelihood = model.configs['likelihood']) - return q - - -def quantile(zs, params, likelihood = "Normal"): - """Get the zs'th quantiles given likelihood parameters""" - if likelihood.startswith('SHASH'): - mu = params['mu'] - sigma = params['sigma'] - epsilon = params['epsilon'] - delta = params['delta'] - if likelihood == "SHASHo": - quantiles = S_inv(zs,epsilon,delta)*sigma + mu - elif likelihood == "SHASHo2": - 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) - quantiles = SHASH_c *sigma + mu - elif likelihood == 'Normal': - quantiles = zs*params['sigma'] + params['mu'] - else: - exit("Unsupported likelihood") - return quantiles - - -def single_parameter_forward(X, Z, model, sample, p_name): - """Get a likelihood paramameter given covariates, batch-effects and model parameters""" - outs = np.zeros(X.shape[0])[:,None] - all_bes = np.unique(Z,axis=0) - for be in all_bes: - bet = tuple(be) - idx = (Z==be).all(1) - if model.configs[f"linear_{p_name}"]: - if model.configs[f'random_slope_{p_name}']: - slope_be = sample[f"slope_{p_name}"][bet] - else: - slope_be = sample[f"slope_{p_name}"] - if model.configs[f'random_intercept_{p_name}']: - intercept_be = sample[f"intercept_{p_name}"][bet] - else: - intercept_be = sample[f"intercept_{p_name}"] - - out = (X[np.squeeze(idx),:]@slope_be)[:,None] + intercept_be - outs[np.squeeze(idx),:] = out - else: - if model.configs[f'random_{p_name}']: - outs[np.squeeze(idx),:] = sample[p_name][bet] - else: - outs[np.squeeze(idx),:] = sample[p_name] - - return outs - - -def forward(X, Z, model, sample): - """Get all likelihood paramameters given covariates and batch-effects and model parameters""" - # TODO think if this is the correct spot for this - mapfuncs={'sigma': lambda x: np.log(1+np.exp(x)), 'delta':lambda x :np.log(1+np.exp(x)) + 0.3} - - likelihood = model.configs['likelihood'] - - if likelihood == 'Normal': - parameter_list = ['mu','sigma'] - elif likelihood in ['SHASHb','SHASHo','SHASHo2']: - parameter_list = ['mu','sigma','epsilon','delta'] - else: - exit("Unsupported likelihood") - - for i in parameter_list: - if not (i in mapfuncs.keys()): - mapfuncs[i] = lambda x: x - - output_dict = {p_name:np.zeros(X.shape) for p_name in parameter_list} - - for p_name in parameter_list: - output_dict[p_name] = mapfuncs[p_name](single_parameter_forward(X,Z,model,sample,p_name)) - - return output_dict - - -def Rhats(model, thin = 1, resolution = 100, varnames = None): - """Get Rhat as function of sampling iteration""" - trace = model.hbr.trace - - if varnames == None: - varnames = trace.varnames - chain_length = trace.get_values(varnames[0],chains=trace.chains[0], thin=thin).shape[0] - - interval_skip=chain_length//resolution - - rhat_dict = {} - - for varname in varnames: - testvar = np.stack(trace.get_values(varname,combine=False)) - vardim = testvar.reshape((testvar.shape[0], testvar.shape[1], -1)).shape[2] - rhats_var = np.zeros((resolution, vardim)) - - var = np.stack(trace.get_values(varname,combine=False)) - var = var.reshape((var.shape[0], var.shape[1], -1)) - for v in range(var.shape[2]): - for j in range(resolution): - rhats_var[j,v] = pm.rhat(var[:,:j*interval_skip,v]) - rhat_dict[varname] = rhats_var - return rhat_dict - - -def S_inv(x, e, d): - return np.sinh((np.arcsinh(x) + e) / d) - -def K(p, x): - return np.array(spp.kv(p, x)) - -def P(q): - """ - The P function as given in Jones et al. - :param q: - :return: - """ - frac = np.exp(1 / 4) / np.sqrt(8 * np.pi) - K1 = K((q + 1) / 2, 1 / 4) - K2 = K((q - 1) / 2, 1 / 4) - a = (K1 + K2) * frac - return a - -def m(epsilon, delta, r): - """ - The r'th uncentered moment. Given by Jones et al. - """ - frac1 = 1 / np.power(2, r) - acc = 0 - for i in range(r + 1): - combs = spp.comb(r, i) - flip = np.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 - - - diff --git a/build/lib/pcntoolkit/util/utils.py b/build/lib/pcntoolkit/util/utils.py deleted file mode 100644 index 24bb8dcc..00000000 --- a/build/lib/pcntoolkit/util/utils.py +++ /dev/null @@ -1,1507 +0,0 @@ -from __future__ import print_function - -import os -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 -import matplotlib.pyplot as plt -import pandas as pd -import bspline -from bspline import splinelab -from sklearn.datasets import make_regression -import pymc3 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 -except ImportError: - pass - - path = os.path.abspath(os.path.dirname(__file__)) - rootpath = os.path.dirname(path) # parent directory - if rootpath not in sys.path: - sys.path.append(rootpath) - del path, rootpath - import configs - -PICKLE_PROTOCOL = configs.PICKLE_PROTOCOL - -# ----------------- -# Utility functions -# ----------------- -def create_poly_basis(X, dimpoly): - """ - Compute a polynomial basis expansion of the specified order - - """ - - if len(X.shape) == 1: - X = X[:, np.newaxis] - D = X.shape[1] - Phi = np.zeros((X.shape[0], D*dimpoly)) - colid = np.arange(0, D) - for d in range(1, dimpoly+1): - Phi[:, colid] = X ** d - colid += D - - return Phi - -def create_bspline_basis(xmin, xmax, p = 3, nknots = 5): - """ - Compute a Bspline basis set where: - - :param p: order of spline (3 = cubic) - :param nknots: number of knots (endpoints only counted once) - - """ - - knots = np.linspace(xmin, xmax, nknots) - k = splinelab.augknt(knots, p) # pad the knot vector - B = bspline.Bspline(k, p) - return B - -def create_design_matrix(X, intercept = True, basis = 'bspline', - basis_column = 0, site_ids=None, all_sites=None, - **kwargs): - """ - Prepare a design matrix from a set of covariates sutiable for - running Bayesian linar regression. This design matrix consists of - a set of user defined covariates, optoinal site intercepts - (fixed effects) and also optionally a nonlinear basis expansion over - one of the columns - - :param X: matrix of covariates - :param basis: type of basis expansion to use - :param basis_column: which colume to perform the expansion over? - :param site_ids: list of site ids (one per data point) - :param all_sites: list of unique site ids - :param p: order of spline (3 = cubic) - :param nknots: number of knots (endpoints only counted once) - - if site_ids is specified, this must have the same number of entries as - there are rows in X. If all_sites is specfied, these will be used to - create the site identifiers in place of site_ids. This accommocdates - the scenario where not all the sites used to create the model are - present in the test set (i.e. there will be some empty site columns). - - """ - - xmin = kwargs.pop('xmin', 0) - xmax = kwargs.pop('xmax', 100) - - N = X.shape[0] - - if type(X) is pd.DataFrame: - X = X.to_numpy() - - # add intercept column - if intercept: - Phi = np.concatenate((np.ones((N, 1)), X), axis=1) - else: - Phi = X - - # add dummy coded site columns - if all_sites is None: - if site_ids is not None: - all_sites = sorted(pd.unique(site_ids)) - - if site_ids is None: - if all_sites is None: - site_cols = None - else: - # site ids are not specified, but all_sites are - site_cols = np.zeros((N, len(all_sites))) - else: - # site ids are defined - # make sure the data are in pandas format - if type(site_ids) is not pd.Series: - site_ids = pd.Series(data=site_ids) - #site_ids = pd.Series(data=site_ids) - - # make sure all_sites is defined - if all_sites is None: - all_sites = sorted(pd.unique(site_ids)) - - # dummy code the sites - site_cols = np.zeros((N, len(all_sites))) - for i, s in enumerate(all_sites): - site_cols[:, i] = site_ids == s - - if site_cols.shape[0] != N: - raise ValueError('site cols must have the same number of rows as X') - - if site_cols is not None: - Phi = np.concatenate((Phi, site_cols), axis=1) - - # create Bspline basis set - if basis == 'bspline': - B = create_bspline_basis(xmin, xmax, **kwargs) - Phi = np.concatenate((Phi, np.array([B(i) for i in X[:,basis_column]])), axis=1) - elif basis == 'poly': - Phi = np.concatenate(Phi, create_poly_basis(X[:,basis_column], **kwargs)) - - return Phi - -def squared_dist(x, z=None): - """ - Compute sum((x-z) ** 2) for all vectors in a 2d array. - - """ - - # do some basic checks - if z is None: - z = x - if len(x.shape) == 1: - x = x[:, np.newaxis] - if len(z.shape) == 1: - z = z[:, np.newaxis] - - nx, dx = x.shape - nz, dz = z.shape - if dx != dz: - raise ValueError(""" - Cannot compute distance: vectors have different length""") - - # mean centre for numerical stability - m = np.mean(np.vstack((np.mean(x, axis=0), np.mean(z, axis=0))), axis=0) - x = x - m - z = z - m - - xx = np.tile(np.sum((x*x), axis=1)[:, np.newaxis], (1, nz)) - zz = np.tile(np.sum((z*z), axis=1), (nx, 1)) - - dist = (xx - 2*x.dot(z.T) + zz) - - return dist - - -def compute_pearsonr(A, B): - """ - Manually computes the Pearson correlation between two matrices. - - Basic usage:: - - compute_pearsonr(A, B) - - :param A: an N * M data array - :param cov: an N * M array - - :returns Rho: N dimensional vector of correlation coefficients - :returns ys2: N dimensional vector of p-values - - Notes:: - - This function is useful when M is large and only the diagonal entries - of the resulting correlation matrix are of interest. This function - does not compute the full correlation matrix as an intermediate step - - """ - - # N = A.shape[1] - N = A.shape[0] - - # first mean centre - Am = A - np.mean(A, axis=0) - Bm = B - np.mean(B, axis=0) - # then normalize - An = Am / np.sqrt(np.sum(Am**2, axis=0)) - Bn = Bm / np.sqrt(np.sum(Bm**2, axis=0)) - del(Am, Bm) - - Rho = np.sum(An * Bn, axis=0) - del(An, Bn) - - # Fisher r-to-z - Zr = (np.arctanh(Rho) - np.arctanh(0)) * np.sqrt(N - 3) - N = stats.norm() - pRho = 2*N.cdf(-np.abs(Zr)) - # pRho = 1-N.cdf(Zr) - - return Rho, pRho - -def explained_var(ytrue, ypred): - """ - Computes the explained variance of predicted values. - - Basic usage:: - - exp_var = explained_var(ytrue, ypred) - - where - - :ytrue: n*p matrix of true values where n is the number of samples - and p is the number of features. - :ypred: n*p matrix of predicted values where n is the number of samples - and p is the number of features. - - :returns exp_var: p dimentional vector of explained variances for each feature. - - """ - - exp_var = 1 - (ytrue - ypred).var(axis = 0) / ytrue.var(axis = 0) - - return exp_var - -def compute_MSLL(ytrue, ypred, ypred_var, train_mean = None, train_var = None): - """ - Computes the MSLL or MLL (not standardized) if 'train_mean' and 'train_var' are None. - - Basic usage:: - - MSLL = compute_MSLL(ytrue, ypred, ytrue_sig, noise_variance, train_mean, train_var) - - where - - :param ytrue : n*p matrix of true values where n is the number of samples - and p is the number of features. - :param ypred : n*p matrix of predicted values where n is the number of samples - and p is the number of features. - :param ypred_var : n*p matrix of summed noise variances and prediction variances where n is the number of samples - and p is the number of features. - - :param train_mean: p dimensional vector of mean values of the training data for each feature. - - :param train_var : p dimensional vector of covariances of the training data for each feature. - - :returns loss : p dimensional vector of MSLL or MLL for each feature. - - """ - - if train_mean is not None and train_var is not None: - - # make sure y_train_mean and y_train_sig have right dimensions (subjects x voxels): - Y_train_mean = np.repeat(train_mean, ytrue.shape[0], axis = 0) - Y_train_sig = np.repeat(train_var, ytrue.shape[0], axis = 0) - - # compute MSLL: - loss = np.mean(0.5 * np.log(2 * np.pi * ypred_var) + (ytrue - ypred)**2 / (2 * ypred_var) - - 0.5 * np.log(2 * np.pi * Y_train_sig) - (ytrue - Y_train_mean)**2 / (2 * Y_train_sig), axis = 0) - - else: - # compute MLL: - loss = np.mean(0.5 * np.log(2 * np.pi * ypred_var) + (ytrue - ypred)**2 / (2 * ypred_var), axis = 0) - - return loss - -def calibration_descriptives(x): - """ - Compute statistics useful to assess the calibration of normative models, - including skew and kurtosis of the distribution, plus their standard - deviation and standar errors (separately for each column in x) - - Basic usage:: - stats = calibration_descriptives(Z) - - where - - :param x : n*p matrix of statistics you wish to assess - :returns stats :[skew, sdskew, kurtosis, sdkurtosis, semean, sesd] - - """ - - n = np.shape(x)[0] - m1 = np.mean(x,axis=0) - m2 = sum((x-m1)**2) - m3 = sum((x-m1)**3) - m4 = sum((x-m1)**4) - 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) - sdkurtosis = np.sqrt( 4*(n**2-1) * sdskew**2 / ((n-3)*(n+5)) ) - semean = np.sqrt(np.var(x)/n) - sesd = s1/np.sqrt(2*(n-1)) - cd = [skew, sdskew, kurtosis, sdkurtosis, semean, sesd] - - return cd - -class WarpBase(with_metaclass(ABCMeta)): - """ - Base class for likelihood warping following: - Rios and Torab (2019) Compositionally-warped Gaussian processes - https://www.sciencedirect.com/science/article/pii/S0893608019301856 - - All Warps must define the following methods:: - - Warp.get_n_params() - return number of parameters - Warp.f() - warping function (Non-Gaussian field -> Gaussian) - Warp.invf() - inverse warp - Warp.df() - derivatives - Warp.warp_predictions() - compute predictive distribution - - """ - - def __init__(self): - self.n_params = np.nan - - def get_n_params(self): - """ Report the number of parameters required """ - - assert not np.isnan(self.n_params), \ - "Warp function not initialised" - - return self.n_params - - def warp_predictions(self, mu, s2, param, percentiles=[0.025, 0.975]): - """ - Compute the warped predictions from a gaussian predictive - distribution, specifed by a mean (mu) and variance (s2) - - :param mu: Gassian predictive mean - :param s2: Predictive variance - :param param: warping parameters - :param percentiles: Desired percentiles of the warped likelihood - - :returns: * median - median of the predictive distribution - * pred_interval - predictive interval(s) - - """ - - # Compute percentiles of a standard Gaussian - N = norm - Z = N.ppf(percentiles) - - # find the median (using mu = median) - median = self.invf(mu, param) - - # compute the predictive intervals (non-stationary) - pred_interval = np.zeros((len(mu), len(Z))) - for i, z in enumerate(Z): - pred_interval[:,i] = self.invf(mu + np.sqrt(s2)*z, param) - - return median, pred_interval - - @abstractmethod - def f(self, x, param): - """ Evaluate the warping function (mapping non-Gaussian respone - variables to Gaussian variables) - """ - - @abstractmethod - def invf(self, y, param): - """ Evaluate the warping function (mapping Gaussian latent variables - to non-Gaussian response variables) - """ - - @abstractmethod - def df(self, x, param): - """ Return the derivative of the warp, dw(x)/dx """ - -class WarpLog(WarpBase): - """ Affine warp - y = a + b*x - """ - - def __init__(self): - self.n_params = 0 - - def f(self, x, params=None): - - y = np.log(x) - - return y - - def invf(self, y, params=None): - - x = np.exp(y) - - return x - - def df(self, x, params): - - df = 1/x - - return df - -class WarpAffine(WarpBase): - """ Affine warp - y = a + b*x - """ - - def __init__(self): - self.n_params = 2 - - def _get_params(self, param): - if len(param) != self.n_params: - raise(ValueError, - 'number of parameters must be ' + str(self.n_params)) - return param[0], np.exp(param[1]) - - def f(self, x, params): - a, b = self._get_params(params) - - y = a + b*x - return y - - def invf(self, y, params): - a, b = self._get_params(params) - - x = (y - a) / b - - return x - - def df(self, x, params): - a, b = self._get_params(params) - - df = np.ones(x.shape)*b - return df - -class WarpBoxCox(WarpBase): - """ Box cox transform having a single parameter (lambda), i.e. - - y = (sign(x) * abs(x) ** lamda - 1) / lambda - - This follows the generalization in Bicken and Doksum (1981) JASA 76 - and allows x to assume negative values. - """ - - def __init__(self): - self.n_params = 1 - - def _get_params(self, param): - - return np.exp(param) - - def f(self, x, params): - lam = self._get_params(params) - - if lam == 0: - y = np.log(x) - else: - y = (np.sign(x) * np.abs(x) ** lam - 1) / lam - return y - - def invf(self, y, params): - lam = self._get_params(params) - - if lam == 0: - x = np.exp(y) - else: - x = np.sign(lam * y + 1) * np.abs(lam * y + 1) ** (1 / lam) - - return x - - def df(self, x, params): - lam = self._get_params(params) - - dx = np.abs(x) ** (lam - 1) - - return dx - -class WarpSinArcsinh(WarpBase): - """ Sin-hyperbolic arcsin warp having two parameters (a, b) and defined by - - y = sinh(b * arcsinh(x) - a) - - Using the parametrisation of Rios et al, Neural Networks 118 (2017) - where a controls skew and b controls kurtosis, such that: - - * a = 0 : symmetric - * a > 0 : positive skew - * a < 0 : negative skew - * b = 1 : mesokurtic - * b > 1 : leptokurtic - * b < 1 : platykurtic - - where b > 0. However, it is more convenentent to use an alternative - parameterisation, given in Jones and Pewsey 2019 JRSS Significance 16 - https://doi.org/10.1111/j.1740-9713.2019.01245.x - - where: - - y = sinh(b * arcsinh(x) + epsilon * b) - - and a = -epsilon*b - - see also Jones and Pewsey 2009 Biometrika, 96 (4) for more details - about the SHASH distribution - https://www.jstor.org/stable/27798865 - """ - - def __init__(self): - self.n_params = 2 - - def _get_params(self, param): - if len(param) != self.n_params: - raise(ValueError, - 'number of parameters must be ' + str(self.n_params)) - - epsilon = param[0] - b = np.exp(param[1]) - a = -epsilon*b - - return a, b - - def f(self, x, params): - a, b = self._get_params(params) - - y = np.sinh(b * np.arcsinh(x) - a) - return y - - def invf(self, y, params): - a, b = self._get_params(params) - - x = np.sinh((np.arcsinh(y)+a)/b) - - return x - - def df(self, x, params): - a, b = self._get_params(params) - - dx = (b *np.cosh(b * np.arcsinh(x) - a))/np.sqrt(1 + x ** 2) - - return dx - -class WarpCompose(WarpBase): - """ Composition of warps. These are passed in as an array and - intialised automatically. For example:: - - W = WarpCompose(('WarpBoxCox', 'WarpAffine')) - - where ell_i are lengthscale parameters and sf2 is the signal variance - """ - - def __init__(self, warpnames=None, debugwarp=False): - - if warpnames is None: - raise ValueError("A list of warp functions is required") - self.debugwarp = debugwarp - self.warps = [] - self.n_params = 0 - for wname in warpnames: - warp = eval(wname + '()') - self.n_params += warp.get_n_params() - self.warps.append(warp) - - def f(self, x, theta): - theta_offset = 0 - - if self.debugwarp: - print('begin composition') - for ci, warp in enumerate(self.warps): - n_params_c = warp.get_n_params() - theta_c = [theta[c] for c in - range(theta_offset, theta_offset + n_params_c)] - theta_offset += n_params_c - - if self.debugwarp: - print('f:', ci, theta_c, warp) - - if ci == 0: - fw = warp.f(x, theta_c) - else: - fw = warp.f(fw, theta_c) - return fw - - def invf(self, x, theta): - n_params = 0 - n_warps = 0 - if self.debugwarp: - print('begin composition') - - for ci, warp in enumerate(self.warps): - n_params += warp.get_n_params() - n_warps += 1 - theta_offset = n_params - for ci, warp in reversed(list(enumerate(self.warps))): - n_params_c = warp.get_n_params() - theta_offset -= n_params_c - theta_c = [theta[c] for c in - range(theta_offset, theta_offset + n_params_c)] - - if self.debugwarp: - print('invf:', theta_c, warp) - - if ci == n_warps-1: - finvw = warp.invf(x, theta_c) - else: - finvw = warp.invf(finvw, theta_c) - - return finvw - - def df(self, x, theta): - theta_offset = 0 - if self.debugwarp: - print('begin composition') - for ci, warp in enumerate(self.warps): - n_params_c = warp.get_n_params() - - theta_c = [theta[c] for c in - range(theta_offset, theta_offset + n_params_c)] - theta_offset += n_params_c - - if self.debugwarp: - print('df:', ci, theta_c, warp) - - if ci == 0: - dfw = warp.df(x, theta_c) - else: - dfw = warp.df(dfw, theta_c) - - return dfw - -# ----------------------- -# Functions for inference -# ----------------------- - -class CustomCV: - """ Custom cross-validation approach. This function does not do much, it - merely provides a wrapper designed to be compatible with - scikit-learn (e.g. sklearn.model_selection...) - - :param train: a list of indices of training splits (each itself a list) - :param test: a list of indices of test splits (each itself a list) - - :returns tr: Indices for training set - :returns te: Indices for test set - - """ - - def __init__(self, train, test, X=None, y=None): - self.train = train - self.test = test - self.n_splits = len(train) - if X is not None: - self.N = X.shape[0] - else: - self.N = None - - def split(self, X, y=None): - if self.N is None: - self.N = X.shape[0] - - for i in range(0, self.n_splits): - tr = self.train[i] - te = self.test[i] - yield tr, te - -def bashwrap(processing_dir, python_path, script_command, job_name, - bash_environment=None): - - """ This function wraps normative modelling into a bash script to run it - on a torque cluster system. - - :param processing_dir: Full path to the processing dir - :param python_path: Full path to the python distribution - :param script_command: python command to execute - :param job_name: Name for the bash script output by this function - :param covfile_path: Full path to covariates - :param respfile_path: Full path to response variables - :param cv_folds: Number of cross validations - :param testcovfile_path: Full path to test covariates - :param testrespfile_path: Full path to tes responses - :param bash_environment: A file containing enviornment specific commands - - :returns: A .sh file containing the commands for normative modelling - - written by Thomas Wolfers - """ - - # change to processing dir - os.chdir(processing_dir) - output_changedir = ['cd ' + processing_dir + '\n'] - - # sets bash environment if necessary - if bash_environment is not None: - bash_environment = [bash_environment] - print("""Your own environment requires in any case: - #!/bin/bash\n export and optionally OMP_NUM_THREADS=1\n""") - else: - bash_lines = '#!/bin/bash\n\n' - bash_cores = 'export OMP_NUM_THREADS=1\n' - bash_environment = [bash_lines + bash_cores] - - command = [python_path + ' ' + script_command + '\n'] - - # writes bash file into processing dir - bash_file_name = os.path.join(processing_dir, job_name + '.sh') - with open(bash_file_name, 'w') as bash_file: - bash_file.writelines(bash_environment + output_changedir + command) - - # changes permissoins for bash.sh file - os.chmod(bash_file_name, 0o700) - - return bash_file_name - -def qsub(job_path, memory, duration, logdir=None): - """This function submits a job.sh scipt to the torque custer using the qsub command. - - Basic usage:: - - qsub_nm(job_path, log_path, memory, duration) - - :param job_path: Full path to the job.sh file. - :param memory: Memory requirements written as string for example 4gb or 500mb. - :param duation: The approximate duration of the job, a string with HH:MM:SS for example 01:01:01. - - :outputs: Submission of the job to the (torque) cluster. - - written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. - """ - if logdir is None: - logdir = os.path.expanduser('~') - - # created qsub command - qsub_call = ['echo ' + job_path + ' | qsub -N ' + job_path + ' -l ' + - 'mem=' + memory + ',walltime=' + duration + - ' -e ' + logdir + ' -o ' + logdir] - - # submits job to cluster - call(qsub_call, shell=True) - -def extreme_value_prob_fit(NPM, perc): - n = NPM.shape[0] - t = NPM.shape[1] - n_perc = int(round(t * perc)) - m = np.zeros(n) - for i in range(n): - temp = np.abs(NPM[i, :]) - temp = np.sort(temp) - temp = temp[t - n_perc:] - temp = temp[0:int(np.floor(0.90*temp.shape[0]))] - m[i] = np.mean(temp) - params = genextreme.fit(m) - return params - -def extreme_value_prob(params, NPM, perc): - n = NPM.shape[0] - t = NPM.shape[1] - n_perc = int(round(t * perc)) - m = np.zeros(n) - for i in range(n): - temp = np.abs(NPM[i, :]) - temp = np.sort(temp) - temp = temp[t - n_perc:] - temp = temp[0:int(np.floor(0.90*temp.shape[0]))] - m[i] = np.mean(temp) - probs = genextreme.cdf(m,*params) - return probs - -def ravel_2D(a): - s = a.shape - return np.reshape(a,[s[0], np.prod(s[1:])]) - -def unravel_2D(a, s): - return np.reshape(a,s) - -def threshold_NPM(NPMs, fdr_thr=0.05, npm_thr=0.1): - """ Compute voxels with significant NPMs. """ - p_values = stats.norm.cdf(-np.abs(NPMs)) - results = np.zeros(NPMs.shape) - masks = np.full(NPMs.shape, False, dtype=bool) - for i in range(p_values.shape[0]): - masks[i,:] = FDR(p_values[i,:], fdr_thr) - results[i,] = NPMs[i,:] * masks[i,:].astype(np.int) - m = np.sum(masks,axis=0)/masks.shape[0] > npm_thr - #m = np.any(masks,axis=0) - return results, masks, m - -def FDR(p_values, alpha): - """ Compute the false discovery rate in all voxels for a subject. """ - dim = np.shape(p_values) - p_values = np.reshape(p_values,[np.prod(dim),]) - sorted_p_values = np.sort(p_values) - sorted_p_values_idx = np.argsort(p_values); - testNum = len(p_values) - thresh = ((np.array(range(testNum)) + 1)/np.float(testNum)) * alpha - h = sorted_p_values <= thresh - unsort = np.argsort(sorted_p_values_idx) - h = h[unsort] - h = np.reshape(h, dim) - return h - - -def calibration_error(Y,m,s,cal_levels): - ce = 0 - for cl in cal_levels: - z = np.abs(norm.ppf((1-cl)/2)) - ub = m + z * s - lb = m - z * s - ce = ce + np.abs(cl - np.sum(np.logical_and(Y>=lb,Y<=ub))/Y.shape[0]) - return ce - - -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 - - """ - - if isinstance(n_samples, int): - n_samples = [n_samples for i in range(n_grps)] - - X_train, Y_train, X_test, Y_test = [], [], [], [] - grp_id_train, grp_id_test = [], [] - coef = [] - for i in range(n_grps): - bias = np.random.randint(-10, high=10) - - if method == 'linear': - X_temp, Y_temp, coef_temp = make_regression(n_samples=n_samples[i]*2, - n_features=n_features, n_targets=1, - noise=10 * np.random.rand(), bias=bias, - n_informative=1, coef=True, - random_state=random_state) - elif method == 'non-linear': - X_temp = np.random.randint(-2,6,[2*n_samples[i], n_features]) \ - + np.random.randn(2*n_samples[i], n_features) - Y_temp = X_temp[:,0] * 20 * np.random.rand() + np.random.randint(10,100) \ - * np.sin(2 * np.random.rand() + 2 * np.pi /5 * X_temp[:,0]) - coef_temp = 0 - elif method == 'combined': - X_temp = np.random.randint(-2,6,[2*n_samples[i], n_features]) \ - + np.random.randn(2*n_samples[i], n_features) - Y_temp = (X_temp[:,0]**3) * np.random.uniform(0, 0.5) \ - + X_temp[:,0] * 20 * np.random.rand() \ - + np.random.randint(10, 100) - 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])) - X_train = np.vstack(X_train) - X_test = np.vstack(X_test) - Y_train = np.concatenate(Y_train) - Y_test = np.concatenate(Y_test) - grp_id_train = np.expand_dims(np.concatenate(grp_id_train), axis=1) - grp_id_test = np.expand_dims(np.concatenate(grp_id_test), axis=1) - - for i in range(n_features): - plt.figure() - for j in range(n_grps): - plt.scatter(X_train[grp_id_train[:,0]==j,i], - Y_train[grp_id_train[:,0]==j,], label='Group ' + str(j)) - plt.xlabel('X' + str(i)) - plt.ylabel('Y') - plt.legend() - - if working_dir is not None: - if not os.path.isdir(working_dir): - os.mkdir(working_dir) - with open(os.path.join(working_dir ,'trbefile.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(grp_id_train),file, protocol=PICKLE_PROTOCOL) - with open(os.path.join(working_dir ,'tsbefile.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(grp_id_test),file, protocol=PICKLE_PROTOCOL) - with open(os.path.join(working_dir ,'X_train.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(X_train),file, protocol=PICKLE_PROTOCOL) - with open(os.path.join(working_dir ,'X_test.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(X_test),file, protocol=PICKLE_PROTOCOL) - with open(os.path.join(working_dir ,'Y_train.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(Y_train),file, protocol=PICKLE_PROTOCOL) - with open(os.path.join(working_dir ,'Y_test.pkl'), 'wb') as file: - pickle.dump(pd.DataFrame(Y_test),file, protocol=PICKLE_PROTOCOL) - - return X_train, Y_train, grp_id_train, X_test, Y_test, grp_id_test, coef - - -def divergence_plot(nm, ylim=None): - - if nm.hbr.configs['n_chains'] > 1 and nm.hbr.model_type != 'nn': - a = pm.summary(nm.hbr.trace).round(2) - plt.figure() - plt.hist(a['r_hat'],10) - plt.title('Gelman-Rubin diagnostic for divergence') - - divergent = nm.hbr.trace['diverging'] - - tracedf = pm.trace_to_dataframe(nm.hbr.trace) - - _, ax = plt.subplots(2, 1, figsize=(15, 4), sharex=True, sharey=True) - ax[0].plot(tracedf.values[divergent == 0].T, color='k', alpha=.05) - ax[0].set_title('No Divergences', fontsize=10) - ax[1].plot(tracedf.values[divergent == 1].T, color='C2', lw=.5, alpha=.5) - ax[1].set_title('Divergences', fontsize=10) - plt.ylim(ylim) - plt.xticks(range(tracedf.shape[1]), list(tracedf.columns)) - plt.xticks(rotation=90, fontsize=7) - plt.tight_layout() - plt.show() - - -def load_freesurfer_measure(measure, data_path, subjects_list): - - """This is a utility function to load different Freesurfer measures in a pandas Dataframe. - - Inputs - - :param measure: a string that defines the type of Freesurfer measure we want to load. \ - The options include: - - * 'NumVert': Number of Vertices in each cortical area based on Destrieux atlas. - * 'SurfArea: Surface area for each cortical area based on Destrieux atlas. - * 'GrayVol': Gary matter volume in each cortical area based on Destrieux atlas. - * 'ThickAvg': Average Cortical thinckness in each cortical area based on Destrieux atlas. - * 'ThickStd': STD of Cortical thinckness in each cortical area based on Destrieux atlas. - * 'MeanCurv': Integrated Rectified Mean Curvature in each cortical area based on Destrieux atlas. - * 'GausCurv': Integrated Rectified Gaussian Curvature in each cortical area based on Destrieux atlas. - * 'FoldInd': Folding Index in each cortical area based on Destrieux atlas. - * 'CurvInd': Intrinsic Curvature Index in each cortical area based on Destrieux atlas. - * 'brain': Brain Segmentation Statistics from aseg.stats file. - * 'subcortical_volumes': Subcortical areas volume. - - :param data_path: a string that specifies the path to the main Freesurfer folder. - :param subjects_list: A Pythin list containing the list of subject names to load the data for. \ - The subject names should match the folder name for each subject's Freesurfer data folder. - - Outputs: - - df: A pandas datafrmae containing the subject names as Index and target Freesurfer measures. - - missing_subs: A Python list of subject names that miss the target Freesurefr measures. - - """ - - df = pd.DataFrame() - missing_subs = [] - - if measure in ['NumVert', 'SurfArea', 'GrayVol', 'ThickAvg', - 'ThickStd', 'MeanCurv', 'GausCurv', 'FoldInd', 'CurvInd']: - l = ['NumVert', 'SurfArea', 'GrayVol', 'ThickAvg', - 'ThickStd', 'MeanCurv', 'GausCurv', 'FoldInd', 'CurvInd'] - col = l.index(measure) + 1 - for i, sub in enumerate(subjects_list): - try: - data = dict() - - a = pd.read_csv(data_path + sub + '/stats/lh.aparc.a2009s.stats', - delimiter='\s+', comment='#', header=None) - temp = dict(zip(a[0], a[col])) - for key in list(temp.keys()): - temp['L_'+key] = temp.pop(key) - data.update(temp) - - a = pd.read_csv(data_path + sub + '/stats/rh.aparc.a2009s.stats', - delimiter='\s+', comment='#', header=None) - temp = dict(zip(a[0], a[col])) - for key in list(temp.keys()): - temp['R_'+key] = temp.pop(key) - data.update(temp) - - df_temp = pd.DataFrame(data,index=[sub]) - df = pd.concat([df, df_temp]) - print('%d / %d: %s is done!' %(i, len(subjects_list), sub)) - except: - missing_subs.append(sub) - print('%d / %d: %s is missing!' %(i, len(subjects_list), sub)) - continue - - elif measure == 'brain': - for i, sub in enumerate(subjects_list): - try: - data = dict() - s = StringIO() - with open(data_path + sub + '/stats/aseg.stats') as f: - for line in f: - if line.startswith('# Measure'): - s.write(line) - s.seek(0) # "rewind" to the beginning of the StringIO object - a = pd.read_csv(s, header=None) # with further parameters? - data_brain = dict(zip(a[1], a[3])) - data.update(data_brain) - df_temp = pd.DataFrame(data,index=[sub]) - df = pd.concat([df, df_temp]) - print('%d / %d: %s is done!' %(i, len(subjects_list), sub)) - except: - missing_subs.append(sub) - print('%d / %d: %s is missing!' %(i, len(subjects_list), sub)) - continue - - elif measure == 'subcortical_volumes': - for i, sub in enumerate(subjects_list): - try: - data = dict() - s = StringIO() - with open(data_path + sub + '/stats/aseg.stats') as f: - for line in f: - if line.startswith('# Measure'): - s.write(line) - s.seek(0) # "rewind" to the beginning of the StringIO object - a = pd.read_csv(s, header=None) # with further parameters? - a = dict(zip(a[1], a[3])) - if ' eTIV' in a.keys(): - tiv = a[' eTIV'] - else: - tiv = a[' ICV'] - a = pd.read_csv(data_path + sub + '/stats/aseg.stats', delimiter='\s+', comment='#', header=None) - data_vol = dict(zip(a[4]+'_mm3', a[3])) - for key in data_vol.keys(): - data_vol[key] = data_vol[key]/tiv - data.update(data_vol) - data = pd.DataFrame(data,index=[sub]) - df = pd.concat([df, data]) - print('%d / %d: %s is done!' %(i, len(subjects_list), sub)) - except: - missing_subs.append(sub) - print('%d / %d: %s is missing!' %(i, len(subjects_list), sub)) - continue - - return df, missing_subs - - -class scaler: - - def __init__(self, scaler_type='standardize', tail=0.01): - - self.scaler_type = scaler_type - self.tail = tail - - if self.scaler_type not in ['standardize', 'minmax', 'robminmax']: - raise ValueError("Undifined scaler type!") - - - def fit(self, X): - - if self.scaler_type == 'standardize': - - self.m = np.mean(X, axis=0) - self.s = np.std(X, axis=0) - - elif self.scaler_type == 'minmax': - self.min = np.min(X, axis=0) - self.max = np.max(X, axis=0) - - elif self.scaler_type == 'robminmax': - self.min = np.zeros([X.shape[1],]) - self.max = np.zeros([X.shape[1],]) - for i in range(X.shape[1]): - self.min[i] = np.median(np.sort(X[:,i])[0:int(np.round(X.shape[0] * self.tail))]) - self.max[i] = np.median(np.sort(X[:,i])[-int(np.round(X.shape[0] * self.tail)):]) - - - def transform(self, X, adjust_outliers=False): - - if self.scaler_type == 'standardize': - - X = (X - self.m) / self.s - - elif self.scaler_type in ['minmax', 'robminmax']: - - X = (X - self.min) / (self.max - self.min) - - if adjust_outliers: - - X[X < 0] = 0 - X[X > 1] = 1 - - return X - - def inverse_transform(self, X, index=None): - - if self.scaler_type == 'standardize': - if index is None: - X = X * self.s + self.m - else: - X = X * self.s[index] + self.m[index] - - elif self.scaler_type in ['minmax', 'robminmax']: - if index is None: - X = X * (self.max - self.min) + self.min - else: - X = X * (self.max[index] - self.min[index]) + self.min[index] - return X - - def fit_transform(self, X, adjust_outliers=False): - - if self.scaler_type == 'standardize': - - self.m = np.mean(X, axis=0) - self.s = np.std(X, axis=0) - X = (X - self.m) / self.s - - elif self.scaler_type == 'minmax': - - self.min = np.min(X, axis=0) - self.max = np.max(X, axis=0) - X = (X - self.min) / (self.max - self.min) - - elif self.scaler_type == 'robminmax': - - self.min = np.zeros([X.shape[1],]) - self.max = np.zeros([X.shape[1],]) - - for i in range(X.shape[1]): - self.min[i] = np.median(np.sort(X[:,i])[0:int(np.round(X.shape[0] * self.tail))]) - self.max[i] = np.median(np.sort(X[:,i])[-int(np.round(X.shape[0] * self.tail)):]) - - X = (X - self.min) / (self.max - self.min) - - if adjust_outliers: - X[X < 0] = 0 - X[X > 1] = 1 - - return X - - - -def retrieve_freesurfer_eulernum(freesurfer_dir, subjects=None, save_path=None): - - """ - This function receives the freesurfer directory (including processed data - for several subjects) and retrieves the Euler number from the log files. If - the log file does not exist, this function uses 'mris_euler_number' to recompute - the Euler numbers (ENs). The function returns the ENs in a dataframe and - the list of missing subjects (that for which computing EN is failed). If - 'save_path' is specified then the results will be saved in a pickle file. - - Basic usage:: - - ENs, missing_subjects = retrieve_freesurfer_eulernum(freesurfer_dir) - - where the arguments are defined below. - - :param freesurfer_dir: absolute path to the Freesurfer directory. - :param subjects: List of subject that we want to retrieve the ENs for. - If it is 'None' (the default), the list of the subjects will be automatically - retreived from existing directories in the 'freesurfer_dir' (i.e. the ENs - for all subjects will be retrieved). - :param save_path: The path to save the results. If 'None' (default) the - results are not saves on the disk. - - - :outputs: * ENs - A dataframe of retrieved ENs. - * missing_subjects - The list of missing subjects. - - Developed by S.M. Kia - - """ - - if subjects is None: - subjects = [temp for temp in os.listdir(freesurfer_dir) - if os.path.isdir(os.path.join(freesurfer_dir ,temp))] - - df = pd.DataFrame(index=subjects, columns=['lh_en','rh_en','avg_en']) - missing_subjects = [] - - for s, sub in enumerate(subjects): - sub_dir = os.path.join(freesurfer_dir, sub) - log_file = os.path.join(sub_dir, 'scripts', 'recon-all.log') - - if os.path.exists(sub_dir): - if os.path.exists(log_file): - with open(log_file) as f: - for line in f: - # find the part that refers to the EC - if re.search('orig.nofix lheno', line): - eno_line = line - f.close() - eno_l = eno_line.split()[3][0:-1] # remove the trailing comma - eno_r = eno_line.split()[6] - euler = (float(eno_l) + float(eno_r)) / 2 - - df.at[sub, 'lh_en'] = eno_l - df.at[sub, 'rh_en'] = eno_r - df.at[sub, 'avg_en'] = euler - - print('%d: Subject %s is successfully processed. EN = %f' - %(s, sub, df.at[sub, 'avg_en'])) - else: - print('%d: Subject %s is missing log file, running QC ...' %(s, sub)) - try: - bashCommand = 'mris_euler_number '+ freesurfer_dir + sub +'/surf/lh.orig.nofix>' + 'temp_l.txt 2>&1' - res = subprocess.run(bashCommand, stdout=subprocess.PIPE, shell=True) - file = open('temp_l.txt', mode = 'r', encoding = 'utf-8-sig') - lines = file.readlines() - file.close() - words = [] - for line in lines: - line = line.strip() - words.append([item.strip() for item in line.split(' ')]) - eno_l = np.float32(words[0][12]) - - bashCommand = 'mris_euler_number '+ freesurfer_dir + sub +'/surf/rh.orig.nofix>' + 'temp_r.txt 2>&1' - res = subprocess.run(bashCommand, stdout=subprocess.PIPE, shell=True) - file = open('temp_r.txt', mode = 'r', encoding = 'utf-8-sig') - lines = file.readlines() - file.close() - words = [] - for line in lines: - line = line.strip() - words.append([item.strip() for item in line.split(' ')]) - eno_r = np.float32(words[0][12]) - - df.at[sub, 'lh_en'] = eno_l - df.at[sub, 'rh_en'] = eno_r - df.at[sub, 'avg_en'] = (eno_r + eno_l) / 2 - - print('%d: Subject %s is successfully processed. EN = %f' - %(s, sub, df.at[sub, 'avg_en'])) - - except: - e = sys.exc_info()[0] - missing_subjects.append(sub) - print('%d: QC is failed for subject %s: %s.' %(s, sub, e)) - - else: - missing_subjects.append(sub) - print('%d: Subject %s is missing.' %(s, sub)) - df = df.dropna() - - if save_path is not None: - with open(save_path, 'wb') as file: - pickle.dump({'ENs':df}, file) - - return df, missing_subjects - -def get_package_versions(): - - import platform - versions = dict() - versions['Python'] = platform.python_version() - - try: - import theano - versions['Theano'] = theano.__version__ - except: - versions['Theano'] = '' - - try: - import pymc3 - versions['PyMC3'] = pymc3.__version__ - except: - versions['PyMC3'] = '' - - try: - import pcntoolkit - versions['PCNtoolkit'] = pcntoolkit.__version__ - except: - versions['PCNtoolkit'] = '' - - return versions - - -def z_to_abnormal_p(Z): - """ - - This function receives a matrix of z-scores (deviations) and transfer them - to corresponding abnormal probabilities. For more information see Sec. 2.5 - in https://www.biorxiv.org/content/10.1101/2021.05.28.446120v1.full.pdf. - - :param Z: n by p matrix of z-scores (deviations in normative modeling) where - n is the number of subjects and p is the number of features. - :type Z: numpy.array - - :return: a matrix of same size as Z, with probability of each sample being - an abnormal sample. - :rtype: numpy.array - - """ - - abn_p = 1- norm.sf(np.abs(Z))*2 - - return abn_p - - -def anomaly_detection_auc(abn_p, labels, n_permutation=None): - """ - This is a utility function for computing region-wise AUC scores for anomaly - detection using normative model. If n_permutations is not None (e.g. 1000), - it also computes permuation p_values for each region. - - :param abn_p: n by p matrix of with probability of each sample being - an abnormal sample. This matrix can be computed using 'z_to_abnormal_p' - function. - :type abn_p: numpy.array - :param labels: a vactor of binary labels for n subjects, 0 for healthy and - 1 for patients. - :type labels: numpy.array - :param n_permutation: If not none the permutation significance test with - n_permutation repetitions is performed for each feature. defaults to None. - :type n_permutation: numpy.int - :return: p by 1 matrix of AUCs and p_values for permutation test for each - feature (i.e. brain region). - :rtype: numpy.array - - """ - - n, p = abn_p.shape - aucs = np.zeros([p]) - p_values = np.zeros([p]) - - for i in range(p): - aucs[i] = roc_auc_score(labels, abn_p[:,i]) - - if n_permutation is not None: - - auc_perm = np.zeros([n_permutation]) - for j in range(n_permutation): - rand_idx = np.random.permutation(len(labels)) - rand_labels = labels[rand_idx] - auc_perm[j] = roc_auc_score(rand_labels, abn_p[:,i]) - - p_values[i] = (np.sum(auc_perm > aucs[i]) + 1) / (n_permutation + 1) - print('Feature %d of %d is done: p_value=%f' %(i,n_permutation,p_values[i])) - - return aucs, p_values - - -def cartesian_product(arrays): - - """ - This is a utility function for creating dummy data (covariates). It computes the cartesian product of N 1D arrays. - - Example: - a = cartesian_product(np.arange(0,5), np.arange(6,10)) - - :param arrays: a list of N input 1D numpy arrays with size d1,d2,dN. - :return: A d1...dN by N matrix of cartesian product of N arrays. - - """ - - la = len(arrays) - dtype = np.result_type(arrays[0]) - arr = np.empty([len(a) for a in arrays] + [la], dtype=dtype) - for i, a in enumerate(np.ix_(arrays)): - arr[...,i] = a - - return arr.reshape(-1, la) - - -def yes_or_no(question): - - """ - Utility function for getting yes/no action from the user. - - :param question: String for user query. - - :return: Boolean of True for 'yes' and False for 'no'. - - - """ - - while "the answer is invalid": - reply = str(input(question+' (y/n): ')).lower().strip() - if reply[:1] == 'y': - return True - if reply[:1] == 'n': - return False - - - -#====== This is stuff used for the SHASH distributions, but using numpy (not pymc or theano) === - -def K(p, x): - return np.array(spp.kv(p, x)) - -def P(q): - """ - The P function as given in Jones et al. - :param q: - :return: - - """ - frac = np.exp(1 / 4) / np.sqrt(8 * np.pi) - K1 = K((q + 1) / 2, 1 / 4) - K2 = K((q - 1) / 2, 1 / 4) - a = (K1 + K2) * frac - return a - -def m(epsilon, delta, r): - """ - The r'th uncentered moment. Given by Jones et al. - """ - frac1 = 1 / np.power(2, r) - acc = 0 - for i in range(r + 1): - combs = spp.comb(r, i) - flip = np.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 - -#====== end stufff for SHASH - -# Design matrix function - -def z_score(y, mean, std, skew=None, kurtosis=None, likelihood = "Normal"): - - """ - Computes Z-score of some data given parameters and a likelihood type string. - if likelihood == "Normal", parameters 'skew' and 'kurtosis' are ignored - :param y: - :param mean: - :param std: - :param skew: - :param kurtosis: - :param likelihood: - :return: - """ - if likelihood == "SHASHo": - SHASH = (y-mean)/std - Z = np.sinh(np.arcsinh(SHASH)*kurtosis - skew) - elif likelihood == "SHASHo2": - std_d = std/kurtosis - SHASH = (y-mean)/std_d - Z = np.sinh(np.arcsinh(SHASH)*kurtosis - skew) - elif likelihood == "SHASHb": - true_mean = m(skew, kurtosis, 1) - true_std = np.sqrt((m(skew, kurtosis, 2) - true_mean ** 2)) - SHASH_c = ((y-mean)/std) - SHASH = SHASH_c * true_std + true_mean - Z = np.sinh(np.arcsinh(SHASH) * kurtosis - skew) - else: - Z = (y-mean)/std - return Z - - -def expand_all(*args): - def expand(a): - if len(a.shape) == 1: - return np.expand_dims(a, axis=1) - else: - return a - return [expand(x) for x in args] diff --git a/dist/pcntoolkit-0.26-py3.8.egg b/dist/pcntoolkit-0.26-py3.8.egg deleted file mode 100644 index 8ab949265a03161e0e9df43f51c7bd221f75cb67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 201504 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
    OL*H?y_|8+JtF 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 diff --git a/pcntoolkit.egg-info/PKG-INFO b/pcntoolkit.egg-info/PKG-INFO deleted file mode 100644 index 4cd5f112..00000000 --- a/pcntoolkit.egg-info/PKG-INFO +++ /dev/null @@ -1,9 +0,0 @@ -Metadata-Version: 2.1 -Name: pcntoolkit -Version: 0.26 -Summary: Predictive Clinical Neuroscience toolkit -Home-page: http://github.com/amarquand/PCNtoolkit -Author: Andre Marquand -Author-email: andre.marquand@donders.ru.nl -License: GNU GPLv3 -License-File: LICENSE diff --git a/pcntoolkit.egg-info/SOURCES.txt b/pcntoolkit.egg-info/SOURCES.txt deleted file mode 100644 index a6e10a60..00000000 --- a/pcntoolkit.egg-info/SOURCES.txt +++ /dev/null @@ -1,37 +0,0 @@ -LICENSE -README.md -setup.py -pcntoolkit/__init__.py -pcntoolkit/configs.py -pcntoolkit/normative.py -pcntoolkit/normative_NP.py -pcntoolkit/normative_parallel.py -pcntoolkit/trendsurf.py -pcntoolkit.egg-info/PKG-INFO -pcntoolkit.egg-info/SOURCES.txt -pcntoolkit.egg-info/dependency_links.txt -pcntoolkit.egg-info/not-zip-safe -pcntoolkit.egg-info/requires.txt -pcntoolkit.egg-info/top_level.txt -pcntoolkit/dataio/__init__.py -pcntoolkit/dataio/fileio.py -pcntoolkit/model/NP.py -pcntoolkit/model/NPR.py -pcntoolkit/model/SHASH.py -pcntoolkit/model/__init__.py -pcntoolkit/model/architecture.py -pcntoolkit/model/bayesreg.py -pcntoolkit/model/gp.py -pcntoolkit/model/hbr.py -pcntoolkit/model/rfa.py -pcntoolkit/normative_model/__init__.py -pcntoolkit/normative_model/norm_base.py -pcntoolkit/normative_model/norm_blr.py -pcntoolkit/normative_model/norm_gpr.py -pcntoolkit/normative_model/norm_hbr.py -pcntoolkit/normative_model/norm_np.py -pcntoolkit/normative_model/norm_rfa.py -pcntoolkit/normative_model/norm_utils.py -pcntoolkit/util/__init__.py -pcntoolkit/util/hbr_utils.py -pcntoolkit/util/utils.py \ No newline at end of file diff --git a/pcntoolkit.egg-info/dependency_links.txt b/pcntoolkit.egg-info/dependency_links.txt deleted file mode 100644 index 8b137891..00000000 --- a/pcntoolkit.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/pcntoolkit.egg-info/not-zip-safe b/pcntoolkit.egg-info/not-zip-safe deleted file mode 100644 index 8b137891..00000000 --- a/pcntoolkit.egg-info/not-zip-safe +++ /dev/null @@ -1 +0,0 @@ - diff --git a/pcntoolkit.egg-info/requires.txt b/pcntoolkit.egg-info/requires.txt deleted file mode 100644 index 2d549589..00000000 --- a/pcntoolkit.egg-info/requires.txt +++ /dev/null @@ -1,14 +0,0 @@ -argparse -nibabel>=2.5.1 -six -sklearn -bspline -matplotlib -numpy<1.23,>=1.19.5 -scipy>=1.3.2 -pandas>=0.25.3 -torch>=1.1.0 -sphinx-tabs -pymc3<=3.9.3,>=3.8 -theano==1.0.5 -arviz==0.11.0 diff --git a/pcntoolkit.egg-info/top_level.txt b/pcntoolkit.egg-info/top_level.txt deleted file mode 100644 index 6f8e8f14..00000000 --- a/pcntoolkit.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -pcntoolkit From 847f9f3aab240d581ecb8777a48c060a89174d74 Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Wed, 15 Feb 2023 09:35:30 +0100 Subject: [PATCH 13/36] more cleaning up --- requirements.txt | 67 ------------------------------------------------ 1 file changed, 67 deletions(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index afc06339..00000000 --- a/requirements.txt +++ /dev/null @@ -1,67 +0,0 @@ -alabaster==0.7.13 -arviz==0.11.0 -Babel==2.11.0 -brotlipy==0.7.0 -bspline==0.1.1 -certifi==2022.12.7 -cffi==1.15.1 -cftime==1.6.2 -charset-normalizer==2.0.4 -contourpy==1.0.7 -cryptography==38.0.1 -cycler==0.11.0 -docutils==0.18.1 -fastprogress==1.0.3 -fonttools==4.38.0 -h5py==3.8.0 -idna==3.4 -imagesize==1.4.1 -Jinja2==3.1.2 -joblib==1.2.0 -kiwisolver==1.4.4 -MarkupSafe==2.1.2 -matplotlib==3.6.3 -netCDF4==1.6.2 -nibabel==5.0.0 -numpy==1.21.5 -packaging==23.0 -pandas==1.5.3 -patsy==0.5.3 -pcntoolkit==0.26 -Pillow==9.4.0 -pip==22.3.1 -pluggy==1.0.0 -pycosat==0.6.4 -pycparser==2.21 -Pygments==2.14.0 -pymc3==3.9.3 -pyOpenSSL==22.0.0 -pyparsing==3.0.9 -PySocks==1.7.1 -python-dateutil==2.8.2 -pytz==2022.7.1 -requests==2.28.1 -ruamel.yaml==0.17.21 -ruamel.yaml.clib==0.2.6 -scikit-learn==1.2.1 -scipy==1.10.0 -setuptools==65.5.0 -six==1.16.0 -snowballstemmer==2.2.0 -Sphinx==6.1.3 -sphinx-tabs==3.4.1 -sphinxcontrib-applehelp==1.0.4 -sphinxcontrib-devhelp==1.0.2 -sphinxcontrib-htmlhelp==2.0.1 -sphinxcontrib-jsmath==1.0.1 -sphinxcontrib-qthelp==1.0.3 -sphinxcontrib-serializinghtml==1.1.5 -Theano==1.0.5 -threadpoolctl==3.1.0 -toolz==0.12.0 -torch==1.13.1 -tqdm==4.64.1 -typing-extensions==3.10.0.2 -urllib3==1.26.13 -wheel==0.37.1 -xarray==2023.1.0 From aaeafc27d6085007c1a87e7abda5546913a4dd61 Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Wed, 15 Feb 2023 11:09:55 +0100 Subject: [PATCH 14/36] bump version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a64d08ef..74ce24e3 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='pcntoolkit', - version='0.26', + version='0.27', description='Predictive Clinical Neuroscience toolkit', url='http://github.com/amarquand/PCNtoolkit', author='Andre Marquand', From 288baa7d2e8c9226bd39c4615e2e45d02f8a5144 Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Fri, 17 Feb 2023 14:39:43 +0100 Subject: [PATCH 15/36] A few corrections for HBR defaults --- pcntoolkit/model/hbr.py | 11 ++++++++--- pcntoolkit/normative_model/norm_hbr.py | 10 +++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pcntoolkit/model/hbr.py b/pcntoolkit/model/hbr.py index 18ed67e2..81a013c2 100644 --- a/pcntoolkit/model/hbr.py +++ b/pcntoolkit/model/hbr.py @@ -173,8 +173,12 @@ def hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): pb = ParamBuilder(model, X, y, batch_effects, trace, configs) if configs['likelihood'] == 'Normal': - mu = pb.make_param("mu", mu_slope_mu_params = (0.,10.), sigma_slope_mu_params = (5.,), mu_intercept_mu_params=(0.,10.), sigma_intercept_mu_params = (5.,)).get_samples(pb) - sigma = pb.make_param("sigma", mu_sigma_params = (10., 5.), sigma_sigma_params = (5.,)).get_samples(pb) + mu = pb.make_param("mu", mu_slope_mu_params = (0.,10.), + sigma_slope_mu_params = (5.,), + mu_intercept_mu_params=(0.,10.), + sigma_intercept_mu_params = (5.,)).get_samples(pb) + sigma = pb.make_param("sigma", mu_sigma_params = (0., 10.), + sigma_sigma_params = (5.,)).get_samples(pb) sigma_plus = pm.math.log(1+pm.math.exp(sigma)) y_like = pm.Normal('y_like',mu=mu, sigma=sigma_plus, observed=y) @@ -530,7 +534,8 @@ def make_param(self, name, dim = (1,), **kwargs): slope_parameterization = self.make_param(f'slope_{name}', dim=[self.feature_num], **kwargs) intercept_parameterization = self.make_param(f'intercept_{name}', **kwargs) return LinearParameterization(name=name, dim=dim, - slope_parameterization=slope_parameterization, intercept_parameterization=intercept_parameterization, + slope_parameterization=slope_parameterization, + intercept_parameterization=intercept_parameterization, pb=self, **kwargs) diff --git a/pcntoolkit/normative_model/norm_hbr.py b/pcntoolkit/normative_model/norm_hbr.py index a23ea073..2f46a9a5 100644 --- a/pcntoolkit/normative_model/norm_hbr.py +++ b/pcntoolkit/normative_model/norm_hbr.py @@ -95,21 +95,25 @@ def __init__(self, **kwargs): if self.configs['linear_sigma']: if 'random_noise' in kwargs.keys(): print("The keyword \'random_noise\' is deprecated. It is now automatically replaced with \'random_intercept_sigma\', because sigma is linear") - self.configs['random_intercept_sigma'] = kwargs.pop('random_noise','False') == 'True' + self.configs['random_intercept_sigma'] = kwargs.pop('random_noise','True') == 'True' elif 'random_noise' in kwargs.keys(): print("The keyword \'random_noise\' is deprecated. It is now automatically replaced with \'random_sigma\', because sigma is fixed") - self.configs['random_sigma'] = kwargs.pop('random_noise','False') == 'True' + self.configs['random_sigma'] = kwargs.pop('random_noise','True') == 'True' if 'random_slope' in kwargs.keys(): print("The keyword \'random_slope\' is deprecated. It is now automatically replaced with \'random_intercept_mu\'") - self.configs['random_slope_mu'] = kwargs.pop('random_slope','False') == 'True' + self.configs['random_slope_mu'] = kwargs.pop('random_slope','True') == 'True' ##### End Deprecations ## Default parameters self.configs['linear_mu'] = kwargs.pop('linear_mu','True') == 'True' + self.configs['random_mu'] = kwargs.pop('random_mu','True') == 'True' self.configs['random_intercept_mu'] = kwargs.pop('random_intercept_mu','True') == 'True' self.configs['random_slope_mu'] = kwargs.pop('random_slope_mu','True') == 'True' self.configs['random_sigma'] = kwargs.pop('random_sigma','True') == 'True' + self.configs['centered_sigma'] = kwargs.pop('centered_sigma','True') == 'True' + self.configs['centered_slope_mu'] = kwargs.pop('centered_slope_mu','True') == 'True' + self.configs['centered_intercept_mu'] = kwargs.pop('centered_intercept_mu','True') == 'True' ## End default parameters self.hbr = HBR(self.configs) From 5fdf2d174ab0c3e6ee1cd6c150c98b386258f075 Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Sat, 18 Feb 2023 20:05:53 +0100 Subject: [PATCH 16/36] Reverting the prior and sampling setting. --- pcntoolkit/model/hbr.py | 2 +- pcntoolkit/normative_model/norm_hbr.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pcntoolkit/model/hbr.py b/pcntoolkit/model/hbr.py index 81a013c2..98c71037 100644 --- a/pcntoolkit/model/hbr.py +++ b/pcntoolkit/model/hbr.py @@ -177,7 +177,7 @@ def hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): sigma_slope_mu_params = (5.,), mu_intercept_mu_params=(0.,10.), sigma_intercept_mu_params = (5.,)).get_samples(pb) - sigma = pb.make_param("sigma", mu_sigma_params = (0., 10.), + sigma = pb.make_param("sigma", mu_sigma_params = (10., 5.), sigma_sigma_params = (5.,)).get_samples(pb) sigma_plus = pm.math.log(1+pm.math.exp(sigma)) y_like = pm.Normal('y_like',mu=mu, sigma=sigma_plus, observed=y) diff --git a/pcntoolkit/normative_model/norm_hbr.py b/pcntoolkit/normative_model/norm_hbr.py index 2f46a9a5..7b73d98a 100644 --- a/pcntoolkit/normative_model/norm_hbr.py +++ b/pcntoolkit/normative_model/norm_hbr.py @@ -112,8 +112,6 @@ def __init__(self, **kwargs): self.configs['random_slope_mu'] = kwargs.pop('random_slope_mu','True') == 'True' self.configs['random_sigma'] = kwargs.pop('random_sigma','True') == 'True' self.configs['centered_sigma'] = kwargs.pop('centered_sigma','True') == 'True' - self.configs['centered_slope_mu'] = kwargs.pop('centered_slope_mu','True') == 'True' - self.configs['centered_intercept_mu'] = kwargs.pop('centered_intercept_mu','True') == 'True' ## End default parameters self.hbr = HBR(self.configs) From cc7e48260d942dfd05c2804a8a9ff93f5df33f2f Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Sun, 19 Feb 2023 14:21:02 +0100 Subject: [PATCH 17/36] norm_hbr documentation is added. --- pcntoolkit/NP_configs.pkl | Bin 0 -> 142 bytes pcntoolkit/__init__.py | 2 + pcntoolkit/model/NPR.py | 22 +- pcntoolkit/model/NPR1.py | 80 ++ pcntoolkit/model/NP_configs.pkl | Bin 0 -> 142 bytes pcntoolkit/model/hbr1.py | 936 +++++++++++++++++++++++ pcntoolkit/normative_model/norm_hbr.py | 97 ++- pcntoolkit/normative_model/norm_hbr1.py | 309 ++++++++ pcntoolkit/normative_model/norm_hbr_b.py | 146 ++++ pcntoolkit/normative_model/norm_np.py | 83 +- pcntoolkit/normative_model/norm_np1.py | 229 ++++++ pcntoolkit/normative_parallel.py | 13 +- pcntoolkit/temp.py | 67 ++ pcntoolkit/util/preprocess.py | 101 +++ 14 files changed, 2009 insertions(+), 76 deletions(-) create mode 100644 pcntoolkit/NP_configs.pkl mode change 100644 => 100755 pcntoolkit/model/NPR.py create mode 100644 pcntoolkit/model/NPR1.py create mode 100644 pcntoolkit/model/NP_configs.pkl create mode 100755 pcntoolkit/model/hbr1.py create mode 100644 pcntoolkit/normative_model/norm_hbr1.py create mode 100644 pcntoolkit/normative_model/norm_hbr_b.py mode change 100644 => 100755 pcntoolkit/normative_model/norm_np.py create mode 100644 pcntoolkit/normative_model/norm_np1.py create mode 100755 pcntoolkit/temp.py create mode 100755 pcntoolkit/util/preprocess.py diff --git a/pcntoolkit/NP_configs.pkl b/pcntoolkit/NP_configs.pkl new file mode 100644 index 0000000000000000000000000000000000000000..336328d9b6c8b9118fe66ae5556037512ab2da37 GIT binary patch literal 142 zcmZo*ncB<%0ku;!dbpAjOOi9K?X~)UwRv)G0lz bCHY0k8B^c_lc)4BCl{1XX`K=@rBn|9!8 1: y_sigma = torch.std(temp, dim=0).to(self.device) - y_sigma_84 = torch.std(temp_84, dim=0).to(self.device) - return y_hat, y_hat_84, z_all, z_context, y_sigma, y_sigma_84 + return y_hat, z_all, z_context, y_sigma ############################################################################### @@ -68,13 +65,8 @@ def kl_div_gaussians(mu_q, logvar_q, mu_p, logvar_p): kl_div = 0.5 * kl_div.sum() return kl_div -def np_loss(y_hat, y_hat_84, y, z_all, z_context): - #PBL = pinball_loss(y, y_hat, 0.05) - BCE = F.binary_cross_entropy(torch.squeeze(y_hat), torch.mean(y,dim=1), reduction="sum") - idx1 = (y >= y_hat_84).squeeze() - idx2 = (y < y_hat_84).squeeze() - BCE84 = 0.84 * F.binary_cross_entropy(torch.squeeze(y_hat_84[idx1,:]), torch.mean(y[idx1,:],dim=1), reduction="sum") + \ - 0.16 * F.binary_cross_entropy(torch.squeeze(y_hat_84[idx2,:]), torch.mean(y[idx2,:],dim=1), reduction="sum") +def np_loss(y_hat, y, z_all, z_context): + BCE = F.mse_loss(y_hat, y, reduction="sum") KLD = kl_div_gaussians(z_all[0], z_all[1], z_context[0], z_context[1]) - return BCE + KLD + BCE84 + return BCE + KLD diff --git a/pcntoolkit/model/NPR1.py b/pcntoolkit/model/NPR1.py new file mode 100644 index 00000000..238e6563 --- /dev/null +++ b/pcntoolkit/model/NPR1.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Nov 22 14:32:37 2019 + +@author: seykia +""" + +import torch +from torch import nn +from torch.nn import functional as F + +##################################### NP Model ################################ + +class NPR(nn.Module): + def __init__(self, encoder, decoder, args): + super(NPR, self).__init__() + self.r_dim = encoder.r_dim + self.z_dim = encoder.z_dim + self.encoder = encoder + self.decoder = decoder + self.r_to_z_mean = nn.Linear(self.r_dim, self.z_dim) + self.r_to_z_logvar = nn.Linear(self.r_dim, self.z_dim) + self.device = args.device + + def xy_to_z_params(self, x, y): + r = self.encoder.forward(x, y) + mu = self.r_to_z_mean(r) + logvar = self.r_to_z_logvar(r) + return mu, logvar + + def reparameterise(self, z): + mu, logvar = z + std = torch.exp(0.5 * logvar) + eps = torch.randn_like(std) + z_sample = eps.mul(std).add_(mu) + return z_sample + + def forward(self, x_context, y_context, x_all=None, y_all=None, n = 10): + y_sigma = None + y_sigma_84 = None + z_context = self.xy_to_z_params(x_context, y_context) + if self.training: + z_all = self.xy_to_z_params(x_all, y_all) + z_sample = self.reparameterise(z_all) + y_hat, y_hat_84 = self.decoder.forward(z_sample) + else: + z_all = z_context + temp = torch.zeros([n,y_context.shape[0], y_context.shape[2]], device = self.device) + temp_84 = torch.zeros([n,y_context.shape[0], y_context.shape[2]], device = self.device) + for i in range(n): + z_sample = self.reparameterise(z_all) + temp[i,:], temp_84[i,:] = self.decoder.forward(z_sample) + y_hat = torch.mean(temp, dim=0).to(self.device) + y_hat_84 = torch.mean(temp_84, dim=0).to(self.device) + if n > 1: + y_sigma = torch.std(temp, dim=0).to(self.device) + y_sigma_84 = torch.std(temp_84, dim=0).to(self.device) + return y_hat, y_hat_84, z_all, z_context, y_sigma, y_sigma_84 + +############################################################################### + +def kl_div_gaussians(mu_q, logvar_q, mu_p, logvar_p): + var_p = torch.exp(logvar_p) + kl_div = (torch.exp(logvar_q) + (mu_q - mu_p) ** 2) / (var_p) \ + - 1.0 \ + + logvar_p - logvar_q + kl_div = 0.5 * kl_div.sum() + return kl_div + +def np_loss(y_hat, y_hat_84, y, z_all, z_context): + #PBL = pinball_loss(y, y_hat, 0.05) + BCE = F.binary_cross_entropy(torch.squeeze(y_hat), torch.mean(y,dim=1), reduction="sum") + #idx1 = (y >= y_hat_84).squeeze() + #idx2 = (y < y_hat_84).squeeze() + #BCE84 = 0.84 * F.binary_cross_entropy(torch.squeeze(y_hat_84[idx1,:]), torch.mean(y[idx1,:],dim=1), reduction="sum") + \ + # 0.16 * F.binary_cross_entropy(torch.squeeze(y_hat_84[idx2,:]), torch.mean(y[idx2,:],dim=1), reduction="sum") + KLD = kl_div_gaussians(z_all[0], z_all[1], z_context[0], z_context[1]) + return BCE + KLD #`+ BCE84 + diff --git a/pcntoolkit/model/NP_configs.pkl b/pcntoolkit/model/NP_configs.pkl new file mode 100644 index 0000000000000000000000000000000000000000..9ee21c470cd92b943b394263d3848c7449a0db4b GIT binary patch literal 142 zcmZo*ncB<%0ku;!dbpAjOOi9 1: + shape = samples.shape[1:] + else: + shape = None + + if (distribution is None): + smin, smax = np.min(samples), np.max(samples) + width = smax - smin + x = np.linspace(smin, smax, 1000) + y = stats.gaussian_kde(np.ravel(samples))(x) + if half: + x = np.concatenate([x, [x[-1] + 0.1 * width]]) + y = np.concatenate([y, [0]]) + else: + x = np.concatenate([[x[0] - 0.1 * width], x, [x[-1] + 0.1 * width]]) + y = np.concatenate([[0], y, [0]]) + if shape is None: + return pm.distributions.Interpolated(param, x, y) + else: + return pm.distributions.Interpolated(param, x, y, shape=shape) + elif (distribution == 'normal'): + temp = stats.norm.fit(samples) + if shape is None: + return pm.Normal(param, mu=temp[0], sigma=freedom * temp[1]) + else: + return pm.Normal(param, mu=temp[0], sigma=freedom * temp[1], shape=shape) + elif (distribution == 'hnormal'): + temp = stats.halfnorm.fit(samples) + if shape is None: + return pm.HalfNormal(param, sigma=freedom * temp[1]) + else: + return pm.HalfNormal(param, sigma=freedom * temp[1], shape=shape) + elif (distribution == 'hcauchy'): + temp = stats.halfcauchy.fit(samples) + if shape is None: + return pm.HalfCauchy(param, freedom * temp[1]) + else: + return pm.HalfCauchy(param, freedom * temp[1], shape=shape) + elif (distribution == 'uniform'): + upper_bound = np.percentile(samples, 95) + lower_bound = np.percentile(samples, 5) + r = np.abs(upper_bound - lower_bound) + if shape is None: + return pm.Uniform(param, lower=lower_bound - freedom * r, + upper=upper_bound + freedom * r) + else: + return pm.Uniform(param, lower=lower_bound - freedom * r, + upper=upper_bound + freedom * r, shape=shape) + elif (distribution == 'huniform'): + upper_bound = np.percentile(samples, 95) + lower_bound = np.percentile(samples, 5) + r = np.abs(upper_bound - lower_bound) + if shape is None: + return pm.Uniform(param, lower=0, upper=upper_bound + freedom * r) + else: + return pm.Uniform(param, lower=0, upper=upper_bound + freedom * r, shape=shape) + + elif (distribution == 'gamma'): + alpha_fit, loc_fit, invbeta_fit = stats.gamma.fit(samples) + if shape is None: + return pm.Gamma(param, alpha=freedom * alpha_fit, beta=freedom / invbeta_fit) + else: + return pm.Gamma(param, alpha=freedom * alpha_fit, beta=freedom / invbeta_fit, shape=shape) + + elif (distribution == 'igamma'): + alpha_fit, loc_fit, beta_fit = stats.gamma.fit(samples) + if shape is None: + return pm.InverseGamma(param, alpha=freedom * alpha_fit, beta=freedom * beta_fit) + else: + return pm.InverseGamma(param, alpha=freedom * alpha_fit, beta=freedom * beta_fit, shape=shape) + + +def hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): + """ + :param X: [N×P] array of clinical covariates + :param y: [N×1] array of neuroimaging measures + :param batch_effects: [N×M] array of batch effects + :param batch_effects_size: [b1, b2,...,bM] List of counts of unique values of batch effects + :param configs: + :param trace: + :param return_shared_variables: If true, returns references to the shared variables. The values of the shared variables can be set manually, allowing running the same model on different data without re-compiling it. + :return: + """ + X = theano.shared(X) + X = theano.tensor.cast(X,'floatX') + y = theano.shared(y) + y = theano.tensor.cast(y,'floatX') + + + with pm.Model() as model: + + # Make a param builder that will make the correct calls + pb = ParamBuilder(model, X, y, batch_effects, trace, configs) + + if configs['likelihood'] == 'Normal': + mu = pb.make_param("mu").get_samples(pb) + sigma = pb.make_param("sigma").get_samples(pb) + sigma_plus = pm.math.log(1+pm.math.exp(sigma)) + y_like = pm.Normal('y_like',mu=mu, sigma=sigma_plus, observed=y) + + elif configs['likelihood'] in ['SHASHb','SHASHo','SHASHo2']: + """ + Comment 1 + The current parameterizations are tuned towards standardized in- and output data. + It is possible to adjust the priors through the XXX_dist and XXX_params kwargs, like here we do with epsilon_params. + Supported distributions are listed in the Prior class. + + Comment 2 + Any mapping that is applied here after sampling should also be applied in util.hbr_utils.forward in order for the functions there to properly work. + For example, the softplus applied to sigma here is also applied in util.hbr_utils.forward + """ + SHASH_map = {'SHASHb':SHASHb,'SHASHo':SHASHo,'SHASHo2':SHASHo2} + + mu = pb.make_param("mu", slope_mu_params = (0.,3.), mu_intercept_mu_params=(0.,1.), sigma_intercept_mu_params = (1.,)).get_samples(pb) + sigma = pb.make_param("sigma", sigma_params = (1.,2.), slope_sigma_params=(0.,1.), intercept_sigma_params = (1., 1.)).get_samples(pb) + sigma_plus = pm.math.log(1+pm.math.exp(sigma)) + epsilon = pb.make_param("epsilon", epsilon_params = (0.,1.), slope_epsilon_params=(0.,1.), intercept_epsilon_params=(0.,1)).get_samples(pb) + delta = pb.make_param("delta", delta_params=(1.5,2.), slope_delta_params=(0.,1), intercept_delta_params=(2., 1)).get_samples(pb) + delta_plus = pm.math.log(1+pm.math.exp(delta)) + 0.3 + y_like = SHASH_map[configs['likelihood']]('y_like', mu=mu, sigma=sigma_plus, epsilon=epsilon, delta=delta_plus, observed = y) + + return model + + + +class HBR: + + """Hierarchical Bayesian Regression for normative modeling + + Basic usage:: + + model = HBR(configs) + trace = model.estimate(X, y, batch_effects) + ys,s2 = model.predict(X, batch_effects) + + where the variables are + + :param configs: a dictionary of model configurations. + :param X: N-by-P input matrix of P features for N subjects + :param y: N-by-1 vector of outputs. + :param batch_effects: N-by-B matrix of B batch ids for N subjects. + + :returns: * ys - predictive mean + * s2 - predictive variance + + Written by S.M. Kia + """ + + def get_step_methods(self, m): + """ + This can be used to assign default step functions. However, the nuts initialization keyword doesnt work together with this... so better not use it. + + STEP_METHODS = ( + NUTS, + HamiltonianMC, + Metropolis, + BinaryMetropolis, + BinaryGibbsMetropolis, + Slice, + CategoricalGibbsMetropolis, + ) + :param m: a PyMC3 model + :return: + """ + samplermap = {'NUTS': NUTS, 'MH': Metropolis, 'Slice': Slice, 'HMC': HamiltonianMC} + fallbacks = [Metropolis] # We are using MH as a fallback method here + if self.configs['sampler'] == 'NUTS': + step_kwargs = {'nuts': {'target_accept': self.configs['target_accept']}} + else: + step_kwargs = None + return pm.sampling.assign_step_methods(m, methods=[samplermap[self.configs['sampler']]] + fallbacks, + step_kwargs=step_kwargs) + + def __init__(self, configs): + self.bsp = None + self.model_type = configs['type'] + self.configs = configs + + def get_modeler(self): + return {'nn': nn_hbr}.get(self.model_type, hbr) + + def transform_X(self, X): + if self.model_type == 'polynomial': + Phi = create_poly_basis(X, self.configs['order']) + elif self.model_type == 'bspline': + if self.bsp is None: + self.bsp = bspline_fit(X, self.configs['order'], self.configs['nknots']) + bspline = bspline_transform(X, self.bsp) + Phi = np.concatenate((X, bspline), axis = 1) + else: + Phi = X + return Phi + + + def find_map(self, X, y, batch_effects,method='L-BFGS-B'): + """ Function to estimate the model """ + X, y, batch_effects = expand_all(X, y, batch_effects) + + self.batch_effects_num = batch_effects.shape[1] + self.batch_effects_size = [] + for i in range(self.batch_effects_num): + self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) + + X = self.transform_X(X) + modeler = self.get_modeler() + with modeler(X, y, batch_effects, self.batch_effects_size, self.configs) as m: + self.MAP = pm.find_MAP(method=method) + return self.MAP + + def estimate(self, X, y, batch_effects): + + """ Function to estimate the model """ + X, y, batch_effects = expand_all(X, y, batch_effects) + + self.batch_effects_num = batch_effects.shape[1] + self.batch_effects_size = [] + for i in range(self.batch_effects_num): + self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) + + X = self.transform_X(X) + modeler = self.get_modeler() + with modeler(X, y, batch_effects, self.batch_effects_size, self.configs) as m: + self.trace = pm.sample(draws=self.configs['n_samples'], + tune=self.configs['n_tuning'], + chains=self.configs['n_chains'], + init=self.configs['init'], n_init=500000, + cores=self.configs['cores']) + return self.trace + + def predict(self, X, batch_effects, pred='single'): + """ Function to make predictions from the model """ + X, batch_effects = expand_all(X, batch_effects) + + samples = self.configs['n_samples'] + y = np.zeros([X.shape[0], 1]) + + if pred == 'single': + X = self.transform_X(X) + modeler = self.get_modeler() + with modeler(X, y, batch_effects, self.batch_effects_size, self.configs): + ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) + pred_mean = ppc['y_like'].mean(axis=0) + pred_var = ppc['y_like'].var(axis=0) + + return pred_mean, pred_var + + def estimate_on_new_site(self, X, y, batch_effects): + """ Function to adapt the model """ + X, y, batch_effects = expand_all(X, y, batch_effects) + + self.batch_effects_num = batch_effects.shape[1] + self.batch_effects_size = [] + for i in range(self.batch_effects_num): + self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) + + X = self.transform_X(X) + modeler = self.get_modeler() + with modeler(X, y, batch_effects, self.batch_effects_size, + self.configs, trace=self.trace) as m: + self.trace = pm.sample(self.configs['n_samples'], + tune=self.configs['n_tuning'], + chains=self.configs['n_chains'], + target_accept=self.configs['target_accept'], + init=self.configs['init'], n_init=50000, + cores=self.configs['cores']) + return self.trace + + def predict_on_new_site(self, X, batch_effects): + """ Function to make predictions from the model """ + X, batch_effects = expand_all(X, batch_effects) + + samples = self.configs['n_samples'] + y = np.zeros([X.shape[0], 1]) + + X = self.transform_X(X) + modeler = self.get_modeler() + with modeler(X, y, batch_effects, self.batch_effects_size, self.configs, trace=self.trace): + ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) + pred_mean = ppc['y_like'].mean(axis=0) + pred_var = ppc['y_like'].var(axis=0) + + return pred_mean, pred_var + + def generate(self, X, batch_effects, samples): + """ Function to generate samples from posterior predictive distribution """ + X, batch_effects = expand_all(X, batch_effects) + + y = np.zeros([X.shape[0], 1]) + + X = self.transform_X(X) + modeler = self.get_modeler() + with modeler(X, y, batch_effects, self.batch_effects_size, self.configs): + ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) + + generated_samples = np.reshape(ppc['y_like'].squeeze().T, [X.shape[0] * samples, 1]) + X = np.repeat(X, samples) + if len(X.shape) == 1: + X = np.expand_dims(X, axis=1) + batch_effects = np.repeat(batch_effects, samples, axis=0) + if len(batch_effects.shape) == 1: + batch_effects = np.expand_dims(batch_effects, axis=1) + + return X, batch_effects, generated_samples + + def sample_prior_predictive(self, X, batch_effects, samples, trace=None): + """ Function to sample from prior predictive distribution """ + + if len(X.shape) == 1: + X = np.expand_dims(X, axis=1) + if len(batch_effects.shape) == 1: + batch_effects = np.expand_dims(batch_effects, axis=1) + + self.batch_effects_num = batch_effects.shape[1] + self.batch_effects_size = [] + for i in range(self.batch_effects_num): + self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) + + y = np.zeros([X.shape[0], 1]) + + if self.model_type == 'linear': + with hbr(X, y, batch_effects, self.batch_effects_size, self.configs, + trace): + ppc = pm.sample_prior_predictive(samples=samples) + return ppc + + def get_model(self, X, y, batch_effects): + X, y, batch_effects = expand_all(X, y, batch_effects) + + self.batch_effects_num = batch_effects.shape[1] + self.batch_effects_size = [] + for i in range(self.batch_effects_num): + self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) + modeler = self.get_modeler() + X = self.transform_X(X) + return modeler(X, y, batch_effects, self.batch_effects_size, self.configs, self.trace) + + def create_dummy_inputs(self, covariate_ranges = [[0.1,0.9,0.01]]): + + arrays = [] + for i in range(len(covariate_ranges)): + arrays.append(np.arange(covariate_ranges[i][0],covariate_ranges[i][1], covariate_ranges[i][2])) + X = cartesian_product(arrays) + X_dummy = np.concatenate([X for i in range(np.prod(self.batch_effects_size))]) + + arrays = [] + for i in range(self.batch_effects_num): + arrays.append(np.arange(0, self.batch_effects_size[i])) + batch_effects = cartesian_product(arrays) + batch_effects_dummy = np.repeat(batch_effects, X.shape[0], axis=0) + + return X_dummy, batch_effects_dummy + +class Prior: + """ + A wrapper class for a PyMC3 distribution. + - creates a fitted distribution from the trace, if one is present + - overloads the __getitem__ function with something that switches between indexing or not, based on the shape + """ + def __init__(self, name, dist, params, pb, shape=(1,)) -> None: + self.dist = None + self.name = name + self.shape = shape + self.has_random_effect = True if len(shape)>1 else False + self.distmap = {'normal': pm.Normal, + 'hnormal': pm.HalfNormal, + 'gamma': pm.Gamma, + 'uniform': pm.Uniform, + 'igamma': pm.InverseGamma, + 'hcauchy': pm.HalfCauchy} + self.make_dist(dist, params, pb) + + def make_dist(self, dist, params, pb): + """This creates a pymc3 distribution. If there is a trace, the distribution is fitted to the trace. If there isn't a trace, the prior is parameterized by the values in (params)""" + with pb.model as m: + if (pb.trace is not None) and (not self.has_random_effect): + int_dist = from_posterior(param=self.name, + samples=pb.trace[self.name], + distribution=dist, + freedom=pb.configs['freedom']) + self.dist = int_dist.reshape(self.shape) + else: + shape_prod = np.product(np.array(self.shape)) + print(self.name) + print(f"dist={dist}") + print(f"params={params}") + int_dist = self.distmap[dist](self.name, *params, shape=shape_prod) + self.dist = int_dist.reshape(self.shape) + + def __getitem__(self, idx): + """The idx here is the index of the batch-effect. If the prior does not model batch effects, this should return the same value for each index""" + assert self.dist is not None, "Distribution not initialized" + if self.has_random_effect: + return self.dist[idx] + else: + return self.dist + + +class ParamBuilder: + """ + A class that simplifies the construction of parameterizations. + It has a lot of attributes necessary for creating the model, including the data, but it is never saved with the model. + It also contains a lot of decision logic for creating the parameterizations. + """ + + def __init__(self, model, X, y, batch_effects, trace, configs): + """ + + :param model: model to attach all the distributions to + :param X: Covariates + :param y: IDPs + :param batch_effects: I guess this speaks for itself + :param trace: idem + :param configs: idem + """ + self.model = model + self.X = X + self.y = y + self.batch_effects = batch_effects + self.trace = trace + self.configs = configs + + self.feature_num = X.shape[1].eval().item() + self.y_shape = y.shape.eval() + self.n_ys = y.shape[0].eval().item() + self.batch_effects_num = batch_effects.shape[1] + + self.batch_effects_size = [] + self.all_idx = [] + for i in range(self.batch_effects_num): + # Count the unique values for each batch effect + self.batch_effects_size.append(len(np.unique(self.batch_effects[:, i]))) + # Store the unique values for each batch effect + self.all_idx.append(np.int16(np.unique(self.batch_effects[:, i]))) + # Make a cartesian product of all the unique values of each batch effect + self.be_idx = list(product(*self.all_idx)) + + # Make tuples of batch effects ID's and indices of datapoints with that specific combination of batch effects + self.be_idx_tups = [] + for be in self.be_idx: + a = [] + for i, b in enumerate(be): + a.append(self.batch_effects[:, i] == b) + idx = reduce(np.logical_and, a).nonzero() + if idx[0].shape[0] != 0: + self.be_idx_tups.append((be, idx)) + + def make_param(self, name, dim = (1,), **kwargs): + + if self.configs.get(f'random_{name}'): + if self.configs.get(f'random_{name}_slope') or self.configs.get(f'random_{name}_intercept'): # Then it is a linear model + # First make a slope and intercept, and use those to make a linear parameterization + if self.configs.get(f'random_{name}_slope'): + if self.configs.get('centered'): + slope_parameterization = CentralRandomFixedParameterization(name=f'random_{name}_slope', + pb=self, dim=[self.feature_num], **kwargs) + else: + slope_parameterization = NonCentralRandomFixedParameterization(name=f'random_{name}_slope', + pb=self, dim=[self.feature_num], **kwargs) + if self.configs.get(f'random_{name}_intercept'): + if self.configs.get('centered'): + intercept_parameterization = CentralRandomFixedParameterization(name=f'random_{name}_intercept', + pb=self, dim=(1,), **kwargs) + else: + intercept_parameterization = NonCentralRandomFixedParameterization(name=f'random_{name}_intercept', + pb=self, dim=(1,), **kwargs) + return LinearParameterization(name=name, dim=dim, + slope_parameterization=slope_parameterization, + intercept_parameterization=intercept_parameterization, + pb=self, **kwargs) + else: # it is a fixed parametrization (this is used usually for sigma and maybe better to use wider dist as default.) + if self.configs.get('centered'): + return CentralRandomFixedParameterization(name=name, pb=self, dim=dim, **kwargs) + else: + return NonCentralRandomFixedParameterization(name=name, pb=self, dim=dim, **kwargs) + else: + return FixedParameterization(name=name, dim=dim, pb=self,**kwargs) + + +class Parameterization: + """ + This is the top-level parameterization class from which all the other parameterizations inherit. + """ + def __init__(self, name, dim): + self.name = name + self.dim = dim + print(name, type(self)) + + def get_samples(self, pb): + + with pb.model: + samples = theano.tensor.zeros([pb.n_ys, *self.dim]) + for be, idx in pb.be_idx_tups: + samples = theano.tensor.set_subtensor(samples[idx], self.dist[be]) + return samples + + +class FixedParameterization(Parameterization): + """ + A parameterization that takes a single value for all input. It does not depend on anything except its hyperparameters + """ + def __init__(self, name, dim, pb:ParamBuilder, **kwargs): + super().__init__(name, dim) + dist = kwargs.get(f'{name}_dist','normal') + params = kwargs.get(f'{name}_params',(0.,1.)) # should be wider I think. + self.dist = Prior(name, dist, params, pb, shape = dim) + + +class CentralRandomFixedParameterization(Parameterization): + """ + A parameterization that is fixed for each batch effect. This is sampled in a central fashion; + the values are sampled from normal distribution with a group mean and group variance + """ + def __init__(self, name, dim, pb:ParamBuilder, **kwargs): + super().__init__(name, dim) + + # Normal distribution is default for mean + mu_dist = kwargs.get(f'mu_{name}_dist','normal') + mu_params = kwargs.get(f'mu_{name}_params',(0.,1.)) + mu_prior = Prior(f'mu_{name}', mu_dist, mu_params, pb, shape = dim) + + # HalfCauchy is default for sigma + sigma_dist = kwargs.get(f'sigma_{name}_dist','hcauchy') + sigma_params = kwargs.get(f'sigma_{name}_params',(1.,)) + sigma_prior = Prior(f'sigma_{name}',sigma_dist, sigma_params, pb, shape = [*pb.batch_effects_size, *dim]) + + self.dist = pm.Normal(name=name, mu=mu_prior.dist, sigma=sigma_prior.dist, shape = [*pb.batch_effects_size, *dim]) + + +class NonCentralRandomFixedParameterization(Parameterization): + """ + A parameterization that is fixed for each batch effect. This is sampled in a non-central fashion; + the values are a sum of a group mean and noise values scaled with a group scaling factor + """ + def __init__(self, name,dim, pb:ParamBuilder, **kwargs): + super().__init__(name, dim) + + # Normal distribution is default for mean + mu_dist = kwargs.get(f'mu_{name}_dist','normal') + mu_params = kwargs.get(f'mu_{name}_params',(0.,1.)) + mu_prior = Prior(f'mu_{name}', mu_dist, mu_params, pb, shape = dim) + + # HalfCauchy is default for sigma + sigma_dist = kwargs.get(f'sigma_{name}_dist','hcauchy') + sigma_params = kwargs.get(f'sigma_{name}_params',(1.,)) + sigma_prior = Prior(f'sigma_{name}',sigma_dist, sigma_params, pb, shape = dim) + + # Normal is default for offset + offset_dist = kwargs.get(f'offset_{name}_dist','normal') + offset_params = kwargs.get(f'offset_{name}_params',(0.,1.)) + offset_prior = Prior(f'offset_{name}',offset_dist, offset_params, pb, shape = [*pb.batch_effects_size, *dim]) + + self.dist = pm.Deterministic(name=name, var=mu_prior.dist+sigma_prior.dist*offset_prior.dist) + + +class LinearParameterization(Parameterization): + """ + A parameterization that can model a linear dependence on X. + """ + def __init__(self, name, dim, slope_parameterization, intercept_parameterization, pb, **kwargs): + super().__init__( name, dim) + self.slope_parameterization = slope_parameterization + self.intercept_parameterization = intercept_parameterization + + def get_samples(self, pb:ParamBuilder): + with pb.model: + samples = theano.tensor.zeros([pb.n_ys, *self.dim]) + for be, idx in pb.be_idx_tups: + dot = theano.tensor.dot(pb.X[idx,:], self.slope_parameterization.dist[be]).T + intercept = self.intercept_parameterization.dist[be] + samples = theano.tensor.set_subtensor(samples[idx,:],dot+intercept) + return samples + + +def get_design_matrix(X, nm, basis="linear"): + if basis == "bspline": + Phi = bspline_transform(X, nm.hbr.bsp) + elif basis == "polynomial": + Phi = create_poly_basis(X, 3) + else: + Phi = X + return Phi + + + +def nn_hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): + n_hidden = configs['nn_hidden_neuron_num'] + n_layers = configs['nn_hidden_layers_num'] + feature_num = X.shape[1] + batch_effects_num = batch_effects.shape[1] + all_idx = [] + for i in range(batch_effects_num): + all_idx.append(np.int16(np.unique(batch_effects[:, i]))) + be_idx = list(product(*all_idx)) + + # Initialize random weights between each layer for the mu: + init_1 = pm.floatX(np.random.randn(feature_num, n_hidden) * np.sqrt(1 / feature_num)) + init_out = pm.floatX(np.random.randn(n_hidden) * np.sqrt(1 / n_hidden)) + + std_init_1 = pm.floatX(np.random.rand(feature_num, n_hidden)) + std_init_out = pm.floatX(np.random.rand(n_hidden)) + + # And initialize random weights between each layer for sigma_noise: + init_1_noise = pm.floatX(np.random.randn(feature_num, n_hidden) * np.sqrt(1 / feature_num)) + init_out_noise = pm.floatX(np.random.randn(n_hidden) * np.sqrt(1 / n_hidden)) + + std_init_1_noise = pm.floatX(np.random.rand(feature_num, n_hidden)) + std_init_out_noise = pm.floatX(np.random.rand(n_hidden)) + + # If there are two hidden layers, then initialize weights for the second layer: + if n_layers == 2: + init_2 = pm.floatX(np.random.randn(n_hidden, n_hidden) * np.sqrt(1 / n_hidden)) + std_init_2 = pm.floatX(np.random.rand(n_hidden, n_hidden)) + init_2_noise = pm.floatX(np.random.randn(n_hidden, n_hidden) * np.sqrt(1 / n_hidden)) + std_init_2_noise = pm.floatX(np.random.rand(n_hidden, n_hidden)) + + with pm.Model() as model: + + X = pm.Data('X', X) + y = pm.Data('y', y) + + if trace is not None: # Used when estimating/predicting on a new site + weights_in_1_grp = from_posterior('w_in_1_grp', trace['w_in_1_grp'], + distribution='normal', freedom=configs['freedom']) + + weights_in_1_grp_sd = from_posterior('w_in_1_grp_sd', trace['w_in_1_grp_sd'], + distribution='hcauchy', freedom=configs['freedom']) + + if n_layers == 2: + weights_1_2_grp = from_posterior('w_1_2_grp', trace['w_1_2_grp'], + distribution='normal', freedom=configs['freedom']) + + weights_1_2_grp_sd = from_posterior('w_1_2_grp_sd', trace['w_1_2_grp_sd'], + distribution='hcauchy', freedom=configs['freedom']) + + weights_2_out_grp = from_posterior('w_2_out_grp', trace['w_2_out_grp'], + distribution='normal', freedom=configs['freedom']) + + weights_2_out_grp_sd = from_posterior('w_2_out_grp_sd', trace['w_2_out_grp_sd'], + distribution='hcauchy', freedom=configs['freedom']) + + mu_prior_intercept = from_posterior('mu_prior_intercept', trace['mu_prior_intercept'], + distribution='normal', freedom=configs['freedom']) + sigma_prior_intercept = from_posterior('sigma_prior_intercept', trace['sigma_prior_intercept'], + distribution='hcauchy', freedom=configs['freedom']) + + else: + # Group the mean distribution for input to the hidden layer: + weights_in_1_grp = pm.Normal('w_in_1_grp', 0, sd=1, + shape=(feature_num, n_hidden), testval=init_1) + + # Group standard deviation: + weights_in_1_grp_sd = pm.HalfCauchy('w_in_1_grp_sd', 1., + shape=(feature_num, n_hidden), testval=std_init_1) + + if n_layers == 2: + # Group the mean distribution for hidden layer 1 to hidden layer 2: + weights_1_2_grp = pm.Normal('w_1_2_grp', 0, sd=1, + shape=(n_hidden, n_hidden), testval=init_2) + + # Group standard deviation: + weights_1_2_grp_sd = pm.HalfCauchy('w_1_2_grp_sd', 1., + shape=(n_hidden, n_hidden), testval=std_init_2) + + # Group the mean distribution for hidden to output: + weights_2_out_grp = pm.Normal('w_2_out_grp', 0, sd=1, + shape=(n_hidden,), testval=init_out) + + # Group standard deviation: + weights_2_out_grp_sd = pm.HalfCauchy('w_2_out_grp_sd', 1., + shape=(n_hidden,), testval=std_init_out) + + # mu_prior_intercept = pm.Uniform('mu_prior_intercept', lower=-100, upper=100) + mu_prior_intercept = pm.Normal('mu_prior_intercept', mu=0., sigma=1e3) + sigma_prior_intercept = pm.HalfCauchy('sigma_prior_intercept', 5) + + # Now create separate weights for each group, by doing + # weights * group_sd + group_mean, we make sure the new weights are + # coming from the (group_mean, group_sd) distribution. + weights_in_1_raw = pm.Normal('w_in_1', 0, sd=1, + shape=(batch_effects_size + [feature_num, n_hidden])) + weights_in_1 = weights_in_1_raw * weights_in_1_grp_sd + weights_in_1_grp + + if n_layers == 2: + weights_1_2_raw = pm.Normal('w_1_2', 0, sd=1, + shape=(batch_effects_size + [n_hidden, n_hidden])) + weights_1_2 = weights_1_2_raw * weights_1_2_grp_sd + weights_1_2_grp + + weights_2_out_raw = pm.Normal('w_2_out', 0, sd=1, + shape=(batch_effects_size + [n_hidden])) + weights_2_out = weights_2_out_raw * weights_2_out_grp_sd + weights_2_out_grp + + intercepts_offset = pm.Normal('intercepts_offset', mu=0, sd=1, + shape=(batch_effects_size)) + + intercepts = pm.Deterministic('intercepts', intercepts_offset + + mu_prior_intercept * sigma_prior_intercept) + + # Build the neural network and estimate y_hat: + y_hat = theano.tensor.zeros(y.shape) + for be in be_idx: + # Find the indices corresponding to 'group be': + a = [] + for i, b in enumerate(be): + a.append(batch_effects[:, i] == b) + idx = reduce(np.logical_and, a).nonzero() + if idx[0].shape[0] != 0: + act_1 = pm.math.tanh(theano.tensor.dot(X[idx, :], weights_in_1[be])) + if n_layers == 2: + act_2 = pm.math.tanh(theano.tensor.dot(act_1, weights_1_2[be])) + y_hat = theano.tensor.set_subtensor(y_hat[idx, 0], + intercepts[be] + theano.tensor.dot(act_2, weights_2_out[be])) + else: + y_hat = theano.tensor.set_subtensor(y_hat[idx, 0], + intercepts[be] + theano.tensor.dot(act_1, weights_2_out[be])) + + # If we want to estimate varying noise terms across groups: + if configs['random_noise']: + if configs['hetero_noise']: + if trace is not None: # # Used when estimating/predicting on a new site + weights_in_1_grp_noise = from_posterior('w_in_1_grp_noise', + trace['w_in_1_grp_noise'], + distribution='normal', freedom=configs['freedom']) + + weights_in_1_grp_sd_noise = from_posterior('w_in_1_grp_sd_noise', + trace['w_in_1_grp_sd_noise'], + distribution='hcauchy', freedom=configs['freedom']) + + if n_layers == 2: + weights_1_2_grp_noise = from_posterior('w_1_2_grp_noise', + trace['w_1_2_grp_noise'], + distribution='normal', freedom=configs['freedom']) + + weights_1_2_grp_sd_noise = from_posterior('w_1_2_grp_sd_noise', + trace['w_1_2_grp_sd_noise'], + distribution='hcauchy', freedom=configs['freedom']) + + weights_2_out_grp_noise = from_posterior('w_2_out_grp_noise', + trace['w_2_out_grp_noise'], + distribution='normal', freedom=configs['freedom']) + + weights_2_out_grp_sd_noise = from_posterior('w_2_out_grp_sd_noise', + trace['w_2_out_grp_sd_noise'], + distribution='hcauchy', freedom=configs['freedom']) + + else: + # The input layer to the first hidden layer: + weights_in_1_grp_noise = pm.Normal('w_in_1_grp_noise', 0, sd=1, + shape=(feature_num, n_hidden), + testval=init_1_noise) + weights_in_1_grp_sd_noise = pm.HalfCauchy('w_in_1_grp_sd_noise', 1, + shape=(feature_num, n_hidden), + testval=std_init_1_noise) + + # The first hidden layer to second hidden layer: + if n_layers == 2: + weights_1_2_grp_noise = pm.Normal('w_1_2_grp_noise', 0, sd=1, + shape=(n_hidden, n_hidden), + testval=init_2_noise) + weights_1_2_grp_sd_noise = pm.HalfCauchy('w_1_2_grp_sd_noise', 1, + shape=(n_hidden, n_hidden), + testval=std_init_2_noise) + + # The second hidden layer to output layer: + weights_2_out_grp_noise = pm.Normal('w_2_out_grp_noise', 0, sd=1, + shape=(n_hidden,), + testval=init_out_noise) + weights_2_out_grp_sd_noise = pm.HalfCauchy('w_2_out_grp_sd_noise', 1, + shape=(n_hidden,), + testval=std_init_out_noise) + + # mu_prior_intercept_noise = pm.HalfNormal('mu_prior_intercept_noise', sigma=1e3) + # sigma_prior_intercept_noise = pm.HalfCauchy('sigma_prior_intercept_noise', 5) + + # Now create separate weights for each group: + weights_in_1_raw_noise = pm.Normal('w_in_1_noise', 0, sd=1, + shape=(batch_effects_size + [feature_num, n_hidden])) + weights_in_1_noise = weights_in_1_raw_noise * weights_in_1_grp_sd_noise + weights_in_1_grp_noise + + if n_layers == 2: + weights_1_2_raw_noise = pm.Normal('w_1_2_noise', 0, sd=1, + shape=(batch_effects_size + [n_hidden, n_hidden])) + weights_1_2_noise = weights_1_2_raw_noise * weights_1_2_grp_sd_noise + weights_1_2_grp_noise + + weights_2_out_raw_noise = pm.Normal('w_2_out_noise', 0, sd=1, + shape=(batch_effects_size + [n_hidden])) + weights_2_out_noise = weights_2_out_raw_noise * weights_2_out_grp_sd_noise + weights_2_out_grp_noise + + # intercepts_offset_noise = pm.Normal('intercepts_offset_noise', mu=0, sd=1, + # shape=(batch_effects_size)) + + # intercepts_noise = pm.Deterministic('intercepts_noise', mu_prior_intercept_noise + + # intercepts_offset_noise * sigma_prior_intercept_noise) + + # Build the neural network and estimate the sigma_y: + sigma_y = theano.tensor.zeros(y.shape) + for be in be_idx: + a = [] + for i, b in enumerate(be): + a.append(batch_effects[:, i] == b) + idx = reduce(np.logical_and, a).nonzero() + if idx[0].shape[0] != 0: + act_1_noise = pm.math.sigmoid(theano.tensor.dot(X[idx, :], weights_in_1_noise[be])) + if n_layers == 2: + act_2_noise = pm.math.sigmoid(theano.tensor.dot(act_1_noise, weights_1_2_noise[be])) + temp = pm.math.log1pexp(theano.tensor.dot(act_2_noise, weights_2_out_noise[be])) + 1e-5 + else: + temp = pm.math.log1pexp(theano.tensor.dot(act_1_noise, weights_2_out_noise[be])) + 1e-5 + sigma_y = theano.tensor.set_subtensor(sigma_y[idx, 0], temp) + + else: # homoscedastic noise: + if trace is not None: # Used for transferring the priors + upper_bound = np.percentile(trace['sigma_noise'], 95) + sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=2 * upper_bound, shape=(batch_effects_size)) + else: + sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=100, shape=(batch_effects_size)) + + sigma_y = theano.tensor.zeros(y.shape) + for be in be_idx: + a = [] + for i, b in enumerate(be): + a.append(batch_effects[:, i] == b) + idx = reduce(np.logical_and, a).nonzero() + if idx[0].shape[0] != 0: + sigma_y = theano.tensor.set_subtensor(sigma_y[idx, 0], sigma_noise[be]) + + else: # do not allow for random noise terms across groups: + if trace is not None: # Used for transferring the priors + upper_bound = np.percentile(trace['sigma_noise'], 95) + sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=2 * upper_bound) + else: + sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=100) + sigma_y = theano.tensor.zeros(y.shape) + for be in be_idx: + a = [] + for i, b in enumerate(be): + a.append(batch_effects[:, i] == b) + idx = reduce(np.logical_and, a).nonzero() + if idx[0].shape[0] != 0: + sigma_y = theano.tensor.set_subtensor(sigma_y[idx, 0], sigma_noise) + + if configs['skewed_likelihood']: + skewness = pm.Uniform('skewness', lower=-10, upper=10, shape=(batch_effects_size)) + alpha = theano.tensor.zeros(y.shape) + for be in be_idx: + a = [] + for i, b in enumerate(be): + a.append(batch_effects[:, i] == b) + idx = reduce(np.logical_and, a).nonzero() + if idx[0].shape[0] != 0: + alpha = theano.tensor.set_subtensor(alpha[idx, 0], skewness[be]) + else: + alpha = 0 # symmetrical normal distribution + + y_like = pm.SkewNormal('y_like', mu=y_hat, sigma=sigma_y, alpha=alpha, observed=y) + + return model diff --git a/pcntoolkit/normative_model/norm_hbr.py b/pcntoolkit/normative_model/norm_hbr.py index 7b73d98a..d769163e 100644 --- a/pcntoolkit/normative_model/norm_hbr.py +++ b/pcntoolkit/normative_model/norm_hbr.py @@ -34,28 +34,115 @@ class NormHBR(NormBase): - """ Classical GPR-based normative modelling approach + + """ HBR multi-batch normative modelling class. By default, this function + estimates a linear model with random intercept, random slope, and random + homoscedastic noise. + + :param X: [N×P] array of clinical covariates + :param y: [N×1] array of neuroimaging measures + :param trbefile: the address to the batch effects file for the training set. + the batch effect array should be a [N×M] array where M is the number of + the type of batch effects. For example when the site and gender is modeled + as batch effects M=2. Each column in the batch effect array contains the + batch ID (starting from 0) for each sample. If not specified (default=None) + then all samples assumed to be from the same batch (i.e., the batch effect + is not modelled). + :param tsbefile: Similar to trbefile for the test set. + :param model_type: Specifies the type of the model from 'linear', 'plynomial', + and 'bspline' (defauls is 'linear'). + :param likelihood: specifies the type of likelihood among 'Normal' 'SHASHb','SHASHo', + and 'SHASHo2' (defauls is normal). + :param linear_mu: Boolean (default='True') to decide whether the mean (mu) is + parametrized on a linear function (thus changes with covariates) or it is fixed. + :param linear_sigma: Boolean (default='False') to decide whether the variance (sigma) is + parametrized on a linear function (heteroscedastic noise) or it is fixed for + each batch (homoscedastic noise). + :param linear_epsilon: Boolean (default='False') to decide the parametrization + of epsilon for the SHASH likelihood that controls its skewness. + If True, epsilon is parametrized on a linear function + (thus changes with covariates) otherwise it is fixed for each batch. + :param linear_delta: Boolean (default='False') to decide the parametrization + of delta for the SHASH likelihood that controls its kurtosis. + If True, delta is parametrized on a linear function + (thus changes with covariates) otherwise it is fixed for each batch. + :param random_intercept_{parameter}: if parameters mu (default='True'), + sigma (default='False'), epsilon (default='False'), and delta (default='False') + are parametrized on a linear function, then this boolean decides + whether the intercept can vary across batches. + :param random_slope_{parameter}: if parameters mu (default='True'), + sigma (default='False'), epsilon (default='False'), and delta (default='False') + are parametrized on a linear function, then this boolean decides + whether the slope can vary across batches. + :param centered_intercept_{parameter}: if parameters mu (default='False'), + sigma (default='False'), epsilon (default='False'), and delta (default='False') + are parametrized on a linear function, then this boolean decides + whether the parameters of intercept are estimated in a centered or + non-centered manner (default). While centered estimation runs faster + it may cause some problems for the sampler (the funnel of hell). + :param centered_slope_{parameter}: if parameters mu (default='False'), + sigma (default='False'), epsilon (default='False'), and delta (default='False') + are parametrized on a linear function, then this boolean decides + whether the parameters of slope are estimated in a centered or + non-centered manner (default). While centered estimation runs faster + it may cause some problems for the sampler (the funnel of hell). + :param sampler: specifies the type of PyMC sampler (Defauls is 'NUTS'). + :param n_samples: The number of samples to draw (Default is '1000'). Please + note that this parameter must be specified in a string fromat ('1000' and + not 1000). + :param n_tuning: String that specifies the number of iterations to adjust + the samplers's step sizes, scalings or similar (defauls is '500'). + :param n_chains: String that specifies the number of chains to sample. Defauls + is '1' for faster estimation, but note that sampling independent chains + is important for some convergence checks. + :param cores: String that specifies the number of chains to run in parallel. + (defauls is '1'). + :param Initialization method to use for auto-assigned NUTS samplers. The + defauls is 'jitter+adapt_diag' that starts with a identity mass matrix + and then adapt a diagonal based on the variance of the tuning samples + while adding a uniform jitter in [-1, 1] to the starting point in each chain. + :param target_accept: String that of a float in [0, 1] that regulates the + step size such that we approximate this acceptance rate. The defauls is '0.8' + but higher values like 0.9 or 0.95 often work better for problematic posteriors. + :param order: String that defines the order of bspline or polynomial model. + The defauls is '3'. + :param nknots: String that defines the numbers of knots for the bspline model. + The defauls is '5'. Higher values increase the model complexity with negative + effect on the spped of estimations. + :param nn_hidden_layers_num: String the specifies the number of hidden layers + in neural network model. It can be either '1' or '2'. The default is set to '2'. + :param nn_hidden_neuron_num: String that specifies the number of neurons in + the hidden layers. The defauls is set to '2'. + + Written by S.de Boer and S.M. Kia + """ def __init__(self, **kwargs): self.configs = dict() - self.configs['transferred'] = False + # inputs self.configs['trbefile'] = kwargs.pop('trbefile', None) self.configs['tsbefile'] = kwargs.pop('tsbefile', None) + # Model settings self.configs['type'] = kwargs.pop('model_type', 'linear') - self.configs['skewed_likelihood'] = kwargs.pop('skewed_likelihood', 'False') == 'True' - self.configs['pred_type'] = kwargs.pop('pred_type', 'single') self.configs['random_noise'] = kwargs.pop('random_noise', 'True') == 'True' + self.configs['likelihood'] = kwargs.pop('likelihood', 'Normal') + # sampler settings self.configs['n_samples'] = int(kwargs.pop('n_samples', '1000')) self.configs['n_tuning'] = int(kwargs.pop('n_tuning', '500')) self.configs['n_chains'] = int(kwargs.pop('n_chains', '1')) - self.configs['likelihood'] = kwargs.pop('likelihood', 'Normal') self.configs['sampler'] = kwargs.pop('sampler', 'NUTS') self.configs['target_accept'] = float(kwargs.pop('target_accept', '0.8')) self.configs['init'] = kwargs.pop('init', 'jitter+adapt_diag') self.configs['cores'] = int(kwargs.pop('cores', '1')) + # model transfer setting self.configs['freedom'] = int(kwargs.pop('freedom', '1')) + self.configs['transferred'] = False + # deprecated settings + self.configs['skewed_likelihood'] = kwargs.pop('skewed_likelihood', 'False') == 'True' + # misc + self.configs['pred_type'] = kwargs.pop('pred_type', 'single') if self.configs['type'] == 'bspline': self.configs['order'] = int(kwargs.pop('order', '3')) diff --git a/pcntoolkit/normative_model/norm_hbr1.py b/pcntoolkit/normative_model/norm_hbr1.py new file mode 100644 index 00000000..fef3e116 --- /dev/null +++ b/pcntoolkit/normative_model/norm_hbr1.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Thu Jul 25 17:01:24 2019 + +@author: seykia +@author: augub +""" + +from __future__ import print_function +from __future__ import division + + +import os +import warnings +import sys +import numpy as np +from ast import literal_eval as make_tuple + +try: + from pcntoolkit.dataio import fileio + from pcntoolkit.normative_model.norm_base import NormBase + from pcntoolkit.model.hbr import HBR +except ImportError: + pass + + path = os.path.abspath(os.path.dirname(__file__)) + if path not in sys.path: + sys.path.append(path) + del path + import dataio.fileio as fileio + from model.hbr import HBR + from norm_base import NormBase + + +class NormHBR(NormBase): + + """ HBR multi-batch normative modelling class + + :param X: [N×P] array of clinical covariates + :param y: [N×1] array of neuroimaging measures + :param trbefile: the address to the batch effects file for the training set. + the batch effect array should be a [N×M] array where M is the number of + the type of batch effects. For example when the site and gender is modeled + as batch effects M=2. Each column in the batch effect array contains the + batch ID (starting from 0) for each sample. If not specified (default=None) + then all samples assumed to be from the same batch (i.e., the batch effect + is not modelled). + :param tsbefile: Similar to trbefile for the test set. + :param model_type: Specifies the type of the model from 'linear', 'plynomial', + and 'bspline' (defauls is 'linear'). + :param likelihood: specifies the type of likelihood among 'Normal' 'SHASHb','SHASHo', + and 'SHASHo2' (defauls is normal). + :param sampler: specifies the type of PyMC sampler (Defauls is 'NUTS'). + :param n_samples: The number of samples to draw (Default is '1000'). Please + note that this parameter must be specified in a string fromat ('1000' and + not 1000). + :param n_tuning: String that specifies the number of iterations to adjust + the samplers's step sizes, scalings or similar (defauls is '500'). + :param n_chains: String that specifies the number of chains to sample. Defauls + is '1' for faster estimation, but note that sampling independent chains + is important for some convergence checks. + :param cores: String that specifies the number of chains to run in parallel. + (defauls is '1'). + :param Initialization method to use for auto-assigned NUTS samplers. The + defauls is 'jitter+adapt_diag' that starts with a identity mass matrix + and then adapt a diagonal based on the variance of the tuning samples + while adding a uniform jitter in [-1, 1] to the starting point in each chain. + :param target_accept: String that of a float in [0, 1] that regulates the + step size such that we approximate this acceptance rate. The defauls is '0.8' + but higher values like 0.9 or 0.95 often work better for problematic posteriors. + :param order: String that defines the order of bspline or polynomial model. + The defauls is '3'. + :param nknots: String that defines the numbers of knots for the bspline model. + The defauls is '5'. Higher values increase the model complexity with negative + effect on the spped of estimations. + :param nn_hidden_layers_num: String the specifies the number of hidden layers + in neural network model. It can be either '1' or '2'. The default is set to '2'. + :param nn_hidden_neuron_num: String that specifies the number of neurons in + the hidden layers. The defauls is set to '2'. + + """ + + def __init__(self, **kwargs): + + self.configs = dict() + self.configs['trbefile'] = kwargs.pop('trbefile', None) + self.configs['tsbefile'] = kwargs.pop('tsbefile', None) + self.configs['type'] = kwargs.pop('model_type', 'linear') + self.configs['likelihood'] = kwargs.pop('likelihood', 'Normal') + self.configs['sampler'] = kwargs.pop('sampler', 'NUTS') + self.configs['n_samples'] = int(kwargs.pop('n_samples', '1000')) + self.configs['n_tuning'] = int(kwargs.pop('n_tuning', '500')) + self.configs['n_chains'] = int(kwargs.pop('n_chains', '1')) + self.configs['cores'] = int(kwargs.pop('cores', '1')) + self.configs['init'] = kwargs.pop('init', 'jitter+adapt_diag') + self.configs['target_accept'] = float(kwargs.pop('target_accept', '0.8')) + self.configs['freedom'] = int(kwargs.pop('freedom', '1')) + self.configs['pred_type'] = kwargs.pop('pred_type', 'single') # ??? + self.configs['transferred'] = False # Specifies whether this model is transferred or not (is always False when initializing a new model). + + ### Get deprecated soon + #self.configs['skewed_likelihood'] = kwargs.pop('skewed_likelihood', 'False') == 'True' + #self.configs['random_noise'] = kwargs.pop('random_noise', 'True') == 'True' + #self.configs['hetero_noise'] = kwargs.pop('hetero_noise', 'False') == 'True' + ### End of deprecation section + + if self.configs['type'] == 'bspline': + self.configs['order'] = int(kwargs.pop('order', '3')) + self.configs['nknots'] = int(kwargs.pop('nknots', '5')) + elif self.configs['type'] == 'polynomial': + self.configs['order'] = int(kwargs.pop('order', '3')) + elif self.configs['type'] == 'nn': + self.configs['nn_hidden_layers_num'] = int(kwargs.pop('nn_hidden_layers_num', '2')) + self.configs['nn_hidden_neuron_num'] = int(kwargs.pop('nn_hidden_neuron_num', '2')) + if self.configs['nn_hidden_layers_num'] > 2 or self.configs['nn_hidden_layers_num'] <= 0: + raise ValueError("Using " + str(self.configs['nn_hidden_layers_num']) \ + + " layers was not implemented. The number of " \ + + " layers has to be either 1 or 2.") + elif self.configs['type'] == 'linear': + pass + else: + raise ValueError("Unknown model type, please specify from 'linear', \ + 'polynomial', 'bspline', or 'nn'.") + + if self.configs['type'] in ['bspline', 'polynomial', 'linear']: + + self.configs['centered'] = kwargs.pop('centered', 'False') == 'True' + self.configs['random_mu'] = kwargs.pop('random_mu', 'True') == 'True' + ######## Deprecations (remove in later version) + for c in ['mu_linear', 'linear_mu', 'random_intercept', 'random_slope']: + if c in kwargs.keys(): + print(f'The keyword {c} is deprecated. It is automatically replaced with random_mu.') + self.configs['random_mu'] = kwargs.pop(c, 'True') == 'True' + ##### End Deprecations + if self.configs['random_mu']: + self.configs['random_mu_intercept'] = kwargs.pop('random_mu_intercept', 'True') == 'True' + self.configs['random_mu_slope'] = kwargs.pop('random_mu_slope', 'True') == 'True' + + + self.configs['random_sigma'] = kwargs.pop('random_sigma', 'True') == 'True' + ######## Deprecations (remove in later version) + for c in ['sigma_linear', 'linear_sigma', 'sigma_intercept', 'random_slope']: + if c in kwargs.keys(): + print(f'The keyword {c} is deprecated. It is automatically replaced with random_mu.') + self.configs['random_sigma'] = kwargs.pop(c, 'True') == 'True' + ##### End Deprecations + if self.configs['random_sigma']: + self.configs['random_sigma_intercept'] = kwargs.pop('random_sigma_intercept', 'False') == 'True' + self.configs['random_sigma_slope'] = kwargs.pop('random_sigma_slope', 'False') == 'True' + + + for p in ['epsilon', 'delta']: + self.configs[f'random_{p}'] = kwargs.pop(f'random_{p}', 'False') == 'True' + + ######## Deprecations (remove in later version) + for c in [f'{p}_linear', f'linear_{p}','random_noise']: + if c in kwargs.keys(): + print(f'The keywords {c} is deprecated. It is automatically replaced with \'random_{p}\'') + self.configs[f'random_{p}'] = kwargs.pop(c, 'True') == 'True' + ##### End Deprecations + + if self.configs[f'random_{p}']: + self.configs[f'random_{p}_intercept'] = kwargs.pop(f'random_{p}_intercept', 'True') == 'True' + self.configs[f'random_{p}_slope'] = kwargs.pop(f'random_{p}_slope', 'True') == 'True' + + #for c in ['centered','random']: + # self.configs[f'{c}_{p}'] = kwargs.pop(f'{c}_{p}', 'False') == 'True' + # for sp in ['slope','intercept']: + # self.configs[f'{c}_{sp}_{p}'] = kwargs.pop(f'{c}_{sp}_{p}', 'False') == 'True' + + ######## Deprecations (remove in later version) + #if self.configs['linear_sigma']: + # if 'random_noise' in kwargs.keys(): + # print("The keyword \'random_noise\' is deprecated. It is now automatically replaced with \'random_intercept_sigma\', because sigma is linear") + # self.configs['random_intercept_sigma'] = kwargs.pop('random_noise','False') == 'True' + #elif 'random_noise' in kwargs.keys(): + # print("The keyword \'random_noise\' is deprecated. It is now automatically replaced with \'random_sigma\', because sigma is fixed") + # self.configs['random_sigma'] = kwargs.pop('random_noise','False') == 'True' + #if 'random_slope' in kwargs.keys(): + # print("The keyword \'random_slope\' is deprecated. It is now automatically replaced with \'random_slope_mu\'") + # self.configs['random_slope_mu'] = kwargs.pop('random_slope','False') == 'True' + ##### End Deprecations + + + self.hbr = HBR(self.configs) + + @property + def n_params(self): + return 1 + + @property + def neg_log_lik(self): + return -1 + + def estimate(self, X, y, **kwargs): + + trbefile = kwargs.pop('trbefile', None) + if trbefile is not None: + batch_effects_train = fileio.load(trbefile) + else: + print('Could not find batch-effects file! Initilizing all as zeros ...') + batch_effects_train = np.zeros([X.shape[0], 1]) + + self.hbr.estimate(X, y, batch_effects_train) + + return self + + def predict(self, Xs, X=None, Y=None, **kwargs): + + tsbefile = kwargs.pop('tsbefile', None) + if tsbefile is not None: + batch_effects_test = fileio.load(tsbefile) + else: + print('Could not find batch-effects file! Initilizing all as zeros ...') + batch_effects_test = np.zeros([Xs.shape[0], 1]) + + pred_type = self.configs['pred_type'] + + if self.configs['transferred'] == False: + yhat, s2 = self.hbr.predict(Xs, batch_effects_test, pred=pred_type) + else: + raise ValueError("This is a transferred model. Please use predict_on_new_sites function.") + + return yhat.squeeze(), s2.squeeze() + + def estimate_on_new_sites(self, X, y, batch_effects): + self.hbr.estimate_on_new_site(X, y, batch_effects) + self.configs['transferred'] = True + return self + + def predict_on_new_sites(self, X, batch_effects): + + yhat, s2 = self.hbr.predict_on_new_site(X, batch_effects) + return yhat, s2 + + def extend(self, X, y, batch_effects, X_dummy_ranges=[[0.1, 0.9, 0.01]], + merge_batch_dim=0, samples=10, informative_prior=False): + + X_dummy, batch_effects_dummy = self.hbr.create_dummy_inputs(X_dummy_ranges) + + X_dummy, batch_effects_dummy, Y_dummy = self.hbr.generate(X_dummy, + batch_effects_dummy, samples) + + batch_effects[:, merge_batch_dim] = batch_effects[:, merge_batch_dim] + \ + np.max(batch_effects_dummy[:, merge_batch_dim]) + 1 + + if informative_prior: + self.hbr.adapt(np.concatenate((X_dummy, X)), + np.concatenate((Y_dummy, y)), + np.concatenate((batch_effects_dummy, batch_effects))) + else: + self.hbr.estimate(np.concatenate((X_dummy, X)), + np.concatenate((Y_dummy, y)), + np.concatenate((batch_effects_dummy, batch_effects))) + + return self + + def tune(self, X, y, batch_effects, X_dummy_ranges=[[0.1, 0.9, 0.01]], + merge_batch_dim=0, samples=10, informative_prior=False): + + tune_ids = list(np.unique(batch_effects[:, merge_batch_dim])) + + X_dummy, batch_effects_dummy = self.hbr.create_dummy_inputs(X_dummy_ranges) + + for idx in tune_ids: + X_dummy = X_dummy[batch_effects_dummy[:, merge_batch_dim] != idx, :] + batch_effects_dummy = batch_effects_dummy[batch_effects_dummy[:, merge_batch_dim] != idx, :] + + X_dummy, batch_effects_dummy, Y_dummy = self.hbr.generate(X_dummy, + batch_effects_dummy, samples) + + if informative_prior: + self.hbr.adapt(np.concatenate((X_dummy, X)), + np.concatenate((Y_dummy, y)), + np.concatenate((batch_effects_dummy, batch_effects))) + else: + self.hbr.estimate(np.concatenate((X_dummy, X)), + np.concatenate((Y_dummy, y)), + np.concatenate((batch_effects_dummy, batch_effects))) + + return self + + def merge(self, nm, X_dummy_ranges=[[0.1, 0.9, 0.01]], merge_batch_dim=0, + samples=10): + + X_dummy1, batch_effects_dummy1 = self.hbr.create_dummy_inputs(X_dummy_ranges) + X_dummy2, batch_effects_dummy2 = nm.hbr.create_dummy_inputs(X_dummy_ranges) + + X_dummy1, batch_effects_dummy1, Y_dummy1 = self.hbr.generate(X_dummy1, + batch_effects_dummy1, samples) + X_dummy2, batch_effects_dummy2, Y_dummy2 = nm.hbr.generate(X_dummy2, + batch_effects_dummy2, samples) + + batch_effects_dummy2[:, merge_batch_dim] = batch_effects_dummy2[:, merge_batch_dim] + \ + np.max(batch_effects_dummy1[:, merge_batch_dim]) + 1 + + self.hbr.estimate(np.concatenate((X_dummy1, X_dummy2)), + np.concatenate((Y_dummy1, Y_dummy2)), + np.concatenate((batch_effects_dummy1, + batch_effects_dummy2))) + + return self + + def generate(self, X, batch_effects, samples=10): + + X, batch_effects, generated_samples = self.hbr.generate(X, batch_effects, + samples) + return X, batch_effects, generated_samples diff --git a/pcntoolkit/normative_model/norm_hbr_b.py b/pcntoolkit/normative_model/norm_hbr_b.py new file mode 100644 index 00000000..a7062a5b --- /dev/null +++ b/pcntoolkit/normative_model/norm_hbr_b.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Thu Jul 25 17:01:24 2019 + +@author: seykia +""" + +from __future__ import print_function +from __future__ import division + +import numpy as np + +from pcntoolkit.fileio import fileio +from pcntoolkit.normative_model.norm_base import NormBase +from pcntoolkit.model.hbr import HBR + + +class NormHBR(NormBase): + """ Classical GPR-based normative modelling approach + """ + + def __init__(self, **kwargs): + + self.configs = dict() + self.configs['transferred'] = False + self.configs['trbefile'] = kwargs.pop('trbefile',None) + self.configs['tsbefile'] = kwargs.pop('tsbefile',None) + self.configs['type'] = kwargs.pop('model_type', 'linear') + self.configs['skewed_likelihood'] = kwargs.pop('skewed_likelihood', 'False') == 'True' + self.configs['pred_type'] = kwargs.pop('pred_type', 'single') + self.configs['random_noise'] = kwargs.pop('random_noise', 'True') == 'True' + self.configs['n_samples'] = int(kwargs.pop('n_samples', '1000')) + self.configs['n_tuning'] = int(kwargs.pop('n_tuning', '500')) + self.configs['n_chains'] = int(kwargs.pop('n_chains', '1')) + self.configs['target_accept'] = float(kwargs.pop('target_accept', '0.8')) + self.configs['init'] = kwargs.pop('init', 'jitter+adapt_diag') + self.configs['cores'] = int(kwargs.pop('cores', '1')) + self.configs['freedom'] = int(kwargs.pop('freedom', '1')) + + if self.configs['type'] == 'bspline': + self.configs['order'] = int(kwargs.pop('order', '3')) + self.configs['nknots'] = int(kwargs.pop('nknots', '5')) + self.configs['random_intercept'] = kwargs.pop('random_intercept', 'True') == 'True' + self.configs['random_slope'] = kwargs.pop('random_slope', 'True') == 'True' + elif self.configs['type'] == 'polynomial': + self.configs['order'] = int(kwargs.pop('order', '3')) + self.configs['random_intercept'] = kwargs.pop('random_intercept', 'True') == 'True' + self.configs['random_slope'] = kwargs.pop('random_slope', 'True') == 'True' + elif self.configs['type'] == 'nn': + self.configs['nn_hidden_neuron_num'] = int(kwargs.pop('nn_hidden_neuron_num', '2')) + self.configs['nn_hidden_layers_num'] = int(kwargs.pop('nn_hidden_layers_num', '2')) + if self.configs['nn_hidden_layers_num'] > 2: + raise ValueError("Using " + str(self.configs['nn_hidden_layers_num']) \ + + " layers was not implemented. The number of " \ + + " layers has to be less than 3.") + elif self.configs['type'] == 'linear': + self.configs['random_intercept'] = kwargs.pop('random_intercept', 'True') == 'True' + self.configs['random_slope'] = kwargs.pop('random_slope', 'True') == 'True' + else: + raise ValueError("Unknown model type, please specify from 'linear', \ + 'polynomial', 'bspline', or 'nn'.") + + if self.configs['random_noise']: + self.configs['hetero_noise'] = kwargs.pop('hetero_noise', 'False') == 'True' + + self.hbr = HBR(self.configs) + + @property + def n_params(self): + return 1 + + @property + def neg_log_lik(self): + return -1 + + + def estimate(self, X, y, **kwargs): + + trbefile = kwargs.pop('trbefile', None) + if trbefile is not None: + batch_effects_train = fileio.load(trbefile) + else: + print('Could not find batch-effects file! Initilizing all as zeros ...') + batch_effects_train = np.zeros([X.shape[0],1]) + + self.hbr.estimate(X, y, batch_effects_train) + + return self + + + def predict(self, Xs, X=None, Y=None, **kwargs): + + tsbefile = kwargs.pop('tsbefile', None) + if tsbefile is not None: + batch_effects_test = fileio.load(tsbefile) + else: + print('Could not find batch-effects file! Initilizing all as zeros ...') + batch_effects_test = np.zeros([Xs.shape[0],1]) + + pred_type = self.configs['pred_type'] + + if self.configs['transferred'] == False: + yhat, s2 = self.hbr.predict(Xs, batch_effects_test, pred = pred_type) + else: + raise ValueError("This is a transferred model. Please use predict_on_new_sites function.") + + return yhat.squeeze(), s2.squeeze() + + + def estimate_on_new_sites(self, X, y, batch_effects): + + self.hbr.estimate_on_new_site(X, y, batch_effects) + self.configs['transferred'] = True + return self + + + def predict_on_new_sites(self, X, batch_effects): + + yhat, s2 = self.hbr.predict_on_new_site(X, batch_effects) + return yhat, s2 + + + def extend(self, X, y, batch_effects, X_dummy, batch_effects_dummy, + samples=10, informative_prior=False): + + X_dummy, batch_effects_dummy, Y_dummy = self.hbr.generate(X_dummy, + batch_effects_dummy, samples) + if informative_prior: + self.hbr.estimate_on_new_sites(np.concatenate((X_dummy, X)), + np.concatenate((Y_dummy, y)), + np.concatenate((batch_effects_dummy, batch_effects))) + else: + self.hbr.estimate(np.concatenate((X_dummy, X)), + np.concatenate((Y_dummy, y)), + np.concatenate((batch_effects_dummy, batch_effects))) + + return self + + + def generate(self, X, batch_effects, samples=10): + + X, batch_effects, generated_samples = self.hbr.generate(X, batch_effects, + samples) + return X, batch_effects, generated_samples + \ No newline at end of file diff --git a/pcntoolkit/normative_model/norm_np.py b/pcntoolkit/normative_model/norm_np.py old mode 100644 new mode 100755 index 7b632522..fb03fff6 --- a/pcntoolkit/normative_model/norm_np.py +++ b/pcntoolkit/normative_model/norm_np.py @@ -42,7 +42,7 @@ def __init__(self, x, y, args): self.r_dim = args.r_dim self.z_dim = args.z_dim self.hidden_neuron_num = args.hidden_neuron_num - self.h_1 = nn.Linear(x.shape[1] + y.shape[1], self.hidden_neuron_num) + self.h_1 = nn.Linear(x.shape[2] + y.shape[2], self.hidden_neuron_num) self.h_2 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) self.h_3 = nn.Linear(self.hidden_neuron_num, self.r_dim) @@ -62,24 +62,17 @@ def __init__(self, x, y, args): self.z_dim = args.z_dim self.hidden_neuron_num = args.hidden_neuron_num - self.g_1 = nn.Linear(self.z_dim, self.hidden_neuron_num) + self.g_1 = nn.Linear(self.z_dim+x.shape[2], self.hidden_neuron_num) self.g_2 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) - self.g_3 = nn.Linear(self.hidden_neuron_num, y.shape[1]) - - self.g_1_84 = nn.Linear(self.z_dim, self.hidden_neuron_num) - self.g_2_84 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) - self.g_3_84 = nn.Linear(self.hidden_neuron_num, y.shape[1]) + self.g_3 = nn.Linear(self.hidden_neuron_num, y.shape[2]) - def forward(self, z_sample): - z_hat = F.relu(self.g_1(z_sample)) + def forward(self, z_sample, x_target): + z_x = torch.cat([z_sample, x_target], dim=2) + z_hat = F.relu(self.g_1(z_x)) z_hat = F.relu(self.g_2(z_hat)) - y_hat = torch.sigmoid(self.g_3(z_hat)) - - z_hat_84 = F.relu(self.g_1(z_sample)) - z_hat_84 = F.relu(self.g_2_84(z_hat_84)) - y_hat_84 = torch.sigmoid(self.g_3_84(z_hat_84)) + y_hat = self.g_3(z_hat) - return y_hat, y_hat_84 + return y_hat @@ -155,37 +148,29 @@ def neg_log_lik(self): return -1 def estimate(self, X, y): - if y.ndim == 1: - y = y.reshape(-1,1) - sample_num = X.shape[0] - batch_size = self.args.batch_size + + sample_num, point_num, feature_num = X.shape factor_num = self.args.m + batch_size = self.args.batch_size mini_batch_num = int(np.floor(sample_num/batch_size)) device = self.args.device - self.scaler = MinMaxScaler() - y = self.scaler.fit_transform(y) - - self.reg = [] - for i in range(factor_num): - self.reg.append(LinearRegression()) - idx = np.random.randint(0, sample_num, sample_num)#int(sample_num/10)) - self.reg[i].fit(X[idx,:],y[idx,:]) + x_all = torch.tensor(X, device=device, dtype = torch.float) + y_all = torch.tensor(y, device=device, dtype = torch.float) - x_context = np.zeros([sample_num, factor_num, X.shape[1]]) + x_context = np.zeros([sample_num, factor_num, X.shape[2]]) y_context = np.zeros([sample_num, factor_num, 1]) - s = X.std(axis=0) - for j in range(factor_num): - x_context[:,j,:] = X + np.sqrt(self.args.nv) * s * np.random.randn(X.shape[0], X.shape[1]) - y_context[:,j,:] = self.reg[j].predict(x_context[:,j,:]) - + for i in range(sample_num): + idx = np.random.permutation(point_num)[0:factor_num] + for j in range(factor_num): + x_context[i,j,:] = X[i,idx[j],:] + y_context[i,j,:] =y[i,idx[j],:] + x_context = torch.tensor(x_context, device=device, dtype = torch.float) y_context = torch.tensor(y_context, device=device, dtype = torch.float) - x_all = torch.tensor(np.expand_dims(X,axis=1), device=device, dtype = torch.float) - y_all = torch.tensor(y.reshape(-1, 1, y.shape[1]), device=device, dtype = torch.float) - + self.model.train() epochs = [int(self.args.epochs/4),int(self.args.epochs/2),int(self.args.epochs/5), int(self.args.epochs-self.args.epochs/4-self.args.epochs/2-self.args.epochs/5)] @@ -197,8 +182,8 @@ def estimate(self, X, y): for i in range(mini_batch_num): optimizer.zero_grad() idx = np.arange(i*batch_size,(i+1)*batch_size) - y_hat, y_hat_84, z_all, z_context, dummy, dummy = self.model(x_context[idx,:,:], y_context[idx,:,:], x_all[idx,:,:], y_all[idx,:,:]) - loss = np_loss(y_hat, y_hat_84, y_all[idx,0,:], z_all, z_context) + y_hat, z_all, z_context, dummy = self.model(x_context[idx,:,:], y_context[idx,:,:], x_all[idx,:,:], y_all[idx,:,:]) + loss = np_loss(y_hat, y_all[idx,:,:], z_all, z_context) loss.backward() train_loss += loss.item() optimizer.step() @@ -206,24 +191,14 @@ def estimate(self, X, y): k += 1 return self - def predict(self, Xs, X=None, Y=None, theta=None): + def predict(self, Xs, ys): sample_num = Xs.shape[0] factor_num = self.args.m - x_context_test = np.zeros([sample_num, factor_num, Xs.shape[1]]) - y_context_test = np.zeros([sample_num, factor_num, 1]) - for j in range(factor_num): - x_context_test[:,j,:] = Xs - y_context_test[:,j,:] = self.reg[j].predict(x_context_test[:,j,:]) - x_context_test = torch.tensor(x_context_test, device=self.args.device, dtype = torch.float) - y_context_test = torch.tensor(y_context_test, device=self.args.device, dtype = torch.float) + + x_context_test = torch.tensor(Xs, device=self.args.device, dtype = torch.float) + y_context_test = torch.tensor(ys, device=self.args.device, dtype = torch.float) self.model.eval() with torch.no_grad(): - y_hat, y_hat_84, z_all, z_context, y_sigma, y_sigma_84 = self.model(x_context_test, y_context_test, n = 100) - - y_hat = self.scaler.inverse_transform(y_hat.cpu().numpy()) - y_hat_84 = self.scaler.inverse_transform(y_hat_84.cpu().numpy()) - y_sigma = y_sigma.cpu().numpy() * (self.scaler.data_max_ - self.scaler.data_min_) - y_sigma_84 = y_sigma_84.cpu().numpy() * (self.scaler.data_max_ - self.scaler.data_min_) - sigma_al = y_hat - y_hat_84 - return y_hat.squeeze(), (y_sigma**2 + sigma_al**2).squeeze() #, z_context[0].cpu().numpy(), z_context[1].cpu().numpy() + y_hat, z_all, z_context, y_sigma = self.model(x_context_test, y_context_test, n = 100) + return y_hat.squeeze(), (y_sigma**2).squeeze() #, z_context[0].cpu().numpy(), z_context[1].cpu().numpy() \ No newline at end of file diff --git a/pcntoolkit/normative_model/norm_np1.py b/pcntoolkit/normative_model/norm_np1.py new file mode 100644 index 00000000..7b632522 --- /dev/null +++ b/pcntoolkit/normative_model/norm_np1.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Nov 22 14:41:07 2019 + +@author: seykia +""" + +from __future__ import print_function +from __future__ import division + +import os +import sys +import numpy as np +import torch +from torch import nn, optim +from torch.nn import functional as F +from sklearn.linear_model import LinearRegression +from sklearn.preprocessing import MinMaxScaler +import pickle + +try: # run as a package if installed + from pcntoolkit.normative_model.normbase import NormBase + from pcntoolkit.model.NPR import NPR, np_loss +except ImportError: + pass + + path = os.path.abspath(os.path.dirname(__file__)) + if path not in sys.path: + sys.path.append(path) + del path + + from model.NPR import NPR, np_loss + from norm_base import NormBase + +class struct(object): + pass + +class Encoder(nn.Module): + def __init__(self, x, y, args): + super(Encoder, self).__init__() + self.r_dim = args.r_dim + self.z_dim = args.z_dim + self.hidden_neuron_num = args.hidden_neuron_num + self.h_1 = nn.Linear(x.shape[1] + y.shape[1], self.hidden_neuron_num) + self.h_2 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) + self.h_3 = nn.Linear(self.hidden_neuron_num, self.r_dim) + + def forward(self, x, y): + x_y = torch.cat([x, y], dim=2) + x_y = F.relu(self.h_1(x_y)) + x_y = F.relu(self.h_2(x_y)) + x_y = F.relu(self.h_3(x_y)) + r = torch.mean(x_y, dim=1) + return r + + +class Decoder(nn.Module): + def __init__(self, x, y, args): + super(Decoder, self).__init__() + self.r_dim = args.r_dim + self.z_dim = args.z_dim + self.hidden_neuron_num = args.hidden_neuron_num + + self.g_1 = nn.Linear(self.z_dim, self.hidden_neuron_num) + self.g_2 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) + self.g_3 = nn.Linear(self.hidden_neuron_num, y.shape[1]) + + self.g_1_84 = nn.Linear(self.z_dim, self.hidden_neuron_num) + self.g_2_84 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) + self.g_3_84 = nn.Linear(self.hidden_neuron_num, y.shape[1]) + + def forward(self, z_sample): + z_hat = F.relu(self.g_1(z_sample)) + z_hat = F.relu(self.g_2(z_hat)) + y_hat = torch.sigmoid(self.g_3(z_hat)) + + z_hat_84 = F.relu(self.g_1(z_sample)) + z_hat_84 = F.relu(self.g_2_84(z_hat_84)) + y_hat_84 = torch.sigmoid(self.g_3_84(z_hat_84)) + + return y_hat, y_hat_84 + + + + +class NormNP(NormBase): + """ Classical GPR-based normative modelling approach + """ + + def __init__(self, X, y, configparam=None): + self.configparam = configparam + if configparam is not None: + with open(configparam, 'rb') as handle: + config = pickle.load(handle) + args = struct() + if 'batch_size' in config: + args.batch_size = config['batch_size'] + else: + args.batch_size = 10 + if 'epochs' in config: + args.epochs = config['epochs'] + else: + args.epochs = 100 + if 'device' in config: + args.device = config['device'] + else: + args.device = torch.device('cpu') + if 'm' in config: + args.m = config['m'] + else: + args.m = 200 + if 'hidden_neuron_num' in config: + args.hidden_neuron_num = config['hidden_neuron_num'] + else: + args.hidden_neuron_num = 10 + if 'r_dim' in config: + args.r_dim = config['r_dim'] + else: + args.r_dim = 5 + if 'z_dim' in config: + args.z_dim = config['z_dim'] + else: + args.z_dim = 3 + if 'nv' in config: + args.nv = config['nv'] + else: + args.nv = 0.01 + else: + args = struct() + args.batch_size = 10 + args.epochs = 100 + args.device = torch.device('cpu') + args.m = 200 + args.hidden_neuron_num = 10 + args.r_dim = 5 + args.z_dim = 3 + args.nv = 0.01 + + if y is not None: + if y.ndim == 1: + y = y.reshape(-1,1) + self.args = args + self.encoder = Encoder(X, y, args) + self.decoder = Decoder(X, y, args) + self.model = NPR(self.encoder, self.decoder, args) + + + @property + def n_params(self): + return 1 + + @property + def neg_log_lik(self): + return -1 + + def estimate(self, X, y): + if y.ndim == 1: + y = y.reshape(-1,1) + sample_num = X.shape[0] + batch_size = self.args.batch_size + factor_num = self.args.m + mini_batch_num = int(np.floor(sample_num/batch_size)) + device = self.args.device + + self.scaler = MinMaxScaler() + y = self.scaler.fit_transform(y) + + self.reg = [] + for i in range(factor_num): + self.reg.append(LinearRegression()) + idx = np.random.randint(0, sample_num, sample_num)#int(sample_num/10)) + self.reg[i].fit(X[idx,:],y[idx,:]) + + x_context = np.zeros([sample_num, factor_num, X.shape[1]]) + y_context = np.zeros([sample_num, factor_num, 1]) + + s = X.std(axis=0) + for j in range(factor_num): + x_context[:,j,:] = X + np.sqrt(self.args.nv) * s * np.random.randn(X.shape[0], X.shape[1]) + y_context[:,j,:] = self.reg[j].predict(x_context[:,j,:]) + + x_context = torch.tensor(x_context, device=device, dtype = torch.float) + y_context = torch.tensor(y_context, device=device, dtype = torch.float) + + x_all = torch.tensor(np.expand_dims(X,axis=1), device=device, dtype = torch.float) + y_all = torch.tensor(y.reshape(-1, 1, y.shape[1]), device=device, dtype = torch.float) + + self.model.train() + epochs = [int(self.args.epochs/4),int(self.args.epochs/2),int(self.args.epochs/5), + int(self.args.epochs-self.args.epochs/4-self.args.epochs/2-self.args.epochs/5)] + k = 1 + for e in range(len(epochs)): + optimizer = optim.Adam(self.model.parameters(), lr=10**(-e-2)) + for j in range(epochs[e]): + train_loss = 0 + for i in range(mini_batch_num): + optimizer.zero_grad() + idx = np.arange(i*batch_size,(i+1)*batch_size) + y_hat, y_hat_84, z_all, z_context, dummy, dummy = self.model(x_context[idx,:,:], y_context[idx,:,:], x_all[idx,:,:], y_all[idx,:,:]) + loss = np_loss(y_hat, y_hat_84, y_all[idx,0,:], z_all, z_context) + loss.backward() + train_loss += loss.item() + optimizer.step() + print('Epoch: %d, Loss:%f' %( k, train_loss)) + k += 1 + return self + + def predict(self, Xs, X=None, Y=None, theta=None): + sample_num = Xs.shape[0] + factor_num = self.args.m + x_context_test = np.zeros([sample_num, factor_num, Xs.shape[1]]) + y_context_test = np.zeros([sample_num, factor_num, 1]) + for j in range(factor_num): + x_context_test[:,j,:] = Xs + y_context_test[:,j,:] = self.reg[j].predict(x_context_test[:,j,:]) + x_context_test = torch.tensor(x_context_test, device=self.args.device, dtype = torch.float) + y_context_test = torch.tensor(y_context_test, device=self.args.device, dtype = torch.float) + self.model.eval() + with torch.no_grad(): + y_hat, y_hat_84, z_all, z_context, y_sigma, y_sigma_84 = self.model(x_context_test, y_context_test, n = 100) + + y_hat = self.scaler.inverse_transform(y_hat.cpu().numpy()) + y_hat_84 = self.scaler.inverse_transform(y_hat_84.cpu().numpy()) + y_sigma = y_sigma.cpu().numpy() * (self.scaler.data_max_ - self.scaler.data_min_) + y_sigma_84 = y_sigma_84.cpu().numpy() * (self.scaler.data_max_ - self.scaler.data_min_) + sigma_al = y_hat - y_hat_84 + return y_hat.squeeze(), (y_sigma**2 + sigma_al**2).squeeze() #, z_context[0].cpu().numpy(), z_context[1].cpu().numpy() + \ No newline at end of file diff --git a/pcntoolkit/normative_parallel.py b/pcntoolkit/normative_parallel.py index 3076ce28..ad9d7c35 100755 --- a/pcntoolkit/normative_parallel.py +++ b/pcntoolkit/normative_parallel.py @@ -88,8 +88,17 @@ def execute_nm(processing_dir, :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 - - written by (primarily) T Wolfers, (adapted) SM Kia, (adapted) S Rutherford. + :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 + jobs are completed then the failed jobs are rerun + and the results are automaticallu collectted. + Using 'query' is similar to 'auto' unless it + asks for user verification thius is immune to + endless loop in the case of bugs in the code. + + written by (primarily) T Wolfers, (adapted) SM Kia + The documentation is adapated by S Rutherford. ''' if normative_path is None: diff --git a/pcntoolkit/temp.py b/pcntoolkit/temp.py new file mode 100755 index 00000000..80356f54 --- /dev/null +++ b/pcntoolkit/temp.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Fri Dec 31 10:21:17 2021 + +@author: seykia +""" +import sys +from subprocess import check_output +import time + +def retrieve_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 + + return jobs + + +def check_job_status(jobs): + + running_jobs = retrieve_jobs() + + r = 0 + c = 0 + q = 0 + u = 0 + for job in jobs: + 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 + + print('Total:%d, Queued: %d, Running:%d, Completed:%d, Unknown:%d' + %(len(jobs), q, r, c, u)) + return q,r,c,u + + +def check_jobs(jobs, delay=60): + + n = len(jobs) + + while(True): + q,r,c,u = check_job_status(jobs) + if c == n: + print('All jobs are finished!') + break + time.sleep(delay) + + + + + + + + \ No newline at end of file diff --git a/pcntoolkit/util/preprocess.py b/pcntoolkit/util/preprocess.py new file mode 100755 index 00000000..67790449 --- /dev/null +++ b/pcntoolkit/util/preprocess.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed Apr 27 17:54:45 2022 + +@author: seykia +""" +import numpy as np + +class scaler: + + def __init__(self, scaler_type='None', tail=0.01): + + self.scaler_type = scaler_type + self.tail = tail + + if self.scaler_type not in ['None', 'standardize', 'minmax', 'robminmax']: + raise ValueError("Undifined scaler type!") + + + def fit(self, X): + + if self.scaler_type == 'standardize': + + self.m = np.mean(X, axis=0) + self.s = np.std(X, axis=0) + + elif self.scaler_type == 'minmax': + self.min = np.min(X, axis=0) + self.max = np.max(X, axis=0) + + elif self.scaler_type == 'robminmax': + self.min = np.zeros([X.shape[1],]) + self.max = np.zeros([X.shape[1],]) + for i in range(X.shape[1]): + self.min[i] = np.median(np.sort(X[:,i])[0:int(np.round(X.shape[0] * self.tail))]) + self.max[i] = np.median(np.sort(X[:,i])[-int(np.round(X.shape[0] * self.tail)):]) + + def transform(self, X, adjust_outliers=False): + + if self.scaler_type == 'standardize': + + X = (X - self.m) / self.s + + elif self.scaler_type in ['minmax', 'robminmax']: + + X = (X - self.min) / (self.max - self.min) + + if adjust_outliers: + + X[X < 0] = 0 + X[X > 1] = 1 + + return X + + def inverse_transform(self, X, index=None): + + if self.scaler_type == 'standardize': + if index is None: + X = X * self.s + self.m + else: + X = X * self.s[index] + self.m[index] + + elif self.scaler_type in ['minmax', 'robminmax']: + if index is None: + X = X * (self.max - self.min) + self.min + else: + X = X * (self.max[index] - self.min[index]) + self.min[index] + return X + + def fit_transform(self, X, adjust_outliers=False): + + if self.scaler_type == 'standardize': + + self.m = np.mean(X, axis=0) + self.s = np.std(X, axis=0) + X = (X - self.m) / self.s + + elif self.scaler_type == 'minmax': + + self.min = np.min(X, axis=0) + self.max = np.max(X, axis=0) + X = (X - self.min) / (self.max - self.min) + + elif self.scaler_type == 'robminmax': + + self.min = np.zeros([X.shape[1],]) + self.max = np.zeros([X.shape[1],]) + + for i in range(X.shape[1]): + self.min[i] = np.median(np.sort(X[:,i])[0:int(np.round(X.shape[0] * self.tail))]) + self.max[i] = np.median(np.sort(X[:,i])[-int(np.round(X.shape[0] * self.tail)):]) + + X = (X - self.min) / (self.max - self.min) + + if adjust_outliers: + X[X < 0] = 0 + X[X > 1] = 1 + + return X + From 1c4585011b048431798bb57fd1b7fc184caee0fa Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Sun, 19 Feb 2023 14:23:49 +0100 Subject: [PATCH 18/36] Revert "Reverting the prior and sampling setting." This reverts commit 5fdf2d174ab0c3e6ee1cd6c150c98b386258f075. :q --- pcntoolkit/model/hbr.py | 2 +- pcntoolkit/normative_model/norm_hbr.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pcntoolkit/model/hbr.py b/pcntoolkit/model/hbr.py index 98c71037..81a013c2 100644 --- a/pcntoolkit/model/hbr.py +++ b/pcntoolkit/model/hbr.py @@ -177,7 +177,7 @@ def hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): sigma_slope_mu_params = (5.,), mu_intercept_mu_params=(0.,10.), sigma_intercept_mu_params = (5.,)).get_samples(pb) - sigma = pb.make_param("sigma", mu_sigma_params = (10., 5.), + sigma = pb.make_param("sigma", mu_sigma_params = (0., 10.), sigma_sigma_params = (5.,)).get_samples(pb) sigma_plus = pm.math.log(1+pm.math.exp(sigma)) y_like = pm.Normal('y_like',mu=mu, sigma=sigma_plus, observed=y) diff --git a/pcntoolkit/normative_model/norm_hbr.py b/pcntoolkit/normative_model/norm_hbr.py index d769163e..6aadef55 100644 --- a/pcntoolkit/normative_model/norm_hbr.py +++ b/pcntoolkit/normative_model/norm_hbr.py @@ -199,6 +199,8 @@ def __init__(self, **kwargs): self.configs['random_slope_mu'] = kwargs.pop('random_slope_mu','True') == 'True' self.configs['random_sigma'] = kwargs.pop('random_sigma','True') == 'True' self.configs['centered_sigma'] = kwargs.pop('centered_sigma','True') == 'True' + self.configs['centered_slope_mu'] = kwargs.pop('centered_slope_mu','True') == 'True' + self.configs['centered_intercept_mu'] = kwargs.pop('centered_intercept_mu','True') == 'True' ## End default parameters self.hbr = HBR(self.configs) From 8134b95359f126691c2cfe910bf2555eea357abb Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Sun, 19 Feb 2023 14:35:37 +0100 Subject: [PATCH 19/36] Delete norm_hbr1.py --- pcntoolkit/normative_model/norm_hbr1.py | 309 ------------------------ 1 file changed, 309 deletions(-) delete mode 100644 pcntoolkit/normative_model/norm_hbr1.py diff --git a/pcntoolkit/normative_model/norm_hbr1.py b/pcntoolkit/normative_model/norm_hbr1.py deleted file mode 100644 index fef3e116..00000000 --- a/pcntoolkit/normative_model/norm_hbr1.py +++ /dev/null @@ -1,309 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Thu Jul 25 17:01:24 2019 - -@author: seykia -@author: augub -""" - -from __future__ import print_function -from __future__ import division - - -import os -import warnings -import sys -import numpy as np -from ast import literal_eval as make_tuple - -try: - from pcntoolkit.dataio import fileio - from pcntoolkit.normative_model.norm_base import NormBase - from pcntoolkit.model.hbr import HBR -except ImportError: - pass - - path = os.path.abspath(os.path.dirname(__file__)) - if path not in sys.path: - sys.path.append(path) - del path - import dataio.fileio as fileio - from model.hbr import HBR - from norm_base import NormBase - - -class NormHBR(NormBase): - - """ HBR multi-batch normative modelling class - - :param X: [N×P] array of clinical covariates - :param y: [N×1] array of neuroimaging measures - :param trbefile: the address to the batch effects file for the training set. - the batch effect array should be a [N×M] array where M is the number of - the type of batch effects. For example when the site and gender is modeled - as batch effects M=2. Each column in the batch effect array contains the - batch ID (starting from 0) for each sample. If not specified (default=None) - then all samples assumed to be from the same batch (i.e., the batch effect - is not modelled). - :param tsbefile: Similar to trbefile for the test set. - :param model_type: Specifies the type of the model from 'linear', 'plynomial', - and 'bspline' (defauls is 'linear'). - :param likelihood: specifies the type of likelihood among 'Normal' 'SHASHb','SHASHo', - and 'SHASHo2' (defauls is normal). - :param sampler: specifies the type of PyMC sampler (Defauls is 'NUTS'). - :param n_samples: The number of samples to draw (Default is '1000'). Please - note that this parameter must be specified in a string fromat ('1000' and - not 1000). - :param n_tuning: String that specifies the number of iterations to adjust - the samplers's step sizes, scalings or similar (defauls is '500'). - :param n_chains: String that specifies the number of chains to sample. Defauls - is '1' for faster estimation, but note that sampling independent chains - is important for some convergence checks. - :param cores: String that specifies the number of chains to run in parallel. - (defauls is '1'). - :param Initialization method to use for auto-assigned NUTS samplers. The - defauls is 'jitter+adapt_diag' that starts with a identity mass matrix - and then adapt a diagonal based on the variance of the tuning samples - while adding a uniform jitter in [-1, 1] to the starting point in each chain. - :param target_accept: String that of a float in [0, 1] that regulates the - step size such that we approximate this acceptance rate. The defauls is '0.8' - but higher values like 0.9 or 0.95 often work better for problematic posteriors. - :param order: String that defines the order of bspline or polynomial model. - The defauls is '3'. - :param nknots: String that defines the numbers of knots for the bspline model. - The defauls is '5'. Higher values increase the model complexity with negative - effect on the spped of estimations. - :param nn_hidden_layers_num: String the specifies the number of hidden layers - in neural network model. It can be either '1' or '2'. The default is set to '2'. - :param nn_hidden_neuron_num: String that specifies the number of neurons in - the hidden layers. The defauls is set to '2'. - - """ - - def __init__(self, **kwargs): - - self.configs = dict() - self.configs['trbefile'] = kwargs.pop('trbefile', None) - self.configs['tsbefile'] = kwargs.pop('tsbefile', None) - self.configs['type'] = kwargs.pop('model_type', 'linear') - self.configs['likelihood'] = kwargs.pop('likelihood', 'Normal') - self.configs['sampler'] = kwargs.pop('sampler', 'NUTS') - self.configs['n_samples'] = int(kwargs.pop('n_samples', '1000')) - self.configs['n_tuning'] = int(kwargs.pop('n_tuning', '500')) - self.configs['n_chains'] = int(kwargs.pop('n_chains', '1')) - self.configs['cores'] = int(kwargs.pop('cores', '1')) - self.configs['init'] = kwargs.pop('init', 'jitter+adapt_diag') - self.configs['target_accept'] = float(kwargs.pop('target_accept', '0.8')) - self.configs['freedom'] = int(kwargs.pop('freedom', '1')) - self.configs['pred_type'] = kwargs.pop('pred_type', 'single') # ??? - self.configs['transferred'] = False # Specifies whether this model is transferred or not (is always False when initializing a new model). - - ### Get deprecated soon - #self.configs['skewed_likelihood'] = kwargs.pop('skewed_likelihood', 'False') == 'True' - #self.configs['random_noise'] = kwargs.pop('random_noise', 'True') == 'True' - #self.configs['hetero_noise'] = kwargs.pop('hetero_noise', 'False') == 'True' - ### End of deprecation section - - if self.configs['type'] == 'bspline': - self.configs['order'] = int(kwargs.pop('order', '3')) - self.configs['nknots'] = int(kwargs.pop('nknots', '5')) - elif self.configs['type'] == 'polynomial': - self.configs['order'] = int(kwargs.pop('order', '3')) - elif self.configs['type'] == 'nn': - self.configs['nn_hidden_layers_num'] = int(kwargs.pop('nn_hidden_layers_num', '2')) - self.configs['nn_hidden_neuron_num'] = int(kwargs.pop('nn_hidden_neuron_num', '2')) - if self.configs['nn_hidden_layers_num'] > 2 or self.configs['nn_hidden_layers_num'] <= 0: - raise ValueError("Using " + str(self.configs['nn_hidden_layers_num']) \ - + " layers was not implemented. The number of " \ - + " layers has to be either 1 or 2.") - elif self.configs['type'] == 'linear': - pass - else: - raise ValueError("Unknown model type, please specify from 'linear', \ - 'polynomial', 'bspline', or 'nn'.") - - if self.configs['type'] in ['bspline', 'polynomial', 'linear']: - - self.configs['centered'] = kwargs.pop('centered', 'False') == 'True' - self.configs['random_mu'] = kwargs.pop('random_mu', 'True') == 'True' - ######## Deprecations (remove in later version) - for c in ['mu_linear', 'linear_mu', 'random_intercept', 'random_slope']: - if c in kwargs.keys(): - print(f'The keyword {c} is deprecated. It is automatically replaced with random_mu.') - self.configs['random_mu'] = kwargs.pop(c, 'True') == 'True' - ##### End Deprecations - if self.configs['random_mu']: - self.configs['random_mu_intercept'] = kwargs.pop('random_mu_intercept', 'True') == 'True' - self.configs['random_mu_slope'] = kwargs.pop('random_mu_slope', 'True') == 'True' - - - self.configs['random_sigma'] = kwargs.pop('random_sigma', 'True') == 'True' - ######## Deprecations (remove in later version) - for c in ['sigma_linear', 'linear_sigma', 'sigma_intercept', 'random_slope']: - if c in kwargs.keys(): - print(f'The keyword {c} is deprecated. It is automatically replaced with random_mu.') - self.configs['random_sigma'] = kwargs.pop(c, 'True') == 'True' - ##### End Deprecations - if self.configs['random_sigma']: - self.configs['random_sigma_intercept'] = kwargs.pop('random_sigma_intercept', 'False') == 'True' - self.configs['random_sigma_slope'] = kwargs.pop('random_sigma_slope', 'False') == 'True' - - - for p in ['epsilon', 'delta']: - self.configs[f'random_{p}'] = kwargs.pop(f'random_{p}', 'False') == 'True' - - ######## Deprecations (remove in later version) - for c in [f'{p}_linear', f'linear_{p}','random_noise']: - if c in kwargs.keys(): - print(f'The keywords {c} is deprecated. It is automatically replaced with \'random_{p}\'') - self.configs[f'random_{p}'] = kwargs.pop(c, 'True') == 'True' - ##### End Deprecations - - if self.configs[f'random_{p}']: - self.configs[f'random_{p}_intercept'] = kwargs.pop(f'random_{p}_intercept', 'True') == 'True' - self.configs[f'random_{p}_slope'] = kwargs.pop(f'random_{p}_slope', 'True') == 'True' - - #for c in ['centered','random']: - # self.configs[f'{c}_{p}'] = kwargs.pop(f'{c}_{p}', 'False') == 'True' - # for sp in ['slope','intercept']: - # self.configs[f'{c}_{sp}_{p}'] = kwargs.pop(f'{c}_{sp}_{p}', 'False') == 'True' - - ######## Deprecations (remove in later version) - #if self.configs['linear_sigma']: - # if 'random_noise' in kwargs.keys(): - # print("The keyword \'random_noise\' is deprecated. It is now automatically replaced with \'random_intercept_sigma\', because sigma is linear") - # self.configs['random_intercept_sigma'] = kwargs.pop('random_noise','False') == 'True' - #elif 'random_noise' in kwargs.keys(): - # print("The keyword \'random_noise\' is deprecated. It is now automatically replaced with \'random_sigma\', because sigma is fixed") - # self.configs['random_sigma'] = kwargs.pop('random_noise','False') == 'True' - #if 'random_slope' in kwargs.keys(): - # print("The keyword \'random_slope\' is deprecated. It is now automatically replaced with \'random_slope_mu\'") - # self.configs['random_slope_mu'] = kwargs.pop('random_slope','False') == 'True' - ##### End Deprecations - - - self.hbr = HBR(self.configs) - - @property - def n_params(self): - return 1 - - @property - def neg_log_lik(self): - return -1 - - def estimate(self, X, y, **kwargs): - - trbefile = kwargs.pop('trbefile', None) - if trbefile is not None: - batch_effects_train = fileio.load(trbefile) - else: - print('Could not find batch-effects file! Initilizing all as zeros ...') - batch_effects_train = np.zeros([X.shape[0], 1]) - - self.hbr.estimate(X, y, batch_effects_train) - - return self - - def predict(self, Xs, X=None, Y=None, **kwargs): - - tsbefile = kwargs.pop('tsbefile', None) - if tsbefile is not None: - batch_effects_test = fileio.load(tsbefile) - else: - print('Could not find batch-effects file! Initilizing all as zeros ...') - batch_effects_test = np.zeros([Xs.shape[0], 1]) - - pred_type = self.configs['pred_type'] - - if self.configs['transferred'] == False: - yhat, s2 = self.hbr.predict(Xs, batch_effects_test, pred=pred_type) - else: - raise ValueError("This is a transferred model. Please use predict_on_new_sites function.") - - return yhat.squeeze(), s2.squeeze() - - def estimate_on_new_sites(self, X, y, batch_effects): - self.hbr.estimate_on_new_site(X, y, batch_effects) - self.configs['transferred'] = True - return self - - def predict_on_new_sites(self, X, batch_effects): - - yhat, s2 = self.hbr.predict_on_new_site(X, batch_effects) - return yhat, s2 - - def extend(self, X, y, batch_effects, X_dummy_ranges=[[0.1, 0.9, 0.01]], - merge_batch_dim=0, samples=10, informative_prior=False): - - X_dummy, batch_effects_dummy = self.hbr.create_dummy_inputs(X_dummy_ranges) - - X_dummy, batch_effects_dummy, Y_dummy = self.hbr.generate(X_dummy, - batch_effects_dummy, samples) - - batch_effects[:, merge_batch_dim] = batch_effects[:, merge_batch_dim] + \ - np.max(batch_effects_dummy[:, merge_batch_dim]) + 1 - - if informative_prior: - self.hbr.adapt(np.concatenate((X_dummy, X)), - np.concatenate((Y_dummy, y)), - np.concatenate((batch_effects_dummy, batch_effects))) - else: - self.hbr.estimate(np.concatenate((X_dummy, X)), - np.concatenate((Y_dummy, y)), - np.concatenate((batch_effects_dummy, batch_effects))) - - return self - - def tune(self, X, y, batch_effects, X_dummy_ranges=[[0.1, 0.9, 0.01]], - merge_batch_dim=0, samples=10, informative_prior=False): - - tune_ids = list(np.unique(batch_effects[:, merge_batch_dim])) - - X_dummy, batch_effects_dummy = self.hbr.create_dummy_inputs(X_dummy_ranges) - - for idx in tune_ids: - X_dummy = X_dummy[batch_effects_dummy[:, merge_batch_dim] != idx, :] - batch_effects_dummy = batch_effects_dummy[batch_effects_dummy[:, merge_batch_dim] != idx, :] - - X_dummy, batch_effects_dummy, Y_dummy = self.hbr.generate(X_dummy, - batch_effects_dummy, samples) - - if informative_prior: - self.hbr.adapt(np.concatenate((X_dummy, X)), - np.concatenate((Y_dummy, y)), - np.concatenate((batch_effects_dummy, batch_effects))) - else: - self.hbr.estimate(np.concatenate((X_dummy, X)), - np.concatenate((Y_dummy, y)), - np.concatenate((batch_effects_dummy, batch_effects))) - - return self - - def merge(self, nm, X_dummy_ranges=[[0.1, 0.9, 0.01]], merge_batch_dim=0, - samples=10): - - X_dummy1, batch_effects_dummy1 = self.hbr.create_dummy_inputs(X_dummy_ranges) - X_dummy2, batch_effects_dummy2 = nm.hbr.create_dummy_inputs(X_dummy_ranges) - - X_dummy1, batch_effects_dummy1, Y_dummy1 = self.hbr.generate(X_dummy1, - batch_effects_dummy1, samples) - X_dummy2, batch_effects_dummy2, Y_dummy2 = nm.hbr.generate(X_dummy2, - batch_effects_dummy2, samples) - - batch_effects_dummy2[:, merge_batch_dim] = batch_effects_dummy2[:, merge_batch_dim] + \ - np.max(batch_effects_dummy1[:, merge_batch_dim]) + 1 - - self.hbr.estimate(np.concatenate((X_dummy1, X_dummy2)), - np.concatenate((Y_dummy1, Y_dummy2)), - np.concatenate((batch_effects_dummy1, - batch_effects_dummy2))) - - return self - - def generate(self, X, batch_effects, samples=10): - - X, batch_effects, generated_samples = self.hbr.generate(X, batch_effects, - samples) - return X, batch_effects, generated_samples From 4f6cc2460f4608e0962e97a39b82ae256b3ff4d6 Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Sun, 19 Feb 2023 14:36:09 +0100 Subject: [PATCH 20/36] Delete norm_np1.py --- pcntoolkit/normative_model/norm_np1.py | 229 ------------------------- 1 file changed, 229 deletions(-) delete mode 100644 pcntoolkit/normative_model/norm_np1.py diff --git a/pcntoolkit/normative_model/norm_np1.py b/pcntoolkit/normative_model/norm_np1.py deleted file mode 100644 index 7b632522..00000000 --- a/pcntoolkit/normative_model/norm_np1.py +++ /dev/null @@ -1,229 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Fri Nov 22 14:41:07 2019 - -@author: seykia -""" - -from __future__ import print_function -from __future__ import division - -import os -import sys -import numpy as np -import torch -from torch import nn, optim -from torch.nn import functional as F -from sklearn.linear_model import LinearRegression -from sklearn.preprocessing import MinMaxScaler -import pickle - -try: # run as a package if installed - from pcntoolkit.normative_model.normbase import NormBase - from pcntoolkit.model.NPR import NPR, np_loss -except ImportError: - pass - - path = os.path.abspath(os.path.dirname(__file__)) - if path not in sys.path: - sys.path.append(path) - del path - - from model.NPR import NPR, np_loss - from norm_base import NormBase - -class struct(object): - pass - -class Encoder(nn.Module): - def __init__(self, x, y, args): - super(Encoder, self).__init__() - self.r_dim = args.r_dim - self.z_dim = args.z_dim - self.hidden_neuron_num = args.hidden_neuron_num - self.h_1 = nn.Linear(x.shape[1] + y.shape[1], self.hidden_neuron_num) - self.h_2 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) - self.h_3 = nn.Linear(self.hidden_neuron_num, self.r_dim) - - def forward(self, x, y): - x_y = torch.cat([x, y], dim=2) - x_y = F.relu(self.h_1(x_y)) - x_y = F.relu(self.h_2(x_y)) - x_y = F.relu(self.h_3(x_y)) - r = torch.mean(x_y, dim=1) - return r - - -class Decoder(nn.Module): - def __init__(self, x, y, args): - super(Decoder, self).__init__() - self.r_dim = args.r_dim - self.z_dim = args.z_dim - self.hidden_neuron_num = args.hidden_neuron_num - - self.g_1 = nn.Linear(self.z_dim, self.hidden_neuron_num) - self.g_2 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) - self.g_3 = nn.Linear(self.hidden_neuron_num, y.shape[1]) - - self.g_1_84 = nn.Linear(self.z_dim, self.hidden_neuron_num) - self.g_2_84 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) - self.g_3_84 = nn.Linear(self.hidden_neuron_num, y.shape[1]) - - def forward(self, z_sample): - z_hat = F.relu(self.g_1(z_sample)) - z_hat = F.relu(self.g_2(z_hat)) - y_hat = torch.sigmoid(self.g_3(z_hat)) - - z_hat_84 = F.relu(self.g_1(z_sample)) - z_hat_84 = F.relu(self.g_2_84(z_hat_84)) - y_hat_84 = torch.sigmoid(self.g_3_84(z_hat_84)) - - return y_hat, y_hat_84 - - - - -class NormNP(NormBase): - """ Classical GPR-based normative modelling approach - """ - - def __init__(self, X, y, configparam=None): - self.configparam = configparam - if configparam is not None: - with open(configparam, 'rb') as handle: - config = pickle.load(handle) - args = struct() - if 'batch_size' in config: - args.batch_size = config['batch_size'] - else: - args.batch_size = 10 - if 'epochs' in config: - args.epochs = config['epochs'] - else: - args.epochs = 100 - if 'device' in config: - args.device = config['device'] - else: - args.device = torch.device('cpu') - if 'm' in config: - args.m = config['m'] - else: - args.m = 200 - if 'hidden_neuron_num' in config: - args.hidden_neuron_num = config['hidden_neuron_num'] - else: - args.hidden_neuron_num = 10 - if 'r_dim' in config: - args.r_dim = config['r_dim'] - else: - args.r_dim = 5 - if 'z_dim' in config: - args.z_dim = config['z_dim'] - else: - args.z_dim = 3 - if 'nv' in config: - args.nv = config['nv'] - else: - args.nv = 0.01 - else: - args = struct() - args.batch_size = 10 - args.epochs = 100 - args.device = torch.device('cpu') - args.m = 200 - args.hidden_neuron_num = 10 - args.r_dim = 5 - args.z_dim = 3 - args.nv = 0.01 - - if y is not None: - if y.ndim == 1: - y = y.reshape(-1,1) - self.args = args - self.encoder = Encoder(X, y, args) - self.decoder = Decoder(X, y, args) - self.model = NPR(self.encoder, self.decoder, args) - - - @property - def n_params(self): - return 1 - - @property - def neg_log_lik(self): - return -1 - - def estimate(self, X, y): - if y.ndim == 1: - y = y.reshape(-1,1) - sample_num = X.shape[0] - batch_size = self.args.batch_size - factor_num = self.args.m - mini_batch_num = int(np.floor(sample_num/batch_size)) - device = self.args.device - - self.scaler = MinMaxScaler() - y = self.scaler.fit_transform(y) - - self.reg = [] - for i in range(factor_num): - self.reg.append(LinearRegression()) - idx = np.random.randint(0, sample_num, sample_num)#int(sample_num/10)) - self.reg[i].fit(X[idx,:],y[idx,:]) - - x_context = np.zeros([sample_num, factor_num, X.shape[1]]) - y_context = np.zeros([sample_num, factor_num, 1]) - - s = X.std(axis=0) - for j in range(factor_num): - x_context[:,j,:] = X + np.sqrt(self.args.nv) * s * np.random.randn(X.shape[0], X.shape[1]) - y_context[:,j,:] = self.reg[j].predict(x_context[:,j,:]) - - x_context = torch.tensor(x_context, device=device, dtype = torch.float) - y_context = torch.tensor(y_context, device=device, dtype = torch.float) - - x_all = torch.tensor(np.expand_dims(X,axis=1), device=device, dtype = torch.float) - y_all = torch.tensor(y.reshape(-1, 1, y.shape[1]), device=device, dtype = torch.float) - - self.model.train() - epochs = [int(self.args.epochs/4),int(self.args.epochs/2),int(self.args.epochs/5), - int(self.args.epochs-self.args.epochs/4-self.args.epochs/2-self.args.epochs/5)] - k = 1 - for e in range(len(epochs)): - optimizer = optim.Adam(self.model.parameters(), lr=10**(-e-2)) - for j in range(epochs[e]): - train_loss = 0 - for i in range(mini_batch_num): - optimizer.zero_grad() - idx = np.arange(i*batch_size,(i+1)*batch_size) - y_hat, y_hat_84, z_all, z_context, dummy, dummy = self.model(x_context[idx,:,:], y_context[idx,:,:], x_all[idx,:,:], y_all[idx,:,:]) - loss = np_loss(y_hat, y_hat_84, y_all[idx,0,:], z_all, z_context) - loss.backward() - train_loss += loss.item() - optimizer.step() - print('Epoch: %d, Loss:%f' %( k, train_loss)) - k += 1 - return self - - def predict(self, Xs, X=None, Y=None, theta=None): - sample_num = Xs.shape[0] - factor_num = self.args.m - x_context_test = np.zeros([sample_num, factor_num, Xs.shape[1]]) - y_context_test = np.zeros([sample_num, factor_num, 1]) - for j in range(factor_num): - x_context_test[:,j,:] = Xs - y_context_test[:,j,:] = self.reg[j].predict(x_context_test[:,j,:]) - x_context_test = torch.tensor(x_context_test, device=self.args.device, dtype = torch.float) - y_context_test = torch.tensor(y_context_test, device=self.args.device, dtype = torch.float) - self.model.eval() - with torch.no_grad(): - y_hat, y_hat_84, z_all, z_context, y_sigma, y_sigma_84 = self.model(x_context_test, y_context_test, n = 100) - - y_hat = self.scaler.inverse_transform(y_hat.cpu().numpy()) - y_hat_84 = self.scaler.inverse_transform(y_hat_84.cpu().numpy()) - y_sigma = y_sigma.cpu().numpy() * (self.scaler.data_max_ - self.scaler.data_min_) - y_sigma_84 = y_sigma_84.cpu().numpy() * (self.scaler.data_max_ - self.scaler.data_min_) - sigma_al = y_hat - y_hat_84 - return y_hat.squeeze(), (y_sigma**2 + sigma_al**2).squeeze() #, z_context[0].cpu().numpy(), z_context[1].cpu().numpy() - \ No newline at end of file From e7323b9ad511a25c7d09a86d735e94d9c6c1b8b4 Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Sun, 19 Feb 2023 14:36:20 +0100 Subject: [PATCH 21/36] Delete norm_hbr_b.py --- pcntoolkit/normative_model/norm_hbr_b.py | 146 ----------------------- 1 file changed, 146 deletions(-) delete mode 100644 pcntoolkit/normative_model/norm_hbr_b.py diff --git a/pcntoolkit/normative_model/norm_hbr_b.py b/pcntoolkit/normative_model/norm_hbr_b.py deleted file mode 100644 index a7062a5b..00000000 --- a/pcntoolkit/normative_model/norm_hbr_b.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Thu Jul 25 17:01:24 2019 - -@author: seykia -""" - -from __future__ import print_function -from __future__ import division - -import numpy as np - -from pcntoolkit.fileio import fileio -from pcntoolkit.normative_model.norm_base import NormBase -from pcntoolkit.model.hbr import HBR - - -class NormHBR(NormBase): - """ Classical GPR-based normative modelling approach - """ - - def __init__(self, **kwargs): - - self.configs = dict() - self.configs['transferred'] = False - self.configs['trbefile'] = kwargs.pop('trbefile',None) - self.configs['tsbefile'] = kwargs.pop('tsbefile',None) - self.configs['type'] = kwargs.pop('model_type', 'linear') - self.configs['skewed_likelihood'] = kwargs.pop('skewed_likelihood', 'False') == 'True' - self.configs['pred_type'] = kwargs.pop('pred_type', 'single') - self.configs['random_noise'] = kwargs.pop('random_noise', 'True') == 'True' - self.configs['n_samples'] = int(kwargs.pop('n_samples', '1000')) - self.configs['n_tuning'] = int(kwargs.pop('n_tuning', '500')) - self.configs['n_chains'] = int(kwargs.pop('n_chains', '1')) - self.configs['target_accept'] = float(kwargs.pop('target_accept', '0.8')) - self.configs['init'] = kwargs.pop('init', 'jitter+adapt_diag') - self.configs['cores'] = int(kwargs.pop('cores', '1')) - self.configs['freedom'] = int(kwargs.pop('freedom', '1')) - - if self.configs['type'] == 'bspline': - self.configs['order'] = int(kwargs.pop('order', '3')) - self.configs['nknots'] = int(kwargs.pop('nknots', '5')) - self.configs['random_intercept'] = kwargs.pop('random_intercept', 'True') == 'True' - self.configs['random_slope'] = kwargs.pop('random_slope', 'True') == 'True' - elif self.configs['type'] == 'polynomial': - self.configs['order'] = int(kwargs.pop('order', '3')) - self.configs['random_intercept'] = kwargs.pop('random_intercept', 'True') == 'True' - self.configs['random_slope'] = kwargs.pop('random_slope', 'True') == 'True' - elif self.configs['type'] == 'nn': - self.configs['nn_hidden_neuron_num'] = int(kwargs.pop('nn_hidden_neuron_num', '2')) - self.configs['nn_hidden_layers_num'] = int(kwargs.pop('nn_hidden_layers_num', '2')) - if self.configs['nn_hidden_layers_num'] > 2: - raise ValueError("Using " + str(self.configs['nn_hidden_layers_num']) \ - + " layers was not implemented. The number of " \ - + " layers has to be less than 3.") - elif self.configs['type'] == 'linear': - self.configs['random_intercept'] = kwargs.pop('random_intercept', 'True') == 'True' - self.configs['random_slope'] = kwargs.pop('random_slope', 'True') == 'True' - else: - raise ValueError("Unknown model type, please specify from 'linear', \ - 'polynomial', 'bspline', or 'nn'.") - - if self.configs['random_noise']: - self.configs['hetero_noise'] = kwargs.pop('hetero_noise', 'False') == 'True' - - self.hbr = HBR(self.configs) - - @property - def n_params(self): - return 1 - - @property - def neg_log_lik(self): - return -1 - - - def estimate(self, X, y, **kwargs): - - trbefile = kwargs.pop('trbefile', None) - if trbefile is not None: - batch_effects_train = fileio.load(trbefile) - else: - print('Could not find batch-effects file! Initilizing all as zeros ...') - batch_effects_train = np.zeros([X.shape[0],1]) - - self.hbr.estimate(X, y, batch_effects_train) - - return self - - - def predict(self, Xs, X=None, Y=None, **kwargs): - - tsbefile = kwargs.pop('tsbefile', None) - if tsbefile is not None: - batch_effects_test = fileio.load(tsbefile) - else: - print('Could not find batch-effects file! Initilizing all as zeros ...') - batch_effects_test = np.zeros([Xs.shape[0],1]) - - pred_type = self.configs['pred_type'] - - if self.configs['transferred'] == False: - yhat, s2 = self.hbr.predict(Xs, batch_effects_test, pred = pred_type) - else: - raise ValueError("This is a transferred model. Please use predict_on_new_sites function.") - - return yhat.squeeze(), s2.squeeze() - - - def estimate_on_new_sites(self, X, y, batch_effects): - - self.hbr.estimate_on_new_site(X, y, batch_effects) - self.configs['transferred'] = True - return self - - - def predict_on_new_sites(self, X, batch_effects): - - yhat, s2 = self.hbr.predict_on_new_site(X, batch_effects) - return yhat, s2 - - - def extend(self, X, y, batch_effects, X_dummy, batch_effects_dummy, - samples=10, informative_prior=False): - - X_dummy, batch_effects_dummy, Y_dummy = self.hbr.generate(X_dummy, - batch_effects_dummy, samples) - if informative_prior: - self.hbr.estimate_on_new_sites(np.concatenate((X_dummy, X)), - np.concatenate((Y_dummy, y)), - np.concatenate((batch_effects_dummy, batch_effects))) - else: - self.hbr.estimate(np.concatenate((X_dummy, X)), - np.concatenate((Y_dummy, y)), - np.concatenate((batch_effects_dummy, batch_effects))) - - return self - - - def generate(self, X, batch_effects, samples=10): - - X, batch_effects, generated_samples = self.hbr.generate(X, batch_effects, - samples) - return X, batch_effects, generated_samples - \ No newline at end of file From 7cd4fd99a2c0d21052b0140bf5596f4446295adf Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Sun, 19 Feb 2023 14:37:23 +0100 Subject: [PATCH 22/36] Delete NPR1.py --- pcntoolkit/model/NPR1.py | 80 ---------------------------------------- 1 file changed, 80 deletions(-) delete mode 100644 pcntoolkit/model/NPR1.py diff --git a/pcntoolkit/model/NPR1.py b/pcntoolkit/model/NPR1.py deleted file mode 100644 index 238e6563..00000000 --- a/pcntoolkit/model/NPR1.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Fri Nov 22 14:32:37 2019 - -@author: seykia -""" - -import torch -from torch import nn -from torch.nn import functional as F - -##################################### NP Model ################################ - -class NPR(nn.Module): - def __init__(self, encoder, decoder, args): - super(NPR, self).__init__() - self.r_dim = encoder.r_dim - self.z_dim = encoder.z_dim - self.encoder = encoder - self.decoder = decoder - self.r_to_z_mean = nn.Linear(self.r_dim, self.z_dim) - self.r_to_z_logvar = nn.Linear(self.r_dim, self.z_dim) - self.device = args.device - - def xy_to_z_params(self, x, y): - r = self.encoder.forward(x, y) - mu = self.r_to_z_mean(r) - logvar = self.r_to_z_logvar(r) - return mu, logvar - - def reparameterise(self, z): - mu, logvar = z - std = torch.exp(0.5 * logvar) - eps = torch.randn_like(std) - z_sample = eps.mul(std).add_(mu) - return z_sample - - def forward(self, x_context, y_context, x_all=None, y_all=None, n = 10): - y_sigma = None - y_sigma_84 = None - z_context = self.xy_to_z_params(x_context, y_context) - if self.training: - z_all = self.xy_to_z_params(x_all, y_all) - z_sample = self.reparameterise(z_all) - y_hat, y_hat_84 = self.decoder.forward(z_sample) - else: - z_all = z_context - temp = torch.zeros([n,y_context.shape[0], y_context.shape[2]], device = self.device) - temp_84 = torch.zeros([n,y_context.shape[0], y_context.shape[2]], device = self.device) - for i in range(n): - z_sample = self.reparameterise(z_all) - temp[i,:], temp_84[i,:] = self.decoder.forward(z_sample) - y_hat = torch.mean(temp, dim=0).to(self.device) - y_hat_84 = torch.mean(temp_84, dim=0).to(self.device) - if n > 1: - y_sigma = torch.std(temp, dim=0).to(self.device) - y_sigma_84 = torch.std(temp_84, dim=0).to(self.device) - return y_hat, y_hat_84, z_all, z_context, y_sigma, y_sigma_84 - -############################################################################### - -def kl_div_gaussians(mu_q, logvar_q, mu_p, logvar_p): - var_p = torch.exp(logvar_p) - kl_div = (torch.exp(logvar_q) + (mu_q - mu_p) ** 2) / (var_p) \ - - 1.0 \ - + logvar_p - logvar_q - kl_div = 0.5 * kl_div.sum() - return kl_div - -def np_loss(y_hat, y_hat_84, y, z_all, z_context): - #PBL = pinball_loss(y, y_hat, 0.05) - BCE = F.binary_cross_entropy(torch.squeeze(y_hat), torch.mean(y,dim=1), reduction="sum") - #idx1 = (y >= y_hat_84).squeeze() - #idx2 = (y < y_hat_84).squeeze() - #BCE84 = 0.84 * F.binary_cross_entropy(torch.squeeze(y_hat_84[idx1,:]), torch.mean(y[idx1,:],dim=1), reduction="sum") + \ - # 0.16 * F.binary_cross_entropy(torch.squeeze(y_hat_84[idx2,:]), torch.mean(y[idx2,:],dim=1), reduction="sum") - KLD = kl_div_gaussians(z_all[0], z_all[1], z_context[0], z_context[1]) - return BCE + KLD #`+ BCE84 - From e732ac53a88989eb11a65f58e8754b61b742cb2a Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Sun, 19 Feb 2023 14:37:35 +0100 Subject: [PATCH 23/36] Delete NP_configs.pkl --- pcntoolkit/model/NP_configs.pkl | Bin 142 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pcntoolkit/model/NP_configs.pkl diff --git a/pcntoolkit/model/NP_configs.pkl b/pcntoolkit/model/NP_configs.pkl deleted file mode 100644 index 9ee21c470cd92b943b394263d3848c7449a0db4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 142 zcmZo*ncB<%0ku;!dbpAjOOi9 Date: Sun, 19 Feb 2023 14:38:02 +0100 Subject: [PATCH 24/36] Delete hbr1.py --- pcntoolkit/model/hbr1.py | 936 --------------------------------------- 1 file changed, 936 deletions(-) delete mode 100755 pcntoolkit/model/hbr1.py diff --git a/pcntoolkit/model/hbr1.py b/pcntoolkit/model/hbr1.py deleted file mode 100755 index bf824799..00000000 --- a/pcntoolkit/model/hbr1.py +++ /dev/null @@ -1,936 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Thu Jul 25 13:23:15 2019 - -@author: seykia -@author: augub -""" - -from __future__ import print_function -from __future__ import division -from ast import Param -from tkinter.font import names - -import numpy as np -import pymc3 as pm -import theano -from itertools import product -from functools import reduce - -import theano -from pymc3 import Metropolis, NUTS, Slice, HamiltonianMC -from scipy import stats -import bspline -from bspline import splinelab - -from model.SHASH import SHASHo2, SHASHb, SHASHo -from util.utils import create_poly_basis -from util.utils import expand_all -from pcntoolkit.util.utils import cartesian_product - -from theano import printing, function - -def bspline_fit(X, order, nknots): - feature_num = X.shape[1] - bsp_basis = [] - - for i in range(feature_num): - minx = np.min(X[:,i]) - maxx = np.max(X[:,i]) - delta = maxx-minx - # Expand range by 20% (10% on both sides) - splinemin = minx-0.1*delta - splinemax = maxx+0.1*delta - knots = np.linspace(splinemin, splinemax, nknots) - k = splinelab.augknt(knots, order) - bsp_basis.append(bspline.Bspline(k, order)) - - return bsp_basis - -def bspline_transform(X, bsp_basis): - if type(bsp_basis) != list: - temp = [] - temp.append(bsp_basis) - bsp_basis = temp - - feature_num = len(bsp_basis) - X_transformed = [] - for f in range(feature_num): - X_transformed.append(np.array([bsp_basis[f](i) for i in X[:, f]])) - X_transformed = np.concatenate(X_transformed, axis=1) - - return X_transformed - -def create_poly_basis(X, order): - """ compute a polynomial basis expansion of the specified order""" - - if len(X.shape) == 1: - X = X[:, np.newaxis] - D = X.shape[1] - Phi = np.zeros((X.shape[0], D * order)) - colid = np.arange(0, D) - for d in range(1, order + 1): - Phi[:, colid] = X ** d - colid += D - - return Phi - - -def from_posterior(param, samples, distribution=None, half=False, freedom=1): - if len(samples.shape) > 1: - shape = samples.shape[1:] - else: - shape = None - - if (distribution is None): - smin, smax = np.min(samples), np.max(samples) - width = smax - smin - x = np.linspace(smin, smax, 1000) - y = stats.gaussian_kde(np.ravel(samples))(x) - if half: - x = np.concatenate([x, [x[-1] + 0.1 * width]]) - y = np.concatenate([y, [0]]) - else: - x = np.concatenate([[x[0] - 0.1 * width], x, [x[-1] + 0.1 * width]]) - y = np.concatenate([[0], y, [0]]) - if shape is None: - return pm.distributions.Interpolated(param, x, y) - else: - return pm.distributions.Interpolated(param, x, y, shape=shape) - elif (distribution == 'normal'): - temp = stats.norm.fit(samples) - if shape is None: - return pm.Normal(param, mu=temp[0], sigma=freedom * temp[1]) - else: - return pm.Normal(param, mu=temp[0], sigma=freedom * temp[1], shape=shape) - elif (distribution == 'hnormal'): - temp = stats.halfnorm.fit(samples) - if shape is None: - return pm.HalfNormal(param, sigma=freedom * temp[1]) - else: - return pm.HalfNormal(param, sigma=freedom * temp[1], shape=shape) - elif (distribution == 'hcauchy'): - temp = stats.halfcauchy.fit(samples) - if shape is None: - return pm.HalfCauchy(param, freedom * temp[1]) - else: - return pm.HalfCauchy(param, freedom * temp[1], shape=shape) - elif (distribution == 'uniform'): - upper_bound = np.percentile(samples, 95) - lower_bound = np.percentile(samples, 5) - r = np.abs(upper_bound - lower_bound) - if shape is None: - return pm.Uniform(param, lower=lower_bound - freedom * r, - upper=upper_bound + freedom * r) - else: - return pm.Uniform(param, lower=lower_bound - freedom * r, - upper=upper_bound + freedom * r, shape=shape) - elif (distribution == 'huniform'): - upper_bound = np.percentile(samples, 95) - lower_bound = np.percentile(samples, 5) - r = np.abs(upper_bound - lower_bound) - if shape is None: - return pm.Uniform(param, lower=0, upper=upper_bound + freedom * r) - else: - return pm.Uniform(param, lower=0, upper=upper_bound + freedom * r, shape=shape) - - elif (distribution == 'gamma'): - alpha_fit, loc_fit, invbeta_fit = stats.gamma.fit(samples) - if shape is None: - return pm.Gamma(param, alpha=freedom * alpha_fit, beta=freedom / invbeta_fit) - else: - return pm.Gamma(param, alpha=freedom * alpha_fit, beta=freedom / invbeta_fit, shape=shape) - - elif (distribution == 'igamma'): - alpha_fit, loc_fit, beta_fit = stats.gamma.fit(samples) - if shape is None: - return pm.InverseGamma(param, alpha=freedom * alpha_fit, beta=freedom * beta_fit) - else: - return pm.InverseGamma(param, alpha=freedom * alpha_fit, beta=freedom * beta_fit, shape=shape) - - -def hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): - """ - :param X: [N×P] array of clinical covariates - :param y: [N×1] array of neuroimaging measures - :param batch_effects: [N×M] array of batch effects - :param batch_effects_size: [b1, b2,...,bM] List of counts of unique values of batch effects - :param configs: - :param trace: - :param return_shared_variables: If true, returns references to the shared variables. The values of the shared variables can be set manually, allowing running the same model on different data without re-compiling it. - :return: - """ - X = theano.shared(X) - X = theano.tensor.cast(X,'floatX') - y = theano.shared(y) - y = theano.tensor.cast(y,'floatX') - - - with pm.Model() as model: - - # Make a param builder that will make the correct calls - pb = ParamBuilder(model, X, y, batch_effects, trace, configs) - - if configs['likelihood'] == 'Normal': - mu = pb.make_param("mu").get_samples(pb) - sigma = pb.make_param("sigma").get_samples(pb) - sigma_plus = pm.math.log(1+pm.math.exp(sigma)) - y_like = pm.Normal('y_like',mu=mu, sigma=sigma_plus, observed=y) - - elif configs['likelihood'] in ['SHASHb','SHASHo','SHASHo2']: - """ - Comment 1 - The current parameterizations are tuned towards standardized in- and output data. - It is possible to adjust the priors through the XXX_dist and XXX_params kwargs, like here we do with epsilon_params. - Supported distributions are listed in the Prior class. - - Comment 2 - Any mapping that is applied here after sampling should also be applied in util.hbr_utils.forward in order for the functions there to properly work. - For example, the softplus applied to sigma here is also applied in util.hbr_utils.forward - """ - SHASH_map = {'SHASHb':SHASHb,'SHASHo':SHASHo,'SHASHo2':SHASHo2} - - mu = pb.make_param("mu", slope_mu_params = (0.,3.), mu_intercept_mu_params=(0.,1.), sigma_intercept_mu_params = (1.,)).get_samples(pb) - sigma = pb.make_param("sigma", sigma_params = (1.,2.), slope_sigma_params=(0.,1.), intercept_sigma_params = (1., 1.)).get_samples(pb) - sigma_plus = pm.math.log(1+pm.math.exp(sigma)) - epsilon = pb.make_param("epsilon", epsilon_params = (0.,1.), slope_epsilon_params=(0.,1.), intercept_epsilon_params=(0.,1)).get_samples(pb) - delta = pb.make_param("delta", delta_params=(1.5,2.), slope_delta_params=(0.,1), intercept_delta_params=(2., 1)).get_samples(pb) - delta_plus = pm.math.log(1+pm.math.exp(delta)) + 0.3 - y_like = SHASH_map[configs['likelihood']]('y_like', mu=mu, sigma=sigma_plus, epsilon=epsilon, delta=delta_plus, observed = y) - - return model - - - -class HBR: - - """Hierarchical Bayesian Regression for normative modeling - - Basic usage:: - - model = HBR(configs) - trace = model.estimate(X, y, batch_effects) - ys,s2 = model.predict(X, batch_effects) - - where the variables are - - :param configs: a dictionary of model configurations. - :param X: N-by-P input matrix of P features for N subjects - :param y: N-by-1 vector of outputs. - :param batch_effects: N-by-B matrix of B batch ids for N subjects. - - :returns: * ys - predictive mean - * s2 - predictive variance - - Written by S.M. Kia - """ - - def get_step_methods(self, m): - """ - This can be used to assign default step functions. However, the nuts initialization keyword doesnt work together with this... so better not use it. - - STEP_METHODS = ( - NUTS, - HamiltonianMC, - Metropolis, - BinaryMetropolis, - BinaryGibbsMetropolis, - Slice, - CategoricalGibbsMetropolis, - ) - :param m: a PyMC3 model - :return: - """ - samplermap = {'NUTS': NUTS, 'MH': Metropolis, 'Slice': Slice, 'HMC': HamiltonianMC} - fallbacks = [Metropolis] # We are using MH as a fallback method here - if self.configs['sampler'] == 'NUTS': - step_kwargs = {'nuts': {'target_accept': self.configs['target_accept']}} - else: - step_kwargs = None - return pm.sampling.assign_step_methods(m, methods=[samplermap[self.configs['sampler']]] + fallbacks, - step_kwargs=step_kwargs) - - def __init__(self, configs): - self.bsp = None - self.model_type = configs['type'] - self.configs = configs - - def get_modeler(self): - return {'nn': nn_hbr}.get(self.model_type, hbr) - - def transform_X(self, X): - if self.model_type == 'polynomial': - Phi = create_poly_basis(X, self.configs['order']) - elif self.model_type == 'bspline': - if self.bsp is None: - self.bsp = bspline_fit(X, self.configs['order'], self.configs['nknots']) - bspline = bspline_transform(X, self.bsp) - Phi = np.concatenate((X, bspline), axis = 1) - else: - Phi = X - return Phi - - - def find_map(self, X, y, batch_effects,method='L-BFGS-B'): - """ Function to estimate the model """ - X, y, batch_effects = expand_all(X, y, batch_effects) - - self.batch_effects_num = batch_effects.shape[1] - self.batch_effects_size = [] - for i in range(self.batch_effects_num): - self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) - - X = self.transform_X(X) - modeler = self.get_modeler() - with modeler(X, y, batch_effects, self.batch_effects_size, self.configs) as m: - self.MAP = pm.find_MAP(method=method) - return self.MAP - - def estimate(self, X, y, batch_effects): - - """ Function to estimate the model """ - X, y, batch_effects = expand_all(X, y, batch_effects) - - self.batch_effects_num = batch_effects.shape[1] - self.batch_effects_size = [] - for i in range(self.batch_effects_num): - self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) - - X = self.transform_X(X) - modeler = self.get_modeler() - with modeler(X, y, batch_effects, self.batch_effects_size, self.configs) as m: - self.trace = pm.sample(draws=self.configs['n_samples'], - tune=self.configs['n_tuning'], - chains=self.configs['n_chains'], - init=self.configs['init'], n_init=500000, - cores=self.configs['cores']) - return self.trace - - def predict(self, X, batch_effects, pred='single'): - """ Function to make predictions from the model """ - X, batch_effects = expand_all(X, batch_effects) - - samples = self.configs['n_samples'] - y = np.zeros([X.shape[0], 1]) - - if pred == 'single': - X = self.transform_X(X) - modeler = self.get_modeler() - with modeler(X, y, batch_effects, self.batch_effects_size, self.configs): - ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) - pred_mean = ppc['y_like'].mean(axis=0) - pred_var = ppc['y_like'].var(axis=0) - - return pred_mean, pred_var - - def estimate_on_new_site(self, X, y, batch_effects): - """ Function to adapt the model """ - X, y, batch_effects = expand_all(X, y, batch_effects) - - self.batch_effects_num = batch_effects.shape[1] - self.batch_effects_size = [] - for i in range(self.batch_effects_num): - self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) - - X = self.transform_X(X) - modeler = self.get_modeler() - with modeler(X, y, batch_effects, self.batch_effects_size, - self.configs, trace=self.trace) as m: - self.trace = pm.sample(self.configs['n_samples'], - tune=self.configs['n_tuning'], - chains=self.configs['n_chains'], - target_accept=self.configs['target_accept'], - init=self.configs['init'], n_init=50000, - cores=self.configs['cores']) - return self.trace - - def predict_on_new_site(self, X, batch_effects): - """ Function to make predictions from the model """ - X, batch_effects = expand_all(X, batch_effects) - - samples = self.configs['n_samples'] - y = np.zeros([X.shape[0], 1]) - - X = self.transform_X(X) - modeler = self.get_modeler() - with modeler(X, y, batch_effects, self.batch_effects_size, self.configs, trace=self.trace): - ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) - pred_mean = ppc['y_like'].mean(axis=0) - pred_var = ppc['y_like'].var(axis=0) - - return pred_mean, pred_var - - def generate(self, X, batch_effects, samples): - """ Function to generate samples from posterior predictive distribution """ - X, batch_effects = expand_all(X, batch_effects) - - y = np.zeros([X.shape[0], 1]) - - X = self.transform_X(X) - modeler = self.get_modeler() - with modeler(X, y, batch_effects, self.batch_effects_size, self.configs): - ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) - - generated_samples = np.reshape(ppc['y_like'].squeeze().T, [X.shape[0] * samples, 1]) - X = np.repeat(X, samples) - if len(X.shape) == 1: - X = np.expand_dims(X, axis=1) - batch_effects = np.repeat(batch_effects, samples, axis=0) - if len(batch_effects.shape) == 1: - batch_effects = np.expand_dims(batch_effects, axis=1) - - return X, batch_effects, generated_samples - - def sample_prior_predictive(self, X, batch_effects, samples, trace=None): - """ Function to sample from prior predictive distribution """ - - if len(X.shape) == 1: - X = np.expand_dims(X, axis=1) - if len(batch_effects.shape) == 1: - batch_effects = np.expand_dims(batch_effects, axis=1) - - self.batch_effects_num = batch_effects.shape[1] - self.batch_effects_size = [] - for i in range(self.batch_effects_num): - self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) - - y = np.zeros([X.shape[0], 1]) - - if self.model_type == 'linear': - with hbr(X, y, batch_effects, self.batch_effects_size, self.configs, - trace): - ppc = pm.sample_prior_predictive(samples=samples) - return ppc - - def get_model(self, X, y, batch_effects): - X, y, batch_effects = expand_all(X, y, batch_effects) - - self.batch_effects_num = batch_effects.shape[1] - self.batch_effects_size = [] - for i in range(self.batch_effects_num): - self.batch_effects_size.append(len(np.unique(batch_effects[:, i]))) - modeler = self.get_modeler() - X = self.transform_X(X) - return modeler(X, y, batch_effects, self.batch_effects_size, self.configs, self.trace) - - def create_dummy_inputs(self, covariate_ranges = [[0.1,0.9,0.01]]): - - arrays = [] - for i in range(len(covariate_ranges)): - arrays.append(np.arange(covariate_ranges[i][0],covariate_ranges[i][1], covariate_ranges[i][2])) - X = cartesian_product(arrays) - X_dummy = np.concatenate([X for i in range(np.prod(self.batch_effects_size))]) - - arrays = [] - for i in range(self.batch_effects_num): - arrays.append(np.arange(0, self.batch_effects_size[i])) - batch_effects = cartesian_product(arrays) - batch_effects_dummy = np.repeat(batch_effects, X.shape[0], axis=0) - - return X_dummy, batch_effects_dummy - -class Prior: - """ - A wrapper class for a PyMC3 distribution. - - creates a fitted distribution from the trace, if one is present - - overloads the __getitem__ function with something that switches between indexing or not, based on the shape - """ - def __init__(self, name, dist, params, pb, shape=(1,)) -> None: - self.dist = None - self.name = name - self.shape = shape - self.has_random_effect = True if len(shape)>1 else False - self.distmap = {'normal': pm.Normal, - 'hnormal': pm.HalfNormal, - 'gamma': pm.Gamma, - 'uniform': pm.Uniform, - 'igamma': pm.InverseGamma, - 'hcauchy': pm.HalfCauchy} - self.make_dist(dist, params, pb) - - def make_dist(self, dist, params, pb): - """This creates a pymc3 distribution. If there is a trace, the distribution is fitted to the trace. If there isn't a trace, the prior is parameterized by the values in (params)""" - with pb.model as m: - if (pb.trace is not None) and (not self.has_random_effect): - int_dist = from_posterior(param=self.name, - samples=pb.trace[self.name], - distribution=dist, - freedom=pb.configs['freedom']) - self.dist = int_dist.reshape(self.shape) - else: - shape_prod = np.product(np.array(self.shape)) - print(self.name) - print(f"dist={dist}") - print(f"params={params}") - int_dist = self.distmap[dist](self.name, *params, shape=shape_prod) - self.dist = int_dist.reshape(self.shape) - - def __getitem__(self, idx): - """The idx here is the index of the batch-effect. If the prior does not model batch effects, this should return the same value for each index""" - assert self.dist is not None, "Distribution not initialized" - if self.has_random_effect: - return self.dist[idx] - else: - return self.dist - - -class ParamBuilder: - """ - A class that simplifies the construction of parameterizations. - It has a lot of attributes necessary for creating the model, including the data, but it is never saved with the model. - It also contains a lot of decision logic for creating the parameterizations. - """ - - def __init__(self, model, X, y, batch_effects, trace, configs): - """ - - :param model: model to attach all the distributions to - :param X: Covariates - :param y: IDPs - :param batch_effects: I guess this speaks for itself - :param trace: idem - :param configs: idem - """ - self.model = model - self.X = X - self.y = y - self.batch_effects = batch_effects - self.trace = trace - self.configs = configs - - self.feature_num = X.shape[1].eval().item() - self.y_shape = y.shape.eval() - self.n_ys = y.shape[0].eval().item() - self.batch_effects_num = batch_effects.shape[1] - - self.batch_effects_size = [] - self.all_idx = [] - for i in range(self.batch_effects_num): - # Count the unique values for each batch effect - self.batch_effects_size.append(len(np.unique(self.batch_effects[:, i]))) - # Store the unique values for each batch effect - self.all_idx.append(np.int16(np.unique(self.batch_effects[:, i]))) - # Make a cartesian product of all the unique values of each batch effect - self.be_idx = list(product(*self.all_idx)) - - # Make tuples of batch effects ID's and indices of datapoints with that specific combination of batch effects - self.be_idx_tups = [] - for be in self.be_idx: - a = [] - for i, b in enumerate(be): - a.append(self.batch_effects[:, i] == b) - idx = reduce(np.logical_and, a).nonzero() - if idx[0].shape[0] != 0: - self.be_idx_tups.append((be, idx)) - - def make_param(self, name, dim = (1,), **kwargs): - - if self.configs.get(f'random_{name}'): - if self.configs.get(f'random_{name}_slope') or self.configs.get(f'random_{name}_intercept'): # Then it is a linear model - # First make a slope and intercept, and use those to make a linear parameterization - if self.configs.get(f'random_{name}_slope'): - if self.configs.get('centered'): - slope_parameterization = CentralRandomFixedParameterization(name=f'random_{name}_slope', - pb=self, dim=[self.feature_num], **kwargs) - else: - slope_parameterization = NonCentralRandomFixedParameterization(name=f'random_{name}_slope', - pb=self, dim=[self.feature_num], **kwargs) - if self.configs.get(f'random_{name}_intercept'): - if self.configs.get('centered'): - intercept_parameterization = CentralRandomFixedParameterization(name=f'random_{name}_intercept', - pb=self, dim=(1,), **kwargs) - else: - intercept_parameterization = NonCentralRandomFixedParameterization(name=f'random_{name}_intercept', - pb=self, dim=(1,), **kwargs) - return LinearParameterization(name=name, dim=dim, - slope_parameterization=slope_parameterization, - intercept_parameterization=intercept_parameterization, - pb=self, **kwargs) - else: # it is a fixed parametrization (this is used usually for sigma and maybe better to use wider dist as default.) - if self.configs.get('centered'): - return CentralRandomFixedParameterization(name=name, pb=self, dim=dim, **kwargs) - else: - return NonCentralRandomFixedParameterization(name=name, pb=self, dim=dim, **kwargs) - else: - return FixedParameterization(name=name, dim=dim, pb=self,**kwargs) - - -class Parameterization: - """ - This is the top-level parameterization class from which all the other parameterizations inherit. - """ - def __init__(self, name, dim): - self.name = name - self.dim = dim - print(name, type(self)) - - def get_samples(self, pb): - - with pb.model: - samples = theano.tensor.zeros([pb.n_ys, *self.dim]) - for be, idx in pb.be_idx_tups: - samples = theano.tensor.set_subtensor(samples[idx], self.dist[be]) - return samples - - -class FixedParameterization(Parameterization): - """ - A parameterization that takes a single value for all input. It does not depend on anything except its hyperparameters - """ - def __init__(self, name, dim, pb:ParamBuilder, **kwargs): - super().__init__(name, dim) - dist = kwargs.get(f'{name}_dist','normal') - params = kwargs.get(f'{name}_params',(0.,1.)) # should be wider I think. - self.dist = Prior(name, dist, params, pb, shape = dim) - - -class CentralRandomFixedParameterization(Parameterization): - """ - A parameterization that is fixed for each batch effect. This is sampled in a central fashion; - the values are sampled from normal distribution with a group mean and group variance - """ - def __init__(self, name, dim, pb:ParamBuilder, **kwargs): - super().__init__(name, dim) - - # Normal distribution is default for mean - mu_dist = kwargs.get(f'mu_{name}_dist','normal') - mu_params = kwargs.get(f'mu_{name}_params',(0.,1.)) - mu_prior = Prior(f'mu_{name}', mu_dist, mu_params, pb, shape = dim) - - # HalfCauchy is default for sigma - sigma_dist = kwargs.get(f'sigma_{name}_dist','hcauchy') - sigma_params = kwargs.get(f'sigma_{name}_params',(1.,)) - sigma_prior = Prior(f'sigma_{name}',sigma_dist, sigma_params, pb, shape = [*pb.batch_effects_size, *dim]) - - self.dist = pm.Normal(name=name, mu=mu_prior.dist, sigma=sigma_prior.dist, shape = [*pb.batch_effects_size, *dim]) - - -class NonCentralRandomFixedParameterization(Parameterization): - """ - A parameterization that is fixed for each batch effect. This is sampled in a non-central fashion; - the values are a sum of a group mean and noise values scaled with a group scaling factor - """ - def __init__(self, name,dim, pb:ParamBuilder, **kwargs): - super().__init__(name, dim) - - # Normal distribution is default for mean - mu_dist = kwargs.get(f'mu_{name}_dist','normal') - mu_params = kwargs.get(f'mu_{name}_params',(0.,1.)) - mu_prior = Prior(f'mu_{name}', mu_dist, mu_params, pb, shape = dim) - - # HalfCauchy is default for sigma - sigma_dist = kwargs.get(f'sigma_{name}_dist','hcauchy') - sigma_params = kwargs.get(f'sigma_{name}_params',(1.,)) - sigma_prior = Prior(f'sigma_{name}',sigma_dist, sigma_params, pb, shape = dim) - - # Normal is default for offset - offset_dist = kwargs.get(f'offset_{name}_dist','normal') - offset_params = kwargs.get(f'offset_{name}_params',(0.,1.)) - offset_prior = Prior(f'offset_{name}',offset_dist, offset_params, pb, shape = [*pb.batch_effects_size, *dim]) - - self.dist = pm.Deterministic(name=name, var=mu_prior.dist+sigma_prior.dist*offset_prior.dist) - - -class LinearParameterization(Parameterization): - """ - A parameterization that can model a linear dependence on X. - """ - def __init__(self, name, dim, slope_parameterization, intercept_parameterization, pb, **kwargs): - super().__init__( name, dim) - self.slope_parameterization = slope_parameterization - self.intercept_parameterization = intercept_parameterization - - def get_samples(self, pb:ParamBuilder): - with pb.model: - samples = theano.tensor.zeros([pb.n_ys, *self.dim]) - for be, idx in pb.be_idx_tups: - dot = theano.tensor.dot(pb.X[idx,:], self.slope_parameterization.dist[be]).T - intercept = self.intercept_parameterization.dist[be] - samples = theano.tensor.set_subtensor(samples[idx,:],dot+intercept) - return samples - - -def get_design_matrix(X, nm, basis="linear"): - if basis == "bspline": - Phi = bspline_transform(X, nm.hbr.bsp) - elif basis == "polynomial": - Phi = create_poly_basis(X, 3) - else: - Phi = X - return Phi - - - -def nn_hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): - n_hidden = configs['nn_hidden_neuron_num'] - n_layers = configs['nn_hidden_layers_num'] - feature_num = X.shape[1] - batch_effects_num = batch_effects.shape[1] - all_idx = [] - for i in range(batch_effects_num): - all_idx.append(np.int16(np.unique(batch_effects[:, i]))) - be_idx = list(product(*all_idx)) - - # Initialize random weights between each layer for the mu: - init_1 = pm.floatX(np.random.randn(feature_num, n_hidden) * np.sqrt(1 / feature_num)) - init_out = pm.floatX(np.random.randn(n_hidden) * np.sqrt(1 / n_hidden)) - - std_init_1 = pm.floatX(np.random.rand(feature_num, n_hidden)) - std_init_out = pm.floatX(np.random.rand(n_hidden)) - - # And initialize random weights between each layer for sigma_noise: - init_1_noise = pm.floatX(np.random.randn(feature_num, n_hidden) * np.sqrt(1 / feature_num)) - init_out_noise = pm.floatX(np.random.randn(n_hidden) * np.sqrt(1 / n_hidden)) - - std_init_1_noise = pm.floatX(np.random.rand(feature_num, n_hidden)) - std_init_out_noise = pm.floatX(np.random.rand(n_hidden)) - - # If there are two hidden layers, then initialize weights for the second layer: - if n_layers == 2: - init_2 = pm.floatX(np.random.randn(n_hidden, n_hidden) * np.sqrt(1 / n_hidden)) - std_init_2 = pm.floatX(np.random.rand(n_hidden, n_hidden)) - init_2_noise = pm.floatX(np.random.randn(n_hidden, n_hidden) * np.sqrt(1 / n_hidden)) - std_init_2_noise = pm.floatX(np.random.rand(n_hidden, n_hidden)) - - with pm.Model() as model: - - X = pm.Data('X', X) - y = pm.Data('y', y) - - if trace is not None: # Used when estimating/predicting on a new site - weights_in_1_grp = from_posterior('w_in_1_grp', trace['w_in_1_grp'], - distribution='normal', freedom=configs['freedom']) - - weights_in_1_grp_sd = from_posterior('w_in_1_grp_sd', trace['w_in_1_grp_sd'], - distribution='hcauchy', freedom=configs['freedom']) - - if n_layers == 2: - weights_1_2_grp = from_posterior('w_1_2_grp', trace['w_1_2_grp'], - distribution='normal', freedom=configs['freedom']) - - weights_1_2_grp_sd = from_posterior('w_1_2_grp_sd', trace['w_1_2_grp_sd'], - distribution='hcauchy', freedom=configs['freedom']) - - weights_2_out_grp = from_posterior('w_2_out_grp', trace['w_2_out_grp'], - distribution='normal', freedom=configs['freedom']) - - weights_2_out_grp_sd = from_posterior('w_2_out_grp_sd', trace['w_2_out_grp_sd'], - distribution='hcauchy', freedom=configs['freedom']) - - mu_prior_intercept = from_posterior('mu_prior_intercept', trace['mu_prior_intercept'], - distribution='normal', freedom=configs['freedom']) - sigma_prior_intercept = from_posterior('sigma_prior_intercept', trace['sigma_prior_intercept'], - distribution='hcauchy', freedom=configs['freedom']) - - else: - # Group the mean distribution for input to the hidden layer: - weights_in_1_grp = pm.Normal('w_in_1_grp', 0, sd=1, - shape=(feature_num, n_hidden), testval=init_1) - - # Group standard deviation: - weights_in_1_grp_sd = pm.HalfCauchy('w_in_1_grp_sd', 1., - shape=(feature_num, n_hidden), testval=std_init_1) - - if n_layers == 2: - # Group the mean distribution for hidden layer 1 to hidden layer 2: - weights_1_2_grp = pm.Normal('w_1_2_grp', 0, sd=1, - shape=(n_hidden, n_hidden), testval=init_2) - - # Group standard deviation: - weights_1_2_grp_sd = pm.HalfCauchy('w_1_2_grp_sd', 1., - shape=(n_hidden, n_hidden), testval=std_init_2) - - # Group the mean distribution for hidden to output: - weights_2_out_grp = pm.Normal('w_2_out_grp', 0, sd=1, - shape=(n_hidden,), testval=init_out) - - # Group standard deviation: - weights_2_out_grp_sd = pm.HalfCauchy('w_2_out_grp_sd', 1., - shape=(n_hidden,), testval=std_init_out) - - # mu_prior_intercept = pm.Uniform('mu_prior_intercept', lower=-100, upper=100) - mu_prior_intercept = pm.Normal('mu_prior_intercept', mu=0., sigma=1e3) - sigma_prior_intercept = pm.HalfCauchy('sigma_prior_intercept', 5) - - # Now create separate weights for each group, by doing - # weights * group_sd + group_mean, we make sure the new weights are - # coming from the (group_mean, group_sd) distribution. - weights_in_1_raw = pm.Normal('w_in_1', 0, sd=1, - shape=(batch_effects_size + [feature_num, n_hidden])) - weights_in_1 = weights_in_1_raw * weights_in_1_grp_sd + weights_in_1_grp - - if n_layers == 2: - weights_1_2_raw = pm.Normal('w_1_2', 0, sd=1, - shape=(batch_effects_size + [n_hidden, n_hidden])) - weights_1_2 = weights_1_2_raw * weights_1_2_grp_sd + weights_1_2_grp - - weights_2_out_raw = pm.Normal('w_2_out', 0, sd=1, - shape=(batch_effects_size + [n_hidden])) - weights_2_out = weights_2_out_raw * weights_2_out_grp_sd + weights_2_out_grp - - intercepts_offset = pm.Normal('intercepts_offset', mu=0, sd=1, - shape=(batch_effects_size)) - - intercepts = pm.Deterministic('intercepts', intercepts_offset + - mu_prior_intercept * sigma_prior_intercept) - - # Build the neural network and estimate y_hat: - y_hat = theano.tensor.zeros(y.shape) - for be in be_idx: - # Find the indices corresponding to 'group be': - a = [] - for i, b in enumerate(be): - a.append(batch_effects[:, i] == b) - idx = reduce(np.logical_and, a).nonzero() - if idx[0].shape[0] != 0: - act_1 = pm.math.tanh(theano.tensor.dot(X[idx, :], weights_in_1[be])) - if n_layers == 2: - act_2 = pm.math.tanh(theano.tensor.dot(act_1, weights_1_2[be])) - y_hat = theano.tensor.set_subtensor(y_hat[idx, 0], - intercepts[be] + theano.tensor.dot(act_2, weights_2_out[be])) - else: - y_hat = theano.tensor.set_subtensor(y_hat[idx, 0], - intercepts[be] + theano.tensor.dot(act_1, weights_2_out[be])) - - # If we want to estimate varying noise terms across groups: - if configs['random_noise']: - if configs['hetero_noise']: - if trace is not None: # # Used when estimating/predicting on a new site - weights_in_1_grp_noise = from_posterior('w_in_1_grp_noise', - trace['w_in_1_grp_noise'], - distribution='normal', freedom=configs['freedom']) - - weights_in_1_grp_sd_noise = from_posterior('w_in_1_grp_sd_noise', - trace['w_in_1_grp_sd_noise'], - distribution='hcauchy', freedom=configs['freedom']) - - if n_layers == 2: - weights_1_2_grp_noise = from_posterior('w_1_2_grp_noise', - trace['w_1_2_grp_noise'], - distribution='normal', freedom=configs['freedom']) - - weights_1_2_grp_sd_noise = from_posterior('w_1_2_grp_sd_noise', - trace['w_1_2_grp_sd_noise'], - distribution='hcauchy', freedom=configs['freedom']) - - weights_2_out_grp_noise = from_posterior('w_2_out_grp_noise', - trace['w_2_out_grp_noise'], - distribution='normal', freedom=configs['freedom']) - - weights_2_out_grp_sd_noise = from_posterior('w_2_out_grp_sd_noise', - trace['w_2_out_grp_sd_noise'], - distribution='hcauchy', freedom=configs['freedom']) - - else: - # The input layer to the first hidden layer: - weights_in_1_grp_noise = pm.Normal('w_in_1_grp_noise', 0, sd=1, - shape=(feature_num, n_hidden), - testval=init_1_noise) - weights_in_1_grp_sd_noise = pm.HalfCauchy('w_in_1_grp_sd_noise', 1, - shape=(feature_num, n_hidden), - testval=std_init_1_noise) - - # The first hidden layer to second hidden layer: - if n_layers == 2: - weights_1_2_grp_noise = pm.Normal('w_1_2_grp_noise', 0, sd=1, - shape=(n_hidden, n_hidden), - testval=init_2_noise) - weights_1_2_grp_sd_noise = pm.HalfCauchy('w_1_2_grp_sd_noise', 1, - shape=(n_hidden, n_hidden), - testval=std_init_2_noise) - - # The second hidden layer to output layer: - weights_2_out_grp_noise = pm.Normal('w_2_out_grp_noise', 0, sd=1, - shape=(n_hidden,), - testval=init_out_noise) - weights_2_out_grp_sd_noise = pm.HalfCauchy('w_2_out_grp_sd_noise', 1, - shape=(n_hidden,), - testval=std_init_out_noise) - - # mu_prior_intercept_noise = pm.HalfNormal('mu_prior_intercept_noise', sigma=1e3) - # sigma_prior_intercept_noise = pm.HalfCauchy('sigma_prior_intercept_noise', 5) - - # Now create separate weights for each group: - weights_in_1_raw_noise = pm.Normal('w_in_1_noise', 0, sd=1, - shape=(batch_effects_size + [feature_num, n_hidden])) - weights_in_1_noise = weights_in_1_raw_noise * weights_in_1_grp_sd_noise + weights_in_1_grp_noise - - if n_layers == 2: - weights_1_2_raw_noise = pm.Normal('w_1_2_noise', 0, sd=1, - shape=(batch_effects_size + [n_hidden, n_hidden])) - weights_1_2_noise = weights_1_2_raw_noise * weights_1_2_grp_sd_noise + weights_1_2_grp_noise - - weights_2_out_raw_noise = pm.Normal('w_2_out_noise', 0, sd=1, - shape=(batch_effects_size + [n_hidden])) - weights_2_out_noise = weights_2_out_raw_noise * weights_2_out_grp_sd_noise + weights_2_out_grp_noise - - # intercepts_offset_noise = pm.Normal('intercepts_offset_noise', mu=0, sd=1, - # shape=(batch_effects_size)) - - # intercepts_noise = pm.Deterministic('intercepts_noise', mu_prior_intercept_noise + - # intercepts_offset_noise * sigma_prior_intercept_noise) - - # Build the neural network and estimate the sigma_y: - sigma_y = theano.tensor.zeros(y.shape) - for be in be_idx: - a = [] - for i, b in enumerate(be): - a.append(batch_effects[:, i] == b) - idx = reduce(np.logical_and, a).nonzero() - if idx[0].shape[0] != 0: - act_1_noise = pm.math.sigmoid(theano.tensor.dot(X[idx, :], weights_in_1_noise[be])) - if n_layers == 2: - act_2_noise = pm.math.sigmoid(theano.tensor.dot(act_1_noise, weights_1_2_noise[be])) - temp = pm.math.log1pexp(theano.tensor.dot(act_2_noise, weights_2_out_noise[be])) + 1e-5 - else: - temp = pm.math.log1pexp(theano.tensor.dot(act_1_noise, weights_2_out_noise[be])) + 1e-5 - sigma_y = theano.tensor.set_subtensor(sigma_y[idx, 0], temp) - - else: # homoscedastic noise: - if trace is not None: # Used for transferring the priors - upper_bound = np.percentile(trace['sigma_noise'], 95) - sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=2 * upper_bound, shape=(batch_effects_size)) - else: - sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=100, shape=(batch_effects_size)) - - sigma_y = theano.tensor.zeros(y.shape) - for be in be_idx: - a = [] - for i, b in enumerate(be): - a.append(batch_effects[:, i] == b) - idx = reduce(np.logical_and, a).nonzero() - if idx[0].shape[0] != 0: - sigma_y = theano.tensor.set_subtensor(sigma_y[idx, 0], sigma_noise[be]) - - else: # do not allow for random noise terms across groups: - if trace is not None: # Used for transferring the priors - upper_bound = np.percentile(trace['sigma_noise'], 95) - sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=2 * upper_bound) - else: - sigma_noise = pm.Uniform('sigma_noise', lower=0, upper=100) - sigma_y = theano.tensor.zeros(y.shape) - for be in be_idx: - a = [] - for i, b in enumerate(be): - a.append(batch_effects[:, i] == b) - idx = reduce(np.logical_and, a).nonzero() - if idx[0].shape[0] != 0: - sigma_y = theano.tensor.set_subtensor(sigma_y[idx, 0], sigma_noise) - - if configs['skewed_likelihood']: - skewness = pm.Uniform('skewness', lower=-10, upper=10, shape=(batch_effects_size)) - alpha = theano.tensor.zeros(y.shape) - for be in be_idx: - a = [] - for i, b in enumerate(be): - a.append(batch_effects[:, i] == b) - idx = reduce(np.logical_and, a).nonzero() - if idx[0].shape[0] != 0: - alpha = theano.tensor.set_subtensor(alpha[idx, 0], skewness[be]) - else: - alpha = 0 # symmetrical normal distribution - - y_like = pm.SkewNormal('y_like', mu=y_hat, sigma=sigma_y, alpha=alpha, observed=y) - - return model From 59b3f7345757615ef34dc9eb881f004ee3185865 Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Sun, 19 Feb 2023 14:39:24 +0100 Subject: [PATCH 25/36] Delete temp.py --- pcntoolkit/temp.py | 67 ---------------------------------------------- 1 file changed, 67 deletions(-) delete mode 100755 pcntoolkit/temp.py diff --git a/pcntoolkit/temp.py b/pcntoolkit/temp.py deleted file mode 100755 index 80356f54..00000000 --- a/pcntoolkit/temp.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Fri Dec 31 10:21:17 2021 - -@author: seykia -""" -import sys -from subprocess import check_output -import time - -def retrieve_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 - - return jobs - - -def check_job_status(jobs): - - running_jobs = retrieve_jobs() - - r = 0 - c = 0 - q = 0 - u = 0 - for job in jobs: - 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 - - print('Total:%d, Queued: %d, Running:%d, Completed:%d, Unknown:%d' - %(len(jobs), q, r, c, u)) - return q,r,c,u - - -def check_jobs(jobs, delay=60): - - n = len(jobs) - - while(True): - q,r,c,u = check_job_status(jobs) - if c == n: - print('All jobs are finished!') - break - time.sleep(delay) - - - - - - - - \ No newline at end of file From e26866f0ca42e7513884658630437cb338ca5d72 Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Sun, 19 Feb 2023 14:40:05 +0100 Subject: [PATCH 26/36] Delete NP_configs.pkl --- pcntoolkit/NP_configs.pkl | Bin 142 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pcntoolkit/NP_configs.pkl diff --git a/pcntoolkit/NP_configs.pkl b/pcntoolkit/NP_configs.pkl deleted file mode 100644 index 336328d9b6c8b9118fe66ae5556037512ab2da37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 142 zcmZo*ncB<%0ku;!dbpAjOOi9K?X~)UwRv)G0lz bCHY0k8B^c_lc)4BCl{1XX`K=@rBn|9!8 Date: Sun, 19 Feb 2023 14:41:15 +0100 Subject: [PATCH 27/36] Delete preprocess.py --- pcntoolkit/util/preprocess.py | 101 ---------------------------------- 1 file changed, 101 deletions(-) delete mode 100755 pcntoolkit/util/preprocess.py diff --git a/pcntoolkit/util/preprocess.py b/pcntoolkit/util/preprocess.py deleted file mode 100755 index 67790449..00000000 --- a/pcntoolkit/util/preprocess.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Wed Apr 27 17:54:45 2022 - -@author: seykia -""" -import numpy as np - -class scaler: - - def __init__(self, scaler_type='None', tail=0.01): - - self.scaler_type = scaler_type - self.tail = tail - - if self.scaler_type not in ['None', 'standardize', 'minmax', 'robminmax']: - raise ValueError("Undifined scaler type!") - - - def fit(self, X): - - if self.scaler_type == 'standardize': - - self.m = np.mean(X, axis=0) - self.s = np.std(X, axis=0) - - elif self.scaler_type == 'minmax': - self.min = np.min(X, axis=0) - self.max = np.max(X, axis=0) - - elif self.scaler_type == 'robminmax': - self.min = np.zeros([X.shape[1],]) - self.max = np.zeros([X.shape[1],]) - for i in range(X.shape[1]): - self.min[i] = np.median(np.sort(X[:,i])[0:int(np.round(X.shape[0] * self.tail))]) - self.max[i] = np.median(np.sort(X[:,i])[-int(np.round(X.shape[0] * self.tail)):]) - - def transform(self, X, adjust_outliers=False): - - if self.scaler_type == 'standardize': - - X = (X - self.m) / self.s - - elif self.scaler_type in ['minmax', 'robminmax']: - - X = (X - self.min) / (self.max - self.min) - - if adjust_outliers: - - X[X < 0] = 0 - X[X > 1] = 1 - - return X - - def inverse_transform(self, X, index=None): - - if self.scaler_type == 'standardize': - if index is None: - X = X * self.s + self.m - else: - X = X * self.s[index] + self.m[index] - - elif self.scaler_type in ['minmax', 'robminmax']: - if index is None: - X = X * (self.max - self.min) + self.min - else: - X = X * (self.max[index] - self.min[index]) + self.min[index] - return X - - def fit_transform(self, X, adjust_outliers=False): - - if self.scaler_type == 'standardize': - - self.m = np.mean(X, axis=0) - self.s = np.std(X, axis=0) - X = (X - self.m) / self.s - - elif self.scaler_type == 'minmax': - - self.min = np.min(X, axis=0) - self.max = np.max(X, axis=0) - X = (X - self.min) / (self.max - self.min) - - elif self.scaler_type == 'robminmax': - - self.min = np.zeros([X.shape[1],]) - self.max = np.zeros([X.shape[1],]) - - for i in range(X.shape[1]): - self.min[i] = np.median(np.sort(X[:,i])[0:int(np.round(X.shape[0] * self.tail))]) - self.max[i] = np.median(np.sort(X[:,i])[-int(np.round(X.shape[0] * self.tail)):]) - - X = (X - self.min) / (self.max - self.min) - - if adjust_outliers: - X[X < 0] = 0 - X[X > 1] = 1 - - return X - From 364ad5ffd553ff947a91f09b34651ce497899903 Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Sun, 19 Feb 2023 14:48:33 +0100 Subject: [PATCH 28/36] minor change in defaults --- pcntoolkit/model/hbr.py | 2 +- pcntoolkit/normative_model/norm_hbr.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pcntoolkit/model/hbr.py b/pcntoolkit/model/hbr.py index 81a013c2..98c71037 100644 --- a/pcntoolkit/model/hbr.py +++ b/pcntoolkit/model/hbr.py @@ -177,7 +177,7 @@ def hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): sigma_slope_mu_params = (5.,), mu_intercept_mu_params=(0.,10.), sigma_intercept_mu_params = (5.,)).get_samples(pb) - sigma = pb.make_param("sigma", mu_sigma_params = (0., 10.), + sigma = pb.make_param("sigma", mu_sigma_params = (10., 5.), sigma_sigma_params = (5.,)).get_samples(pb) sigma_plus = pm.math.log(1+pm.math.exp(sigma)) y_like = pm.Normal('y_like',mu=mu, sigma=sigma_plus, observed=y) diff --git a/pcntoolkit/normative_model/norm_hbr.py b/pcntoolkit/normative_model/norm_hbr.py index 6aadef55..d769163e 100644 --- a/pcntoolkit/normative_model/norm_hbr.py +++ b/pcntoolkit/normative_model/norm_hbr.py @@ -199,8 +199,6 @@ def __init__(self, **kwargs): self.configs['random_slope_mu'] = kwargs.pop('random_slope_mu','True') == 'True' self.configs['random_sigma'] = kwargs.pop('random_sigma','True') == 'True' self.configs['centered_sigma'] = kwargs.pop('centered_sigma','True') == 'True' - self.configs['centered_slope_mu'] = kwargs.pop('centered_slope_mu','True') == 'True' - self.configs['centered_intercept_mu'] = kwargs.pop('centered_intercept_mu','True') == 'True' ## End default parameters self.hbr = HBR(self.configs) From ee28be3359796c2c7522a6527372946085ae9414 Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Sun, 19 Feb 2023 14:57:07 +0100 Subject: [PATCH 29/36] Back to old version. --- pcntoolkit/__init__.py | 2 - pcntoolkit/model/NPR.py | 23 +++++--- pcntoolkit/normative_model/norm_np.py | 85 +++++++++++++++++---------- 3 files changed, 70 insertions(+), 40 deletions(-) diff --git a/pcntoolkit/__init__.py b/pcntoolkit/__init__.py index 45d02964..087fe624 100644 --- a/pcntoolkit/__init__.py +++ b/pcntoolkit/__init__.py @@ -2,5 +2,3 @@ from . import normative from . import normative_parallel from . import normative_NP - -__version__ = "0.20" diff --git a/pcntoolkit/model/NPR.py b/pcntoolkit/model/NPR.py index 0482d044..6e8c1f53 100755 --- a/pcntoolkit/model/NPR.py +++ b/pcntoolkit/model/NPR.py @@ -34,26 +34,29 @@ def reparameterise(self, z): std = torch.exp(0.5 * logvar) eps = torch.randn_like(std) z_sample = eps.mul(std).add_(mu) - z_sample = z_sample.unsqueeze(1).expand(-1, 10, -1) return z_sample def forward(self, x_context, y_context, x_all=None, y_all=None, n = 10): y_sigma = None + y_sigma_84 = None z_context = self.xy_to_z_params(x_context, y_context) if self.training: z_all = self.xy_to_z_params(x_all, y_all) z_sample = self.reparameterise(z_all) - y_hat = self.decoder.forward(z_sample, x_all) + y_hat, y_hat_84 = self.decoder.forward(z_sample) else: z_all = z_context temp = torch.zeros([n,y_context.shape[0], y_context.shape[2]], device = self.device) + temp_84 = torch.zeros([n,y_context.shape[0], y_context.shape[2]], device = self.device) for i in range(n): z_sample = self.reparameterise(z_all) - temp[i,:] = self.decoder.forward(z_sample, x_all) + temp[i,:], temp_84[i,:] = self.decoder.forward(z_sample) y_hat = torch.mean(temp, dim=0).to(self.device) + y_hat_84 = torch.mean(temp_84, dim=0).to(self.device) if n > 1: y_sigma = torch.std(temp, dim=0).to(self.device) - return y_hat, z_all, z_context, y_sigma + y_sigma_84 = torch.std(temp_84, dim=0).to(self.device) + return y_hat, y_hat_84, z_all, z_context, y_sigma, y_sigma_84 ############################################################################### @@ -65,8 +68,12 @@ def kl_div_gaussians(mu_q, logvar_q, mu_p, logvar_p): kl_div = 0.5 * kl_div.sum() return kl_div -def np_loss(y_hat, y, z_all, z_context): - BCE = F.mse_loss(y_hat, y, reduction="sum") +def np_loss(y_hat, y_hat_84, y, z_all, z_context): + #PBL = pinball_loss(y, y_hat, 0.05) + BCE = F.binary_cross_entropy(torch.squeeze(y_hat), torch.mean(y,dim=1), reduction="sum") + idx1 = (y >= y_hat_84).squeeze() + idx2 = (y < y_hat_84).squeeze() + BCE84 = 0.84 * F.binary_cross_entropy(torch.squeeze(y_hat_84[idx1,:]), torch.mean(y[idx1,:],dim=1), reduction="sum") + \ + 0.16 * F.binary_cross_entropy(torch.squeeze(y_hat_84[idx2,:]), torch.mean(y[idx2,:],dim=1), reduction="sum") KLD = kl_div_gaussians(z_all[0], z_all[1], z_context[0], z_context[1]) - return BCE + KLD - + return BCE + KLD + BCE84 diff --git a/pcntoolkit/normative_model/norm_np.py b/pcntoolkit/normative_model/norm_np.py index fb03fff6..14bdd291 100755 --- a/pcntoolkit/normative_model/norm_np.py +++ b/pcntoolkit/normative_model/norm_np.py @@ -42,7 +42,7 @@ def __init__(self, x, y, args): self.r_dim = args.r_dim self.z_dim = args.z_dim self.hidden_neuron_num = args.hidden_neuron_num - self.h_1 = nn.Linear(x.shape[2] + y.shape[2], self.hidden_neuron_num) + self.h_1 = nn.Linear(x.shape[1] + y.shape[1], self.hidden_neuron_num) self.h_2 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) self.h_3 = nn.Linear(self.hidden_neuron_num, self.r_dim) @@ -62,17 +62,24 @@ def __init__(self, x, y, args): self.z_dim = args.z_dim self.hidden_neuron_num = args.hidden_neuron_num - self.g_1 = nn.Linear(self.z_dim+x.shape[2], self.hidden_neuron_num) + self.g_1 = nn.Linear(self.z_dim, self.hidden_neuron_num) self.g_2 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) - self.g_3 = nn.Linear(self.hidden_neuron_num, y.shape[2]) + self.g_3 = nn.Linear(self.hidden_neuron_num, y.shape[1]) + + self.g_1_84 = nn.Linear(self.z_dim, self.hidden_neuron_num) + self.g_2_84 = nn.Linear(self.hidden_neuron_num, self.hidden_neuron_num) + self.g_3_84 = nn.Linear(self.hidden_neuron_num, y.shape[1]) - def forward(self, z_sample, x_target): - z_x = torch.cat([z_sample, x_target], dim=2) - z_hat = F.relu(self.g_1(z_x)) + def forward(self, z_sample): + z_hat = F.relu(self.g_1(z_sample)) z_hat = F.relu(self.g_2(z_hat)) - y_hat = self.g_3(z_hat) + y_hat = torch.sigmoid(self.g_3(z_hat)) + + z_hat_84 = F.relu(self.g_1(z_sample)) + z_hat_84 = F.relu(self.g_2_84(z_hat_84)) + y_hat_84 = torch.sigmoid(self.g_3_84(z_hat_84)) - return y_hat + return y_hat, y_hat_84 @@ -148,29 +155,37 @@ def neg_log_lik(self): return -1 def estimate(self, X, y): - - sample_num, point_num, feature_num = X.shape - factor_num = self.args.m + if y.ndim == 1: + y = y.reshape(-1,1) + sample_num = X.shape[0] batch_size = self.args.batch_size + factor_num = self.args.m mini_batch_num = int(np.floor(sample_num/batch_size)) device = self.args.device - x_all = torch.tensor(X, device=device, dtype = torch.float) - y_all = torch.tensor(y, device=device, dtype = torch.float) + self.scaler = MinMaxScaler() + y = self.scaler.fit_transform(y) + + self.reg = [] + for i in range(factor_num): + self.reg.append(LinearRegression()) + idx = np.random.randint(0, sample_num, sample_num)#int(sample_num/10)) + self.reg[i].fit(X[idx,:],y[idx,:]) - x_context = np.zeros([sample_num, factor_num, X.shape[2]]) + x_context = np.zeros([sample_num, factor_num, X.shape[1]]) y_context = np.zeros([sample_num, factor_num, 1]) - for i in range(sample_num): - idx = np.random.permutation(point_num)[0:factor_num] - for j in range(factor_num): - x_context[i,j,:] = X[i,idx[j],:] - y_context[i,j,:] =y[i,idx[j],:] - + s = X.std(axis=0) + for j in range(factor_num): + x_context[:,j,:] = X + np.sqrt(self.args.nv) * s * np.random.randn(X.shape[0], X.shape[1]) + y_context[:,j,:] = self.reg[j].predict(x_context[:,j,:]) + x_context = torch.tensor(x_context, device=device, dtype = torch.float) y_context = torch.tensor(y_context, device=device, dtype = torch.float) - + x_all = torch.tensor(np.expand_dims(X,axis=1), device=device, dtype = torch.float) + y_all = torch.tensor(y.reshape(-1, 1, y.shape[1]), device=device, dtype = torch.float) + self.model.train() epochs = [int(self.args.epochs/4),int(self.args.epochs/2),int(self.args.epochs/5), int(self.args.epochs-self.args.epochs/4-self.args.epochs/2-self.args.epochs/5)] @@ -182,8 +197,8 @@ def estimate(self, X, y): for i in range(mini_batch_num): optimizer.zero_grad() idx = np.arange(i*batch_size,(i+1)*batch_size) - y_hat, z_all, z_context, dummy = self.model(x_context[idx,:,:], y_context[idx,:,:], x_all[idx,:,:], y_all[idx,:,:]) - loss = np_loss(y_hat, y_all[idx,:,:], z_all, z_context) + y_hat, y_hat_84, z_all, z_context, dummy, dummy = self.model(x_context[idx,:,:], y_context[idx,:,:], x_all[idx,:,:], y_all[idx,:,:]) + loss = np_loss(y_hat, y_hat_84, y_all[idx,0,:], z_all, z_context) loss.backward() train_loss += loss.item() optimizer.step() @@ -191,14 +206,24 @@ def estimate(self, X, y): k += 1 return self - def predict(self, Xs, ys): + def predict(self, Xs, X=None, Y=None, theta=None): sample_num = Xs.shape[0] factor_num = self.args.m - - x_context_test = torch.tensor(Xs, device=self.args.device, dtype = torch.float) - y_context_test = torch.tensor(ys, device=self.args.device, dtype = torch.float) + x_context_test = np.zeros([sample_num, factor_num, Xs.shape[1]]) + y_context_test = np.zeros([sample_num, factor_num, 1]) + for j in range(factor_num): + x_context_test[:,j,:] = Xs + y_context_test[:,j,:] = self.reg[j].predict(x_context_test[:,j,:]) + x_context_test = torch.tensor(x_context_test, device=self.args.device, dtype = torch.float) + y_context_test = torch.tensor(y_context_test, device=self.args.device, dtype = torch.float) self.model.eval() with torch.no_grad(): - y_hat, z_all, z_context, y_sigma = self.model(x_context_test, y_context_test, n = 100) - return y_hat.squeeze(), (y_sigma**2).squeeze() #, z_context[0].cpu().numpy(), z_context[1].cpu().numpy() - \ No newline at end of file + y_hat, y_hat_84, z_all, z_context, y_sigma, y_sigma_84 = self.model(x_context_test, y_context_test, n = 100) + + y_hat = self.scaler.inverse_transform(y_hat.cpu().numpy()) + y_hat_84 = self.scaler.inverse_transform(y_hat_84.cpu().numpy()) + y_sigma = y_sigma.cpu().numpy() * (self.scaler.data_max_ - self.scaler.data_min_) + y_sigma_84 = y_sigma_84.cpu().numpy() * (self.scaler.data_max_ - self.scaler.data_min_) + sigma_al = y_hat - y_hat_84 + return y_hat.squeeze(), (y_sigma**2 + sigma_al**2).squeeze() #, z_context[0].cpu().numpy(), z_context[1].cpu().numpy() + \ No newline at end of file From cb437cea2ff85007edcac80928d4ce56fc465e1b Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Sun, 19 Feb 2023 16:00:49 +0100 Subject: [PATCH 30/36] The script is updated. --- tests/testHBR.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/testHBR.py b/tests/testHBR.py index c5a33d57..6b98ed9f 100644 --- a/tests/testHBR.py +++ b/tests/testHBR.py @@ -4,6 +4,9 @@ Created on Mon Jul 29 13:26:35 2019 @author: seykia + +This script tests HBR models with default configs on toy data. + """ import os @@ -22,20 +25,20 @@ working_dir = '/home/preclineu/seykia/temp/tests/' # Specift a working directory # to save data and results. -simulation_method = 'non-linear' # 'linear' +simulation_method = 'linear' # 'non-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 # sample numbers across different batches) -model_types = ['linear', 'polynomial', 'bspline', 'nn'] # models to try +model_types = ['linear', 'polynomial', 'bspline'] # models to try ############################## Data Simulation ################################ 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='hetero_gaussian') + working_dir=working_dir, plot=True) ################################# Methods Tests ############################### @@ -43,9 +46,7 @@ for model_type in model_types: - nm = norm_init(X_train, Y_train, alg='hbr', model_type=model_type, - random_intercept='True', random_slope='True', random_noise='True', - hetero_noise='True', skewed_likelihood='False', order='3') + nm = norm_init(X_train, Y_train, alg='hbr', model_type=model_type) nm.estimate(X_train, Y_train, trbefile=working_dir+'trbefile.pkl') yhat, ys2 = nm.predict(X_test, tsbefile=working_dir+'tsbefile.pkl') @@ -88,9 +89,7 @@ estimate(covfile, respfile, testcov=testcov, testresp=testresp, trbefile=trbefile, tsbefile=tsbefile, alg='hbr', outputsuffix='_' + model_type, inscaler='None', outscaler='None', model_type=model_type, - random_intercept='True', random_slope='True', random_noise='True', - hetero_noise= 'True', skewed_likelihood='False', savemodel='True', - saveoutput='True') + savemodel='True', saveoutput='True') ############################################################################### From a6f0f043b35905827d6ea11c886b83381377ddb2 Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Sun, 26 Feb 2023 10:01:18 +0100 Subject: [PATCH 31/36] pcnonline transfer bug fixed --- pcntoolkit/normative.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pcntoolkit/normative.py b/pcntoolkit/normative.py index 5153a735..d4a8cbed 100755 --- a/pcntoolkit/normative.py +++ b/pcntoolkit/normative.py @@ -1032,11 +1032,7 @@ def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, Yhat[:, i] = yhat.squeeze() S2[:, i] = s2.squeeze() - # Creates a file for every job succesfully completed (for tracking failed jobs). - if count_jobsdone==True: - done_path = os.path.join(log_path, str(job_id)+".jobsdone") - Path(done_path).touch() - + if testresp is None: save_results(respfile, Yhat, S2, maskvol, outputsuffix=outputsuffix) @@ -1069,7 +1065,17 @@ def transfer(covfile, respfile, testcov=None, testresp=None, maskfile=None, save_results(respfile, Yhat, S2, maskvol, Z=Z, results=results, outputsuffix=outputsuffix) + # Creates a file for every job succesfully completed (for tracking failed jobs). + if count_jobsdone==True: + done_path = os.path.join(log_path, str(job_id)+".jobsdone") + Path(done_path).touch() + return (Yhat, S2, Z) + + # Creates a file for every job succesfully completed (for tracking failed jobs). + if count_jobsdone==True: + done_path = os.path.join(log_path, str(job_id)+".jobsdone") + Path(done_path).touch() def extend(covfile, respfile, maskfile=None, **kwargs): From 687548c4079d8c8e6ff325b6decc6a9b51b94d70 Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Kia Date: Tue, 28 Feb 2023 09:42:16 +0100 Subject: [PATCH 32/36] The default options for robminmax normalization are modified. --- pcntoolkit/util/utils.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/pcntoolkit/util/utils.py b/pcntoolkit/util/utils.py index 24bb8dcc..e81c8158 100644 --- a/pcntoolkit/util/utils.py +++ b/pcntoolkit/util/utils.py @@ -1094,10 +1094,29 @@ def load_freesurfer_measure(measure, data_path, subjects_list): class scaler: - def __init__(self, scaler_type='standardize', tail=0.01): + def __init__(self, scaler_type='standardize', tail=0.05, + adjust_outliers=True): + + """ + A class for rescaling data using either standardization or minmax + normalization. + + :param scaler_type: String that decides the type of scaler including + 1) 'standardize' for standardizing data, 2) 'minmax' for minmax normalization + in range of [0,1], and 3) 'robminmax' for robust (to outliers) minmax + normalization.The default is 'standardize'. + :param tail: Is a decimal in range [0,1] that decides the tails of + distribution for finding robust min and max in 'robminmax' + normalization. The defualt is 0.05. + :param adjust_outliers: Boolean that decides whether to adjust the + outliers in 'robminmax' normalization or not. If True the outliers + values are truncated to 0 or 1. The defauls is True. + + """ self.scaler_type = scaler_type self.tail = tail + self.adjust_outliers = adjust_outliers if self.scaler_type not in ['standardize', 'minmax', 'robminmax']: raise ValueError("Undifined scaler type!") @@ -1122,7 +1141,7 @@ def fit(self, X): self.max[i] = np.median(np.sort(X[:,i])[-int(np.round(X.shape[0] * self.tail)):]) - def transform(self, X, adjust_outliers=False): + def transform(self, X): if self.scaler_type == 'standardize': @@ -1132,7 +1151,7 @@ def transform(self, X, adjust_outliers=False): X = (X - self.min) / (self.max - self.min) - if adjust_outliers: + if self.adjust_outliers: X[X < 0] = 0 X[X > 1] = 1 @@ -1154,7 +1173,7 @@ def inverse_transform(self, X, index=None): X = X * (self.max[index] - self.min[index]) + self.min[index] return X - def fit_transform(self, X, adjust_outliers=False): + def fit_transform(self, X): if self.scaler_type == 'standardize': @@ -1179,7 +1198,7 @@ def fit_transform(self, X, adjust_outliers=False): X = (X - self.min) / (self.max - self.min) - if adjust_outliers: + if self.adjust_outliers: X[X < 0] = 0 X[X > 1] = 1 From f23b39327851ffb0d91de291a3f93d27ce0f98c0 Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Mon, 6 Mar 2023 11:42:35 +0100 Subject: [PATCH 33/36] added docker entrypoint --- docker/Dockerfile | 14 ++++++++++++-- docker/entrypoint.sh | 8 ++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 docker/entrypoint.sh diff --git a/docker/Dockerfile b/docker/Dockerfile index 3738eff1..6e22e953 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -20,10 +20,20 @@ RUN unzip dev.zip RUN pip install scikit-learn RUN cd PCNtoolkit-dev; pip install . ; cd .. -# Add command line links and clean up +# Add command line links RUN ln -s /opt/conda/lib/python3.10/site-packages/pcntoolkit /opt/ptk RUN chmod 755 /opt/ptk/normative.py RUN chmod 755 /opt/ptk/normative_parallel.py RUN chmod 755 /opt/ptk/trendsurf.py RUN echo "export PATH=${PATH}:/opt/ptk" >> ~/.bashrc -RUN rm -rf PCNtoolkit-dev dev.zip \ No newline at end of file + +# clean up +RUN rm -rf PCNtoolkit-dev dev.zip +RUN conda clean -a +RUN pip cache purge +RUN apt-get clean + +# execute entrypoint +COPY entrypoint.sh ./entrypoint.sh +RUN chmod +x ./entrypoint.sh +ENTRYPOINT [ "./entrypoint.sh" ] \ No newline at end of file diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 00000000..517f2b1e --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# take the pcntoolkit function from the first argument specified +func="$1" +shift + +# run using all remaining arguments +/opt/ptk/${func} "$@" \ No newline at end of file From f38f75c34d74385bf3300b93fe517fc5d6668475 Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Mon, 6 Mar 2023 13:17:50 +0100 Subject: [PATCH 34/36] Update CHANGES --- CHANGES | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES b/CHANGES index 456c53ef..c652fa55 100644 --- a/CHANGES +++ b/CHANGES @@ -72,3 +72,9 @@ version 0.26 - Added support for web portal - Provided a wrapper for blr to use transfer() functionality - Also streamlined tutorials (PCNtoolkit-demo), so that all tutorials run with this version + +version 0.27 +- Configured more sensible default options for HBR +- Fixed a translation problem between the previous naming convention for HBR models (only Gaussian models) and the current naming (also SHASH models) +- Minor updates to fix synchronisation problems in PCNportal (related to the HBR updates above) +- Added configuration files for containerisation with Docker From 3bae416a4eb8aeec9a796e24f2b6a1bdb3276bef Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Mon, 6 Mar 2023 13:18:23 +0100 Subject: [PATCH 35/36] Update CHANGES --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index c652fa55..909c230d 100644 --- a/CHANGES +++ b/CHANGES @@ -74,7 +74,7 @@ version 0.26 - Also streamlined tutorials (PCNtoolkit-demo), so that all tutorials run with this version version 0.27 -- Configured more sensible default options for HBR +- Configured more sensible default options for HBR (random slope and intercept for mu and random intercept for sigma) - Fixed a translation problem between the previous naming convention for HBR models (only Gaussian models) and the current naming (also SHASH models) - Minor updates to fix synchronisation problems in PCNportal (related to the HBR updates above) - Added configuration files for containerisation with Docker From 4c44b02c330bbbc18b11828418dc2339403ce749 Mon Sep 17 00:00:00 2001 From: Andre Marquand Date: Mon, 6 Mar 2023 13:41:25 +0100 Subject: [PATCH 36/36] Update CHANGES --- CHANGES | 5 ----- 1 file changed, 5 deletions(-) diff --git a/CHANGES b/CHANGES index 909c230d..c41977f2 100644 --- a/CHANGES +++ b/CHANGES @@ -5,7 +5,6 @@ version 0.16 Minor bug fix relative to release 0.15. Fixed a transpose operation that would cause normative.py to fail in certain cases version 0.17: -Changes: - New syntax for functions related to parallelization (python path is automatically generated) - Support for slurm clusters has been added - Updates to hierarchical Bayesian regression (different priors for intercepts and noise) for improved transfer @@ -15,12 +14,10 @@ Changes: - Updated sphinx documentation version 0.18 -Changes: - addition of cross-validation functionality for HBR - minor bug fixes for HBR version 0.19 -Changes: - separate standardization of responses and covariates - standardization is no longer on by default - new standardization methods @@ -28,7 +25,6 @@ Changes: - minor bugs resolved (CV with HBR) version 0.20 -Changes: - Major code refactoring - Requirements updated for python 3.8.3 - Updates to documentation and Integrations with Read the Docs @@ -54,7 +50,6 @@ Some minor updates and bug fixes: - other minor bug fixes related to cross-validation (computation of error metrics), import problems and calibration statistics version 0.23 -Changes: - SHASH functionality for HBR added - Bug fix for normative model predict() function

    ~tVJW6nBdASQyq3OGT1{xxY40gTRJR)PR1XsO*oATIqVbCAkO`^6YL8Z9zwcL?lYW4&reF;6*ScwR?4#qyD!%5a~WNQnt^Og*Ay z;6S3^|8M9?fA0?1;~(I=^IziWf31Z0$Nc`!-$FH7+A-Lxo_WmdaN%3JH&I$Obgd(9 zM)anZq|~i6_D9jx(bj7_9F0blF|b}~%tvtU9KyMnQe9UzmnO$#5En$0A1Vd?c5~=} zf_NVbz0WlQp9%%OI4FYid?;dp1)v`$D5}4|`Ox$S8)65k9vr7rnM2M$kN&&PSkH=EM(TB!+e$qmuBaEsA27P;TkAJEzHzmzZ40?|v+n2@&@vb*1*K;!tOaKk?KbsK&{NPEba(D;QrRy24v5983;w0!F@xq88A zvyQi1f!g(kqIRKkYnkhab z4ZNk(OvYa^ZR(9GUqd^M{t)B^m2R7NRqF+4%`eD=G;+JU|7`NMoM zSvZMJo{BB@Sv!qBWwj1tcpjB61ay3eVf_dxT{EgJ?)b#Yf~%$8+i z((g`de8($(zZvl1KHxiCR%kSs^%rr?=;7cn-KzKIgSS-f_H*Ap3EO3|Ky|Zt?B9FI z?FH5{hso=FIGe%B!^h%)9(Bp>Z?$5|58RDGB?kz+qU&&h2VdXI`;Lt8etw!h}K2@&T+{H*i8lf^jC?rFK-9D1*J$w`{GMU)N%D zzsVw$0cHo)E(@3xfD5#R@^V9dna5Xl^cmp|Pyi?n_n;W2 zqJcZ}x(fm?Kr1} z=FTH zuq&(ot#9+{HTRbIT{LM)N}iV`D1w~AU>Gb}CwF4iV@rcwwWr^#)#jE}7yM(k6sCJ? zySBx)$F|UH835EW6bs;B4DUY5z&i<{b=m;EU;tnR*)(@9w3`?z}yWw0-fkZ zz%`xNlQ#>(j+<*0rSQ=bAF`yt9F2?mGCt7Hl>pjQQY^%3nXE*TCrlFNL{db^5BDb1 z>n_?~GM8vU(gC94jFa~Vid(7#K?$+~r{>b!1C)W-ns$IqXuq=o1n7{|!n;6Dd=FMH zyU|5OMOuPW%K%l46oRf%RP2Qaafk$Fl;o~lUlEA-51P0dY}|C&7=Kplhr|pCKo7~; zv$(_nZY7mWx^jhvlN{JY#MGN6ja=cL3K9-~_^`7^&L|M*Xuj-?yu>XdO`bSn)Cw_8 z7{aR}#IlZ0G(o?yjG0PQaHdX#@X1~|yipjl!WNz(m~8qJQj1^Bl)?m=>-)&^Kp*C< zFRPry7plim+eugo7S?R87_$(_eE-KXp{C4XR9JAa9C`GfyGitFAwhQ4y_A{ET$P(h z;~ygbtf}6k)CO2unnKLxIi&c#mK-jSeEu#o8%%ZB$&ePe=>%?V$-8Da=Zk;q4=@rN z@Lh-ZcLEBMKn&GrU*tq72)5a5-1;(oS!F&%9G)XE?m zs2_8ZFk=wG0umo0aiu8MJfI&x6+x4%{Vr;_jM!mBH*n+ZJ8cRrqdA=eow`%65|aKw zsSePrFQh8sWFLbl+nB09;SrvC=D7{3hm<)5VU(=%xDRU8pusu(Xhju-+*D{?cGX;+ zf88#tKYq$^0CY@w*$GoZ3{ZANk-5;872pDbec8N-`_R$WmRRlB`S5+?+A70mA$)@C zod2#KW38}_rG4gh-X}z=E+Ce)S#!6m7w0{sJT`ITpJli+dCj<*#p0Pc`%5ifK>xh_ z8(Uvk7^+yC3L=@cUr8`g9D(fsu5(xIf;mZ{T>_*!DEwN0j0@pVP;QzBRSEp#_1el)HAuQV6^VBf$u2CDvyN;Y~|FcBQeVPs*<#EX%!r z{@8^$KXbMPacu(~D{QThh7yboOC5Bmh#Z=w68K5(EAj$o@zDHecY-9`iMv3IXzEj= z6`y`2J)AP(C+P|>*}hHhWBgDSBz42DfkR=RS{)}}0P(wtiF)T61a#ZF-IfkWg8>}d z##bozo>pA+I$PFjyecGrEeTqM8kS{HdgKIbzdxvJhBR!rKUlIf(ow8gj)dn6F`2vv zwffU7z~aB3g>GX7@I+^!GZXNt4j@!l7;9Zo8nQ%ZE~>^+iXrlZ$}xIX7#Vs1rEd_i z^LW6n3gSzGB(4h1t`r8a%Y;-8+eyNf>T5K2n|CD5C7adT^M=9VS_S+qu(*%ArrGbu zIyAyZI>864#@yaase!KjD@mCk6(`vfy96rayyFHdQOdDb*&2H{_igna+89k8L*Ijh zMmdX0o%&wA+F22-8-EtYcnI~rjk-QNk!I|tZtz-F{s_3K zH${0gCg2P&Eg!v=>Pf`uUSzdcOlw2WHCf;`R?QV9>+&H<+jn~peNjeyz&7E72vmv1 ze`YWNvDrGmfr*fWooOL(S?CZ32na6%7r7>R08zX{=dn@bljaS$o+} zk1=^Vo7<%zyy7yL#SM7#L>dyy#>g zW;{4EgVI@l3D;>=+ho}_6LzajJ(7iARFvhy1adeO)N^D3IOx4<$wg=@N=KBV#xG$$ z348Nuc#3=-=GX-{L-T~p(8(Z(Wo6*}3j>>curdROY`;ePrn)wuTBWC%iGaZ(GiEae zcxz^y*xF;v#(uYSZ@YPf_rn0|5zTD~VO(R6ovvO0-?uoM^U*TPko=7Q`?=Tm`=0yk zuHUmmBe&MWbJj-)BM-c2PpL$?BpR*+_=l9GUsQ5X4?1@hs1${@4yiC&GorcYpjUv7 zBF!tyiewV(BtJ+)rg9jGLYKzVjyFwcH&(R{5b4-)zjk`{yFc#Oo9uSjv@UQrk~rZ5 z-L`3ymHE?|n3;pI3%AMkhqv0;FRL$pxP~$ehROTmEKgZglfNytV#wQ+>~Ip z5Sa7gVDg+?{$GnQunde%)Ta%2&MU8_{@x!#5BR;O==0PF^^`qfbzju@J=*$hODQ@v znYK2=1*zIynPkW3AWQ@#%hU!CK@FX?fL4a1C1&0MY^cIo0_v?yo_i|R58eXlu2b-R zrb!~*1|Yqp7lps^5Q#T9HJ)otl`c_79I5nABXMPs+M#totBXeC0;W$U518_z!@P}ZpRUEvqz$Fu^gBu9g&Yl?PIxm2<7yB=P6A1zi$OaJO`8%8U@cVHI1S)0K z6`Y-$OqffU4uunHu@W8ardvl7&np!kXGNTlWa{2sd7q>q->HYv96Hje-)wf&p4zc_ zjy1&CoBmDeu^qj#jq@H)Z+J71F}B(Wfx72H<0lA(?tXkuS_MFp1>}Ii6^m1LWSuQk zXXsC3d9`47}HeJ*a1G?FwgBc9J57LAw&)8d=1$IswmpZ z+|>?f6|QhFMD@ZCnU`r3|?`A z(ZE;GrFFPmy50|YTt#U^=! z6M{hQ3<7dUbO}L724u_FLC{1U$Ko@+w`_eJfq}?jyml{P!{nSx=~!VE7P)BHmU) z6k~9L^sDQR?@RnczBa5Nyx*&D< zvbO=y|ARVKDbcjz+@Hp{&s`x4dUY3?Wia>xvm>Fu#T;&~LfnJ=%g)kzoe*c}69-58 zksg^wRDmo5AB!Qbvw{)&fhqFqR-gqU-y6uDpYeCE2FGs*IEIjJ8D_L0W_?*>q`?DR zo4vsSksGRghfN+PJqb9meq)NTvfvW;FI2Cgg>o;E=WIdraJa@+p&d&7iZlS)loh)G zhzMCrW-y=)v}O@8N#461O(17Uw-`-Lw%XJRIiqep63YwX61sC&1+y#|gtV{#J>b#+ z6JN@4R5Jx_fP5>An=|AMPp&{$$ef&b^srgie=&9r!J-9WlD@Y6u5H`4ZQHhOziZpJ zZQHhO_Uo94ndx54^x~}REb5P_%F4{IToyxyXOPYGcI1nW=(Tq!Ht9oeygj+0^a|S9 z@k^l0b&lWsXaEfvrIi?3NsrT8(%k%mkQZMS|1QKA18xbry5^82!)B&liP+cx0*v?>X=? zH03Bv3B@6m8SaFy+*Xq`4V;=s25F34{|5O94v0H_Jn+pgBRbvjKA+Sg&jfW&EW6yI z=`ZYkIN9d%Q1wIjCY@Z{cbY!>3^rq$_Qv-09^rnc0s>&yS+*M2W3}_cPlXVcuyD*YD zQQNdO#CoMd?2E9^71~1x0vX*6q&YfIN_Y~7gY>gMw}V(%%$7I4h|NwsSq~1lILvxd z9?3aA+dCQiV~nCxF6DK)aN@-njhj2(ROM4yfyrAsztgruZuh>3qEl+FZsiAi5-ih5 z6G~^%klaKx0jdSzGLt9lOEVhOK8$6wg7|)D;ofz~Yia>s5Newj`st!!Wrh*da`o>I zy*-ilzuofQV&Y3uSV;M3h#{ZDJ_xGIvgcBoPoGE2)UL-ncFdbT401PPz(%PdM&S=Eticdu>8`rDA0aoR}hQp<7lJXyD03Ediu$40%gN;dxF{~VAu`<+i{7}QwGodrGqHA-e@WPKt~tF!1`bD z&RjS=nL=l>C;(+Evq`JeJCnNBKcFVa`HLxo!qTy;_v5LlvJrAU!nzz=_HKpeNGY(DA>C*wa4H&e*FiYhP|J|E3mSJn$%- zkRlMMKi9W}B%y#j!jU={^*ke8+>B@K4*>MlOrV{q1W|b?Nf941p zyiuF25s8hgrE;GwVwr8HL{f{=*vvR1tr)pW;_AyoJg-|Hrp87*`G!j;&AY2iwpX9i z?oU6rP&+C#C!jB$Uot-(6<%@(KZ>U>iaRb~zFVOJ{Ix)ewE~*u;ZS2q!V{CR$r5Rg z;u>aUrQ$BlRm?w3LLSvMnHn7@Q`|bxrhql+p7jchQp#er@GKLi92K*ZjnCRfYuL8Y zX+$v!PE8D^u?ikFJ=F}hv0^T*nVb6=wRU#WX~-Z{OK!}r8C+PMQ#vscPKo-%A2%bo zPVmQ4@w-+x#b5H<=353k0`HF9b8BWdR$CT3kgtO;V@`qXySUAxd!Bv&T$+O}aXte( z2Djd_%n2=b#9wpImlN$mx&$_H!mAtTYv1?K;7KdKKE?#ws1l_xvszM zTk&pgLOX$91lqXHwz_FE7w!F0eIRb&=f4r#avR_WzYRKa13o*iE!uKRJ_}!XUZQ~6i(E8jId-@+nUxGcNQwJ$Zl9-bGcm(j>->TG09?a z{FG?jMl9c>WLziivX+lCmfKr3p{v`stipkm4AnIaSXOJNTDL0ntU3VZuQ!%6MjtOg zqcmWr+EfScO$kyp3}iZ0o(U=$z#V6jkWO+Oy;rm?R;+Z(rh2XyhUL4}YgMxA+;=RO zE_@fOHExvR48?Nn-f@i^b|kw`I9;r4CE=LRXZalOy&r|k6ijJ5So3%6lXF^tvTIdO zsB1%GoMxU;C!48MyJc0VPq=Vl6e7sTLO6NUr*$Bn-}WmUp}wQ;Ci`v`hy!v%i4jr& z{xt{LcnFwN^X@JAC1V-~Y2({WTzA#l7zcXe+e%&w=2@Tlx)PzI-1#b@gisv@0BOghlONxUa|VnP0bD#w5BOJXvNS&%e*aw8BQzf z7hT3{uGWTCTrc@dM!z6IC04m?D|@INJX8X4fhfIy@Hka#Zu4@Qt#bQ;SvJ?;wg2?_ zB!Av;Tu@VE>y&r+HfreAyo+JUoV}hMU}tJ*z&_3gQ$7AujcivyY;)6)QOogT2l;Sn zKrJ%U37SziL|j@Z zOG-#Yu-%z`3qbk%%qDH_LH9xV2l%zo{j@uPU@gwcJyf8I(~YhGOxAtdf)BXqat1QD zg7|%Xo7dGn-UO~tJ6HAdnt|`3KK$u+y#KP8;#RR$s&0F^mn??H7>yQ`tFH!(Jjvgy z0S=>3n|amwwcoT^gTu8V+o?n`K(~}a1%rZI4A@DUfTb*Mq3l{z)o~$$N2PKJak1t; zBpB}u`YduqEaDZ~(1t_VG{db1vJ<5cyZYUtSuDxB6mBR>7STEohI0K`(S{mgFu)bBr6Ee+U__v>&aGWH1bA+RzC^+gnpXuTkQWQ# zKluim=z7XSAP;cn!=Fwz1NzgVrQtQ#XJYdnYw%~RYs<9IbQ!jQDCiBHQKOKqJpS37R+KBpdIq zMgOpQKWYp3BR>KEpSq!n%C&f~L>lT#HW5`qTPSBiDR?yrb1R%4i7y4ASjaDis4ql( zPyQs-e@RrQfT@28Vzf4MwAN`=b~_yeboC=712JN2Lg z@PTf^3RLyTLd=uFRdA55d&cUT1vHss#d`Kd^PE+l{q+3xI5S`uf6eSAQ0Uc> zFVpM0w~f3}%Nr$pBJbZZmm>1v`Kiv`#E@y-KKhRN=Uo-~-hWIj>qR$U8TEG#mG2RG zXFQ6E-vx85VGm$zM2R7;7ddmm_M{Q)CD|e?6KcQCYZ9q!6ybPc5TqjR0E_06JoJ0y z!v39g|83!P-(u#*o&Cm$W(6x8zq%c~usN-Kjo!kwOx+w*m}TSw<&mEAgpD!a?2@W$ z72$d9%b#X{_Wt9!Bg^C0RC}6GbebEC>ssWA@#pes3pmcJ1yo-ixlwqvnGW#1#})c# z&GiU^5E9UkH4g@WuH}N<4U=Y$!x9?SO44+Uer-LCBqUu#*G7_fFKJ0dwOMm{c$t3D z(Q0g6uM*&mC0UB8d8Z4EGi(EBI=Gv%k*t1O>tLj$2ZIG_8MG2mNC>*Jr@=gABJ1!_ zI%lAxaok)pW_5FYkba$3nv`KZMgq`_j#`~2&UwUSlR@Bb$Hmfm_gYNT0z_0~`!q|{^8I)KY|5NajMI%pE&szgfaP(zV8hw?f;9~hF4B6dOvL8;I< zNO%s=7iqDNf~|H&w4;#%+;UfzI+{}}^B+Zlj^w|bcaRoHH=>@7Rgni$QW3R|MM#Nn zo(jVE%>-a$Q24Dw$v(>C9N6jk82uuFQ9TX$wm_90=QR3Y=J+-IUjZ+9U=y5?jKg64 zW|Mhd-rQr==in04-U%YMeTn@btM87+s1QEEW@US(x{#*}q z59}i6pwU7DZnSx673S?6!@O~cu-j?#ntbhDk?p$eYXl|9DrZ#WF|P23fzn)73|sQ2 z>2?fL+BtA954dPnX;`U4>!V=8jz0ADoLe%Py)O?(GV$Fi`^2^SQ7-31#+12Fx}w&$ zobVsEA1;n;MVzOnr=N429ar;LGTY$O?+#+qw^GrJwplJ~#ky_&2n^=b(zur{(9b1T z%AyBz=-0KQ*1_~s*Un{30cNtBa6@8}T5&8Yz{1x&!Z~eG!d$YozevPH7tKdISTOK3 z3%EpA9Ai2GxOo>EwA0iLtChlvXRg`@XiU1?OUhJ-8pSJw@j`d9${l`&+PtSYRG-kX zYVl58Ku}IKbGB5xGlBWB^IH5A<+JM9Y5tfnsnybOJ#QE`S~Qj>JaP*ro<=c*h^2p< zl?;({xtdBch+$Sos@c`BqmIp0c$}~zS#|H(V>hrEf2f!Z%k{h6T>wWKp0mY0YUFis z1OqDunKvbvH=#ri*_TzT-lZ=buq8dlmLcwNS( zM&EC+&iJAS$HH$qX_J9j3fktTT{|vVY#Bh@!{6{pA^cd!AKTb?b_)PYwpy2h!>gR8 zXRqH)h*-l+9Y5~a-HtG!kL0L*vwp$IcD57Hjozs)!@e-1XPL*BC>NJmJP>oAKM>kWSD{vq2gGP``^sbc%{SdB<;1(F1 z3y`iefzDqs`b#<-gd}v?MfvoVZ*>CW`cfFr9tc@dNU>fJ@zVT-UB*BzurG;(QiC}m z(`I_*o%|peDHD#0%(XEQDfTRQ|1nn> ztsGXLac4{SqUmzxi!8B?Du9rc3SQ>ZQYqLfS^zk*+l{?5|HT%5zmYnaE9k5R15$|h zMOWJ}wPK=~C&GZ#)XTuXMebM6mIo;)qe9yiYLjxv-nq=18QU* zCNsz%Kpq$PMWXuh>GdUjGopOWMlZ3<@#10VCLWkcGi-(el|mU1O}UknUmRBbfKJN! zg8=g5?e6Sl?>)$otqKQ+lkz`@|9aW`Ke-blIxqeJF3agXk?6GyrzW=?ML`>4DbE$h zVtU{q0SGMN2+CAN*bJ*!S69DXB(@|G95Yq)0{dVwu9ZTOXP$*GRPUk?nLz#0(ySnj zZ$I$o1cB)TLecqayPj05ZnN<_=rI`Pa+Cp`_A>$q8E3=~htT-*RKK%S>S`5cv!!G& zTgiYGEjn|@U&QN$+6K%5Mkct2rLe#=6AW4nwABfUQ}M>mg8DFaMy29*%6nYYOP%)q zZi(<83F%D6okA_@#Mcal+!V|N2(x`z!#cdZ6w580oOD%J%(aWMohjr^g^&vE)R`>8 zC5x7A@>g`!eJmA{aj>{dEMmg5s9k1(O^?ACcS-mAKt}Zp%Xg`( zB-uCW)0<(|LCb7HKekmSA6v=yXD*Z@E;97vYT*9G8vqGOD_7d>>zz`F#X_~jd1fgN zjXd?Qr(oreztiyFqi=l7`!A`S@mpnR;H(htASsZiPfr2p7zJc;=Jx>q_ zbLaK=%Hg<7hC1*pez3s~vldUYkkzKzgE<_a9pybqb~im!ZSHrkiZIuw1&k@Yg)v2u ztl=sS&bU_(;>UP%fedY6E1WaGW}v0ILcAfS#R{V#;F&$Z_Fl-mxqk=@b07s*J18YXdVC~ zy_4m<+NnG4A$Z(CIwEVE@CoYZZvi^M-s4-|p|`s0d31Fsr_JryGI<`el;{qti`KS? zg;Co@%SY6@uMuD|Da8OJ>7D~@65(t z$K%+aL=Zr8W4*;i4@vJqN9tY!`W1Od?`43%qNvwrDLVY-EiUMT>@n&~L3UB(=C~1E zshYfmX1bvDR(@*=ogAQ5)-{7x5wOu30^h>2A=p+pNY+I;uM$&(Ira;4>^x>-XAa!T zIqJ(;Qn*fkNjeg8qxzL1MYd;K++n_~P0udwjFtJPPPsxxexQU|zB1hfHZrdP*|Ll9 z6TqfQ3OlwtkU4b#?nNohIt2d8Ck=)_R6eR`m}~!Jd5T||pIYA?n-2rnka;Hz!(Rrp z9UT}det=H^>Dex}3>)jflpa_;u46F-wVhejnpnM_mWPNymN+Z~imyHLnUPaR5fB_j zKaCHTtD4lShQzos(KIFhQ2eH_v0cDWm6jDispgrmXETWAK;S7-?u1hG4iIVjpDy>FH z4}eVhk2T<3&*DMcz0yr6YaYf3YI=w^aNz@KK`F1X&NN}t4m#&F(L5*8eqW09ak%qe zCSso#fhk|Mrk(W^c!LK@QoIq`1|H&Vp{XmPv@b?dJe<1ZLPV7k^xyQ@_|v86PWC^G z4=*S*ZZ;YjHJ!>Wbk8>FbhhGk3XHayG$dHX#8|~I)=8n6%Y&mPkq_q^glBBoVMA*JAfYu28&84{NTNx!9jJ5L1k>UMKCd(fFBSi^4p2L>`|@+C`h6&e zwTa@~TQUw*){9%DCA};X7(&)TD@IhpI{aO)h7H(7?mAZMy=b z{9C{XDMgrG5HAh*9^jjm)ShQ+>>WU;Pu$1UKC9g(Tu*~x2dp@7f=IU}Hlu3LwR`{Q zkBqrb`=Z(hJdr92;HA(q=Fei-`Xi9$L3}lgyDBCP;nBAIOAvZpL@ThVQ{v02&)>Apb;%h!;+5 zqR>$+ad@ynD;!N@Iyday#GKJ3^@dB1%Dv?5uP9&*r5qXV04qYOcZity8_w&N8}m)S<1eKf?Pllv~T= z)-lm=a*DrFsR{w!P+Ly*u;7ZT%L2LVIalOk$oR>&=Kq@uK<^xV=*`zxSYhl!J@PoCx z%5KtT@lm<)Hi^b#7uc=-GKnURa%z(BB4f6^#L&z-TXwU-HCA?0KA$}J0`$n$N{_Vn ziD-l+6!3VeY=tHcuV(1xs!2Xv_C5~7JDI19IZlk&we?K=N`>WaoH{-BPbh#l{Y8e<0~YRe8FHiA2A>hiR_Qtq#G!bg&-x|3JRyL`2&CD*oqH_d|}&~Ps>pt zikL;*DJupM93RdP6rD{Ofr=xQtlH9j{vm$t%9MzZq$Yhn|4~Gm*66`i+85xj)EZ@w4@*@t zy;RtmJG#@aU(|SMeIi=+d@mR2qwbc7<{JwG?E+?u;Ldc3I|Ya1gx&@1ZP0B|*F5BW>%{Cu5t0&|t#NvS=g zL{$}QMYr=KvB)+(ArMsw^w13LK)^tFPJuXX#oF>l_vG!3zmkOYeWx+1}k zM<8jgEC|E`QYUzVL@}G&+O2C5Xzj}_Jpqi+YF_!FmlS(PI#FP;o1f?X%`5)9ve^Er z0My<)EXL37LQ+F88DQC^Or(_%_A3(c?nj%2rnsy^!%jH-#8=1LptI<}3jub*0pW7n z7@M~4rvglA#rkTDyO=|91my6%3h{odeM59B-3A4>YVo7uP1!jVa%M@P%AWeP>UJzp z7bk&QQ|eGA4zygqL*IJyxRV9UK>hftO9TL~UCO8}`y`5;*bIxd&KZwe@jw`VX6FUzuq-b366FbJNnCT%Qu+z9hKf z=%~%wOJR^5F5ZwiY_QRE^Aax+_>YW79R07sRrhaOUkT!44jPJsh4b$jgKYI1&ISxx z+An>&CFYb+QF};t61xB|(820TRyU&>6(tmlq0*wyf|KeIK~WRrEDgFZtvoBvhelDLG2oQuHz*(a;*@)v)V$f&-vUU{ZgLNgWqp&{7pk!+3FJkTp z@rHRAua`e~=iRZA$(tD) zS=(2ivDC2mW%9+k=z+xGiEhXq$^aO1MK`al^aG83Z~qi@dSueAefbphPCkbq?BV7{ zt^sv8`{0dlRjhu3fQov)u=aXSxHoaRvZ}OsPPjj8h49+T^-cdB0fK<0GZdtXZJtu; z&xivA>W=Z4ssN0hvv^7&$rsfL*(&yDox57HrB#N>pFHw5BUC$CD+VnALV88za%jFqGC`y2+1NP{VihP zo<)O3+DR_@ULYu^F02xoG=u*V9cs2YFB|&T?=QM_d@)ig((>UW=!_dUU@bJXx+nix zUn~ zDp4A4e*?hgoyvtkUu&N3KefGw72+cHGu@oKf5vUENDkd}8s=3_1(%HlmlAWrwc6l> zlc7L~a#iyO#mzKJ?cz9Me@6Cuhfz^&O1j87Q^WgwuwSW43g4{!)NOQo)SoXVUx{-x zGL^nPbUd3ms&%1=?rFwww{VWF@j`AZVXOFgG0>blE8GW; z;MWD9<)A98XswnsRsyX1k^nN1+(#>+*v{etgMXwLvTnH3z7d(83(E>>VnsEZIL{Vz-w1HYDxzRPU=iCP)#4YkJ@`_ z(Lr<@fxL+~Qr@L;{8@Z0D#yy^VyaK29INw!z8?HJj(_;F28{C|Zt1a@FYs}pZw5l! z`N6e@6>Ruj(hzj=@AqKb@exRdKSM$HCMBH?1ZSU^NVxy*vraY*b_|O7JB_V_yO&z~ zbfII{e>vZ%E-CxLINk1Mxz>VuV-2O~qK+n#Y>wLl%X#6GWrhrs_t^wicK)2V`G`wi z)W8RaLlMPgiff>R*B{Yj3VbuxD_Dc~GWzGBOzIrdVw`e5h%P}!i@OV?#2h<^9hmQ}r|G)x0U;wJ>Z z-;fc67q539KDb=LPkx0lZiOW2=3Eo*z;wevE6lHeGiOdlf%4(_gTeNJOY!hYh>LjS z6)fTxtNygvI!x%O)FJ>P`N^|ww&mYUTQkGgPzB$>g^BWi9j(;_%2YQRu-!C#%i z559R*KoBg!gu2euDul2DknZ}7HFo<%73ouv4(()7{p~(LeN2T=>jS|2lL1$h9#o%l zbV%Jn@=l>M|G+Cyp6zebYJN>lpn;dXN8doaQ8NUvKM*&IBG7qv7rHZ+VV;611@h4z zdg`%l-gGiS^z3Y?W1)({+cf$O*Oy%yF+`k#!W$<3kmT{)_iB6;$eDjBOdY)1o8(a^gZ( z27z;2i|dl6n?;HR1&9Y8!t7rDh4<}So^$_ohohKy_`7G-pe@91 zlmk{PUBzW!FH%?9Ar{2YEuWnNbmkWPe8r65o{wL6NRc5S7bl&=Q>8lhFQBGbk?Ti+ z{4kIx9%6OviMdqZ2ezVM5a`A$s&E;t1pH4dX`~M(rs;PNWyKO=Ss^@M;3IUjS%Md4 zfI^;L)#G6G4NdKD8dATo@!z2%KZ8KxQTyB}Z)V_!I<&&fGnx)T?@m;OAAc0QE*C@- z!3-w5F$53br3et`@+i(r#$C^<=9SBXiVdoD?zlR6XK5SHBohgc`?USJr2Vv#?J zIY0(bU%2XkEq#Oa&OZ83n|4MhNju>R_?t4hpQh&L;_090jGg z_?i2dv@Q`T8JjTR!-4MV+W&TIVrdmP{x?lbSf#`7m7lc;NqC>_Ml^XDh>?5jiir#* zV*Q-yfdz~x6^)cN5HJq?jJd?XD$f>VG$-&Ivt}x5H_!o4j-fa-CzaVd^dCOBaVthK zP0yepLXWfRSOd7>XE>5pgNi2|;vYt_+4%u_+TiOsFY&MC9=F3rl!9bxBm2^#@GC6A zSvOqVIPoal6N9=H05f_)r9(YJd_4vcKmWP-0&Q17z6H@-%mR!o@AyLooV&s}383xML#0N!1-6I|V zYjV)Vv-pOujzgH<8FnJzkw4d1X4GT){+}`lKJ43imP4{}n+w&2xTAWl%P)Y{qyVZL zD7F`L-Id+I4?=GrL_P_?kC;C6az%1`#&(id=wFQCZm5U)Hb+DnZGk_VJCOEY(F!>| z9P^NpcUwjFSqTgw2a(BdnHe z64#P*u|lKe+qM-vLsr0UvnmsmtE;oZotdvKJS zBHeS==*03~r1{G|`g(O%(h#n;=o@aNPG2Myy<8Qw#TU2mFW{Z|Kg3IX{LL>z&1geq zGyQw|b?6sY0i13M+1*{SHvH!VX{%*EV)o?pfn4(6f$uW!MpB#qiU4r|WpUu3PJ=TK z8U;UX^X@JDyzJc^ZU5TgL6)!;*?z#q-YI2fP9!t<0ns~)(Rb6@Ty3FjF!e|CbO5)5 z7rmS{cqCg*s)2G}sf3*E|&pRA)VAW8NCqIcrky1wmSI|SeZ zxC7?F@FMiYP40$ZffI}g;-QfP`l9vUU0!^^^~VEx%g_0E1@ea4Q{s8Pb+TKixs{tA zq2IrD7}x}cbWWmY%Sj)BQ|t_)*jSUjmR0QRL^=a$?hLcUlQcI9zSxm+DJ5LPSni6m z#KraheFc97Cj(H61FqqXp9(ts-3GqP@?QCt4({ayR&Tt2$r4b(eXjGShXFadEA>)tGrO3pt&y#dhM$=9Y8+Jgt`l zkmJ2f;gTC90L}^V8J$h+R&)oReVG0+i^w_5A60A_f|Iw} zWwwglhu&ntn(Bb^k)0T3Gg_SMXuhET>7mZn`K6K`t8fR`glMW zUV`3?u%WQi1fS|< zq6|ZV8SC9V<;w9}u6T?Cl;BpqL?g|fh0FFhhv5p!BeRdh%w=5~Ry%$_Jyp)AOpiE0 zg(Cy@M7C2ySbm$ZJ{3cwp z_uOYUukxZfq!1Rm>*T(T+(kJAbMCxji}HIPsP(;mgde?;3S?v!$H1-VH8r~7rd_CO zz0!8Nl01!4&5y-L=c9gH)@rrw7S%IXKX_3bC6L#+{SAj4z9j>M z*^-D73d7XX{JP1v`gcgsG3Y7ZgS9efcbs{k!EF(f)!}Y3`qesPEBFOLb_T5b4IcL! zF;4cpqj5`^GHl46hM^U1G|lwvZnyUa?R9-(rS|4Z7#49i{FxsrAN2wz=A=V1C<`th zZ3^X(wRL8kG2;3`K=y>+kAt8Yw{pM^0^NuDG&6x zOR~l3e{&~OEX)f3;F({Q10mQ2*=tnNQU@6bcfh!72M+|{LjgAOrC$Kf7^xp5vlNJyP)B`m5_+0Wo}0g8|3FN*gpcK!|dC$o@c+=Ashf zMV4!-Y@v2!(T{%S%a{XJkWW4Oon`7WfEol_Gg zvptuj_0)tteXx-^KLQi_bm~YM+-kKD?mC$Iz?XEEQ}dZ@He zdk9EKPM7?!^kz=R9PP^lX@%)gX4MSi5V0cfwW{i&HmMS2dBy||Du!MwXyPrxFD`7+ zwnaZazmN#+r-83iN>b>~`h7|Yr zA3yKX)b{f07Ko`F>S+L}$7rg>VBviUMuPW2^`MjaD=!4Q8pB5*?9>@XycBjT`6DJR z8VuAi_#@~mIX~=Nhh+&kNjiX6TPyx;^NgEKz^)XXJPn?}L;`O`>O=l!e5Xth0VQ&soK8{x@rc~)1B{n(s;sJn|W0YT9?5)-7^}Y4FosL68 ziv;u4`#OMsR;$Grxs6uhL!N!T7setlBGA}8tD`hhJ}rBsTy<%*QKk8AseG{!(p>mi zihlvtbt!t!S8&BeZf2B{(m4iqsGf(usK*048<+b1moDC|$lc*#+xZ?P=PsNmQaDaj zk@1k=WPGI<%$a1QgDzWdAV1}0soU++?&BhpdVtySXeXaN(1}ky_Gk}I7x56#6`svH z+OsBcvE6O=a(gWK{AIt=VT(5qv-Y&zQp#ytzpl8OyKNh)G!K^Ca`jR;>I`Tr9Oyt7 zIjC7xb~ox$M|tbU8a{l_Bk7p<+rwJ{fpviMI<{E3>ATl1SvGDs4_Uk#%ZPc~mY|2; zJ!a#Hg{6W1eG|zhGID9@vC!_dB=Wi)nR1LlJwciu zJ+Y4a>c*4BvGY6>Q0eVfdMz@bqRZMg?~=Tkf6Y1+pcY2}jSV3qXAtbFs$k-I@)Ej+ z6n2mjZ+jIO>ljxO7RNeN**YY1K`K~ZnW2jvvD|{re5g~el9P`bpnhfHcw-D1OMPK` zBvOjuZ6YL`hyE#`6g`Y|zKPX%>B@aaH9!mYExx;QmNYRq48CRyD~PN{_Ug755qk=T z9AVna0$4vIy^sifL6pB8L~ZuEgp}R0boKM2&@^~riITMHGHlf<)?B85#)o*Y>6sI^ zOnBzfAx7jOtD}YD?U+qE_Y9hkmtEID%OBfa#vH3E@EH<#kpM5xV2rNhGRT*~q%?RiB_MoZf4Nlh9bAscl zrdRImX7hY!+57dd;bm(k)}cYA6kd*O7Nkz+)7mb{SlRtC{_LVqu6#Dstd*-x7svcb zbUa#*9-d9)&`7xBw(JWZwj!hzN78rv>i7Qe`}6so&1Gk|@~8wL?1D854-wp=FeDm9 zA{rC7V|tL+galojy+)o*9L7jy!JG?F_X2gHJ0ND~^u!7l5=_F)5JuWhrXOy{QxY>V zoCLnT1ha+D;&5EnTv`4n>^jMgMUkxRy{&=SZLd6={$ZhAQyw@lzD2Bj8Md%3ORjp% ziVX$OYobNTRe87p3_&J&uK1*!J?KD6NT=^ouJ-!rtEH=j=$pI-Ak8yO4Zio)^ZI$U z#$)=V*_J1Ku&(=bbd_0HM+vrdfk*POIRo#_p}Ix3ful5ABF3d8h3{{_iV1hC ztqkNUCXZ&@Z*fRw9XPcqO;4&?R6lfK%X-6{Zq$08>{>idBR(EiBFmec#sFA*zQdF# z(2rWx9x{|03n*{?+zJXixxGvPatM3wo$5Z|-xFLlJz{V9qLwDw!Bp7&%{d{iyZPmB zOqA(yxZgiX@|lBaG}*K7_31j6LEO5wm45qT{kjM{@QfhQ0l7YAL5mQ76T$`mH^goUdK@P|1YPRVP5@{dtcrIh18%)iGXql#o6|2$u2 zKu1CpsC0&`*=`L=b@};L!73?VcuNw!oy}tkOk~ud8NzLW_6my{ z+LoI<@t$y|$Or0rN8T@94HWkAGA~$M#ypvlMN7xcmX$C;@iG(r#!3YYV>&m>^FJCr zVyMYTt{UPw2?ED}h)yQLNIDR;ka%i(kGu1e>krF9r_C)eVPaQzG4qwOgtUqKz^~C9 zlG))H?dZdW2h5$DeKjx`r*X(#9xYY$56r0pvB@ituFtv$)?!@kO0#8zMjVdw8<7HHBuGucVk`JEbD!JS*ZgYemBVRYRNWe4&@9 z8iMH4%VI!WN1LUez^2o3T+=!2;rspBGUaNeHM$yj!%hv~m@vO)V!Bid%~}D($`u5! z%#jhVm>r%a3#I1X+dDyrv@dcI(X`|DvQ#4uszBLBcd$8VL3f9$Us{7t$gBvI>@GBK#q8$E%SKGZ~{Op65!|Vv(Kz<$!5CV2+UwdPAkA1hohhobt6dT{Z zFt6nDTvB7%V9Nz1QVT7A2O?-Jm++bWuu!R6tdUO8(k#1=uwrG(Or%uf_AJ!9?l4)p zrup1>gDj~vEq(g?dd(;@AIy}V%+p`%XM)17K<(nF>sSvNLTp!I7iR$76Yl*t7ObE_ zo^H;6PS(6~!Vaebv?7x|!!y*O;G#^xAI1^~6dpK{qG{-oeL=k&Qd z$&C~9c5nMdB`d=vHIiO(dMvEAtMRh*D&@thyxJ)`X;@{E>p{^KxeUDpUjEWpm!b8N z@GL6XGXh~~9HEFk$dAHG`TYgF6kjA;2tG0EsSLq-9JEz#+=|eLPXaaMJoX z!ic6G$-j0?&oD|3o3^Y8+bQSyRCG;fAgdpHAhVs}Wb2)DiUI73*zv5FSYryD6c;)! z2K-)}ip_Z4Z=Ah_moe~_nrP-0^&g6ey`{ba37vS0(7H8#?E|(IeYW{zA2Xq*zXJ_v zl!2G=r~i#Zk=O!f^Viz05cST6*kvtWm`f7;_>VBB^vTGJgxAGAM>Nx%w)TS0WYeey z_bJjriu{RpDC#7JpCsNd=QRbUl6s>x@d~3U_To4l04%FRhxdWFusS?7yM5#wEh(IV z;4f_bI|@EVFQQ%4G8_^jUWuA$Y|Cl$_cnc!pq0mObDTD69|CL1Uv6STIdG(T`|lRA z+nDRi*4_ovVw+++qAD+?Cnq8*O1+7xkR_@{IHn7iz4}2?MeX3$Wp5ugVcr(EqMf|` zfzB|OTB*&l+l34}1%Z=%DCREt#c~-MBY|#dV3gA0uFFcOX5G>QyYe?KBNTYbg&xNn zixv6l0Oa#wHl>qui&CBHAcwI_riJa*=D#5nX<19T3d;0x;r7Gc0W>`OUmC7(#|LU( zRJL^A>6>4(vTzK6=XE~=W9*EUdQoU(^?<{541~any4k~--*!CDmLxaFeZ^DMNZ*r- z)%ejNUI6d%Ih`Tqfk*6cL8fwkA9Wn*_pf$Cktg$0kG>W|pXT(zOQ`Ef7xD8=n`R{X>R6b(q`xfIUUlDJtCqF4*@f?`((jAw7rWqUO4bQ3YJtE@4CV`#8w^6 z;m$Z@O#j>_Q-BWE+zp{83<3}Uwqpe<{N*BtkPag)6RyqtxDw!77ugX68@|EDRT;Yu zmtL5D^0D2qgzUF?00~?IP915w@N*CAk7ZkT_LrFgiBjlL01y^yr$I`r8%C=2H z#wYVy$@2x%Kgn0XBD>iGeo z`A;&a$kh0=bKD!f1*)TGD|1K^i_@P9OZG{c&laJUIJ<7kE2=drH*UKdJwAn=v8##A zDtd&52~q7XS@=h4SgS1+xSlZcDHW0ETocCDIFaSmMiZ$|nHYY)sXtH#>Iuv6kW@-a z_|2UNufNojehs2|n7`^l>9h~v@XZl%*Y~B`ktPV;*0>vCeue}y1SOynQ);Of_x~2T zNZX7AM@&Yfy7sI~4N}3blU-r-{5@Dk+hhXwHW$WXSc2~xF2qX&%6WTO3%zY=dOaD1 z(|mfyp9F>io1oM zJyMw0l{TOGH{)m7M@?!xaS+<4a8jI*J;vDRiC_AVrrv4Sf z$&r^MYwg?gBXGA~%4gZTrG`;Zian0^<+^cpVic7s)7NJ~6(JBz6%U2v$Mes%9>Mbx zzhO-W8rAM70JMXZCV=@Sw1mpq#+7o^RtFTKQ4{Sn|45AK1w&4aQ>|kJh+u?|v#XYX zj{jK4G%J<}y0tRT@0vEfr!8Vc?V2*7&zdqigoK*Dbh1Sy_0dl%+xZ7O!Ou$d)hrLP zhyemQ8g!P@EYW1ciudnb$-NnV1i4f-=N`w}s9VgQPs!MuiAJg?!bR`nCK|hS8X3I_ zIL9;_LlVt|p35)dR0y-h!@N5DAetK8&G8c?v_>2KPkBB8qSY zj@zkI1I;K1T;?G~W9ro5grcVaBeK8(R6 zRw{o_IyLB$fR59!j?O8Xh}yvBbC9Zf)*;XBZd}w~W=3f=uuwgctexesqZLR*uC6H? z$Hzav=DhQp@Fc8P{zQpZ$t@6ZbBv=q7H@m7iCjMAWtM~C5?Ca3!8r^$g=-RYt>Sru zkk9tc;%E?luUnt~*A#&>GGL(+0}ZPTb@bJTOtucVF>_Tiwu4W21?hXdm>*Kik{^Bw6J(Td zeej>E553Oc|Che;pA*5KLORU`0RYfS^#7fR|Ft!(9sljjVGW(yO;#kI*IIoaJZh4f z`Kv?Ft!?k7J|F>wqP1(0MZr9h4O5!Gnu#s750`G@k|-1k4L(?TqKo3hr&IRqX6%L5 zpnL_?n>kAo@zg?3>JItxB;-o;6OJMrnKp-&P@W}sw@LY^%Xc3 z^i=u)Kb26>vBj1OQWfESCZaQKS<^{nc!-|-2SZPuFb(g3w-5zd!s*Iw)DjFrcTE35 z**i9g8U{(CZQHhO+qP}nwr$%wZJVcU^R#WdZ_n(9*_j)0?{2*RprWGQ%zUycIaFs* zPL2YrmJ`uSbZi}DO9;BqzP3l@=a*xmiPauun5fJDHNmEppngJv?F@LUboN6j@6c^8 zBq`wOdMGus{3KuP>mbxfsNFTLyjSQH={HpD+?~99dPe%OGc5A=d|$aqV>UG2a0*Nw zAE8g|MCmBC<|m%ys6c&fQhW9%3H;+IK)wZH4kD}{>W{-1VM$xlLRUR z(3)vxVNr5G5sz`;5G|V9r}1+wZYHAEQVm9l@M|kNv?S)b?sV>L*>U8YA-`FTI#gen zdqV!&dHp_9nxD&7#2?9K&`j?G>ix#OFeOBb3~NB!+s_^nQSd zv489W+fx)%O(RK;+#=_y)x)9DxHH|rA3?(Hz|~Rfl13JdAi)@uI#FHiMR_#}pGEd| zsF>L_Dl4gBJvHT!n#>|gkn#li2&&aIAauz6%<7FS!K>KY_EU>SVbZ&1;&2tm2V)lL zMQGBZqxAcGqFs(RA3)GBsfCe2%92QaMvfvedTHiAfkDuMl93)Uape zbrJs+A&$pL%pxJ^9Ol@b)eZvTqi1cRogt#2l^~IT&!7m{m)$;qRN*7>8Eq^cOM-6L zg*=ZeLZ-*rcAPBWcZ@-9ar6`VP^I4JfS_YcypZr`h!meky?#HVZXLNKENeuv=Uahf zYc8We$bcI)5H$3LbK?MdM(o}T?Bc1hfP<=lX=~&(jJ;`+%l%LQ0UJnySx=eH5$BNS zf-wPHMgQ65NCazy}YN zQFhX)FlPl^yX6XEn0a(eO`M;Nm$ro@M*oyK!Usf{WMiX@t9p45E^s4%tz4jT<#>(& ze=zg9Z~uB_Jc>L(+TFxQ1C3M5az` zP{Y@oH#D>)c_{tHSCPIL75q7w!vBEFpzE_Nq4%fzbe%R@8R7HQS$6A~=M?w1Qm zovsXhm)>>O1$U1e$!(4In>s0nNKlpTaQAdfvnkzCM|kS!aih@0m49z3s%VPGY- z+BciC6Nr?`X}iQ?e^B{_X-)K3HuqeAK74&CG5@AYT_Ni#%Btb-y?zlD_BbyYf1f~$ zP)gjM_Y4~z?h?T?{S67+#skyB^`$0P?H<2>^m?ur`gI_D$xN8*e~$FhllJW%odrNl zZtnw-1bbz*C;TB#djpS=?dRhsiX)%|l zKEYmFwa=W|(WHqObzi-rDRD zuAUx8(}IGUN)(RDl=qruhucNubJi*d(hvC8A4ywd&#U=rfb8SDY*g_Jj1C~9GxdFg zOnWv^tkxU(=d=E|o#(`8H;c|X+|hHpnbPGHpVBSCOYbZ{gjG&++` zdbkM0tj-S`3mSpe+t@?r0|vDUTO{laZ#ft%HX7g{uE0SX_l@pp-@&0j(O%gdJO~&& zIw?^2HU_P?>RoQ>HRkfvtBUvPjmxs=w^l|HSZz)rA5jsfjS_`H^bYv0p$=!97oGIle-H}M&gpthmGk!{Y=i)_izYl_)ov2* z{QF|z(*LvbvOwQMAM5kgmov=wX^>ciK$ zDQ-9YP0*R$3*3YrqWhZjBzgp-y-5aYW{(BWXfsUhW;l25(&nl9tvb%UMjITMp4rh8 zTFp}-<`4gWN5v^-!@Om`s2JoIe*FJ{@&AM#<_`Y}Jyc|)e?f82hdRtS;r$A$ZCs$h z=9LnZDP>oBJ$i8qs<=p+ilpS~*Awn|!@f`BwlwTO@m+VhCp|H3tW^O;8g?6F4MhOE z7AE+H!x;m$^k}L~+DSE4@6n3Cyc?wH$5<<4O14;=0IftW$xJhI0Aoh%BPCQ;nza`5 zuJrB#7Zpe~Qa|}+rSw?}lJ)hyn5xWS%QgGY^8;b9c8l*8i}^(<6RfMxWE9-l>Iid7 z4SD079v4Kg&$?H!swWo4~rFn^=pdXi{@ z-Co?Nk1f!}xzQ{f!^c_{Od2yxcBvOM=J8aU$x#DeDyR;Pj&^))nCO>R>2OX{WAo;F&*b=;hK|wr#88sAaroD*e$a{7wH~olaUPr$!#A0u=n)<);+? zXPy2ZM~oaiue5CAH`@`vuk{CP_}1i_qplw3xicH3fOg!D8n?*xO(nM#5GR^f^N*>;qyM)HwvFUag9p&$Pc1-ub4p}>V zoFxZo_Kmznp>Xy6V&%!u(b1FPlN6_kk|6Ib)f4(qAT@nO)B5_fXzJAZh^t5Lv&Rp$ zwE&X3iK4DO}~Ut5#1wYQt1kk=`c$s2W&5bl$Silfp(4|vt?Rz^kpHm z$0*=I<(iJ;uKJ0v9%+IiK7`y&aVnZHm1g~GBA6yh%eWZ`qf#6HM#b|1|ChT^I%P*( zz3%^gT@L9DqggvH{&46`8K_U3qg+b%EO#@8RdIJWQ;r>)-*oOhTv|Yx4cJrQfiQI< z7ByO0FcyFv_?US{%AvQ6oPW%TJP>tvI(Zmd3H78b;K22o}j}Hg2iIyCJ`1f<$Ie zeMuAfFzAl(>O>Gzb|21t@GHHl$0+(XcX%;HBe$3&K2- zv_@>dvnA+Zi(2DY04(;&fXw)h%7A;nFmF1ycvYFUQh_l!Npw*=;}cl6W>VLg9j1c` zdD8TYXVN?z5Z(s8#+`PVyzcM#%|mU?9u0#qUM7%d%!DO=P^U3G4HS~!C{piaV+HVP zE?`Ai%2`g$s>!<~n#~4qK5%rP^5hUiKjke0#%4K*2g$^OfQgQ4pjL36>PfR1kpfHT zL?H|TO0y>NBLh^#C)z*jX&8F@mMsn^lca1z~|wmonNQvK6n{$)iGRSF{;ZV;+ZI zBTgXbKXz3&krmu-Qm_3goWV64BY_eai(K<@Jrl}s058draCB572S&mm+;}1~paH1N z0r9=(WM`s^My>%v+O#>|WGfIq<09xjW1Cqde}d}PJr7B!mblMcGEipOk;{Ganu5ci zZc?eqb&NyKjD`Tpc1z^u-Tuv5e`)uYz=yG@yF7^7G(VvL}-uxx)_6jCsnpuL$;8^7KhKYxGML=T5xSj=+ zd4o=YEjXezV(vC|W)HQTWEc*`K<`EwgWcd2Jcnh0&cFgtUSmq+FsJcY`2qs{Y!c>c zAwuR^mysr(Lc$iZ_#DmK?66>VWs9}F=J)IMHuY^*^OC*jirKStHyDV9n23aBG|$a> zR~b5tyK^6_w{3W(SN#;R)TSx1@~I|kZv)&oDO8&vZEU_+l|B-GtR7ul_{f>K$E|&J z%5sMY5G>lN%}4G;xnr^g_|Ejf@-`>HQpo`Z}l*kC-&;#!NFrJQoNwGIf4G9?Ij)QpK1qdu^|zb@q5Cq)r;6L2-_73-+~a zfIX(ZM{J$q+0&m2)H0e;=wryF-2eDi#6D1yJ?ury(Di>_jyt~ea=b`9y7F1LYtdKM zPC5s$kft==Jx!W^{HWZaf@r~3ULb(z$-u+Tq^k#EoL2V28;}Qx6BXyx3_*BN0!UyLM<81 zuDZDy#xA8yZiR(V2b1Ob6^r`5g63lWO=Azsy8m}k^@U=m04)%Uh0p49z9^mD14BeK z>l&EBZF1@T-D{7YxAC@bz#naLQ~RcUS|W3+u^=SUqf@Ux=?L-03-U#0_-U3up)6A; z+rkdZW|ap{>!Ds!$imOB3CbxEU2A&deP%RJDD$GoKGR@9MV0&PF>!KyvZK|sywKWu z<0;jBiZX2TjMIKJ2Z@!A!j#fChttJnU>4ASok|OLmJ1FkQN!7fCwQ44wSEy*!fBf6 zjVB*o$(QC;nzWr?dG0L64Qx6*%IM|m!z4S;cqN$@n@+70;gS@3=wTAb3gs5UUtOl1 z2~hfSbPgy&8ju{$hjw=rwcFB{BRj7k=MzUdoEUGmu`-n z=22gh^+!MGdsHeRQb9Xdgsbe3qiQN~Cp_!mlp}41LqAU&HBLcYPQ~U#^yYVQZ%*Mr2 zV`aPrOYSKT$uXjf`6w6B1x%6dTOK`)t9vV(zOGKn&}S>=AI-L@oo-L&4^@RCiy)^sKFBbF@I;`eB&mkL}_gY zjSw(pQ!3UXbP5^Gwg7>klK7EnN3DS|(xFN!XH6k|PH~7%spgf>%7F5qV=7*TYJE+; zASX8CJpuOq!2K&8R6E?8Tm}0&s)pZ*S zeAE`mEAiWZ=Qa#Vjxoy6PaC}S%1Z>wjv<&u#V99V>Zk4aUZw@4gqlFE`S+(H5AciC zdYaQ-7BKzawkN&*dZD?#FSh&sb8( zbNEc}@OF)TO;B8(4hj&GY=bloK5#2BsCG#F5iWH)+}{YLZnpKZJ{DS*tSej2Oy_ty z3v%OdAD#%w+%G4SQ-qXZvjPD*bhQ)T1|;gEbT!x)3Pkp${TVGx?S{vU6VQ2eC_=0- z2C0_&4_6^@Ap$EWQrW<_Ir=&OX}m@S>g3O!&=r!6H+3tn>wf3P_n2sBZo9i4&=d^X#Z19=Vh)H?g!$Xt&YlJO|B$n$@U8;HxB1iLU>0zJ@kq-OvK(*c2k)FzSD1&GA%f|R+gVf_PVqPWA zf{z3W9k|pYag^IrlQ7yjr4odkdI2FMx)2T0*YFOcO(UmRl$1j=I;`K0>Lar5z{sCB zr~a~0D6?3aO&6B773Xer$C@Wcp!qmNO)%}}oPqH^+V zN+v%u1Ub&>L3n)}>Wh*F*v>;Re~;zVs5`{R_8gT5{;S@)SyE{k<#e<@d4n1Ean*d- z-d2}4guFRyHT7Myy~neCm0bH0?!2S_dZttN0SD>Wp9B^p{UaR!(!_ z!9)N%B(qb(-d20u$w$53>YbZ+`t7%3tpFAEX7L+xpSG~UuhrsxHF{zb`@$fJ8uP)8 z6F8M)q4~P7u%KAcPH1 zJa{7g33mG&*JCh%wf3j;aIOVEH zZ=HkHIxbG}JApCBK}uiZyvE%bT>UGwB~Gg+>)WY@lGqnjDhu6Pk_*j=iuzs6(R<~} zu0&HjSPyaf71{*k9Bly$wVx8hJMhhz8LzJ^w;!l~QRJ_X?@WWeF2Rz^8ZH50ln5rL zh(%o36@mcRl-l_7LHM1vP!g8g)tu)E)W+u)wpE4JOA21)OlSMT$*~zEs_it0zFxGD z`b<^;$CB#S6|}dn+3OF`f2k?|0YmcRKd9_{7O6Mheu2sj2UG{h|VgAM@U63i_Yi$NP6xb$toZA8J0N}bX zK-L%eAbEpm1~1J(m)1)K`?39~Z>qRx>98)vUS*tjP++Mf@#XFE?Nq1Bqoz^ly2 znI;o+BzP%|VfjnuOiJIMkE>@!Oo^I~aWIvD?}iyH-@z}FInFT+%t47FPqj8kQi7d4uVU-M zc(3hImJ6*ZG$o|A9Z>M#$ich=nDA{!$zt2?R0~Zqk_9&9F+X)o2wg3~0vDWK>B#$W zQSq4pTrW*Xh-=CZ%C(89{+mml zd!}^;`5(XO&6pW!JwF|rhhGAcSt~!t%JtoVzI_bU1bhs6Y_+D<;vAV)pHzxZ?efzy zan^11B{`?$UP&-+pKa?#o z<4&a_{=1Eh*erX!I+$R(oRdJAef+)C_U6#;BfV4C2At}$Zr?mTx!bk1$GvpK3qJ_+9ayPkP+`7$m8=D4xHkJ!W^hg{fnWQoTtrX|wYGo3{eR?VV4jR{} z=uf*Avp7#e!Y;RoasCTUx;vX-BM|>>Cr?`6!F2dxMK0RWKs<%sLt~%c@lVn+zrA@=p`h)l752p-3K826v*df_LwPSOq$P3adLgnxX;A&RV~wi9HjsI zx%*i_d`G5*Mc$F$o%RDAKo}x)`pfLbFhxRw=t`01&9X$aJzt!pJZOwR#25xU*E|<9 zA%|I9r})_s*1p^}9FH*`UK~6jsydEUr@t?Ksp1iO5G?f%q1>rgV=q-YX1)y-6?xw$ z*r+j_Pgush)OlDc$LR|uU$K1P)_IBLJ8f)JR%Z!5xcfwm=jC&5vXP83=68*x%}{r# zM=lL|c6MMx8iNDd56M0FYfArEAls=PIEXm2O&G(CLfbIK@?>qko$s$xST52j7;~zf z1G1W_**I#=Z%BF7$ykF%?6P4k1CR%H=(?Q9_?Te^j#Rnh`>%e+ zlZSI<*=>tiJJQ4x!0O-j=C4<|OLO}Wsk})JO(KOm)I}M&q{1wC-u{IhRAqq)-v!Et z1cs5~@n&g`5Mt9a6hvb$$Q4}dEsoFUfZyFW^YXH)%s-P%atZ$SPUEa@TDxKgE@4wT z8hX|#h;vxmMaEx!ySo=lx%sDZxg1*JjJ2e(a_qLXht*I}v0W#lb#S{x=vNfKL4Ebe zi%Tf4VJK(W$t)yKc~$fcw7&(H;E#gLeu(N<&@LX4k6AMB&t1wOWpXnE4Biy$1g zT|$P#&2yi8?|O0}4Iy&P0xLAFgojSba`#18&%y?wR4Bbv)AQNmA(~wKx+%B3OP)QS zk8(m=oKN0V5}FrPFh@R0BZV2hljrGPPx0K7F_E zT`8Ag>&Ej=jnp-L^|hg4<9*qVUe4CaXWB{I)Q)!2Z3&#nw%WF+cG&&zBX9f3&OA&m+e8PqbEj)Qa#sgbYS`%4VYEa$eYe0qCNs_>9JEI(I>JLt|KCTG}w+d zHqZtDo48sKA+wwwz`@}xOU0madV^9k%{LZ%6q0eMEL>?s97xU5ATM0~IZQCobk;s_ zRc6%G(L^5qstYLDmMB2}&W1rCLgyq+ci>b@NI%3|dPRL-)Wg9~i>Zmx`)2bt6gy-M zTZ3eT1lLZeEj2*j-0%PKmvDBMw{w940JuZ?zxqr3ukrS4oBQw6%@sr1hD5xj`kkAX z_W;v><=Koq(}s;uY#=%*fu$B%& z)Bok?Hl*xJm;2?U|3TH`9&etd))C2Cco0G`14sr_wI1(6M%_Lb^W2t6y2tjoe=`DG zIK4xJ+XvOmYzxP0N}2mlJF87oYtWU4-I^gTD7T|K$Lw)9w2fi=7+ScrjipDRF2M%2 z)_rSX=w#Qmd?Y~JEA&%t>VYb&9t=JwA$iE35bDB)rbg>%{=JV^rFJJEFMog0@jf2n zpLxu;`0245UY+lk=8&D`-oiUD_Z#ai*iCUR@0^P|OY3R)yO|Y<^%;>A34aEucP1(1 zeh!|EXr};ngZ+_27*T|eJw1dWoj%VGxCNUMBM;^=;ebDaDB+5oW@+2K5Wtx4nppU+ z*@AdSr5|DhjoD}4e_Z;4^f6(INNBPl{z-*vW`&z#lh=ukQy1lnkGuVJTRufv+7BXF z#W=(&%Js*jT)eQFg^;ngd+Ku&O#!X#w+7i>--6X}caLOmb;)DsfZ+7%Zb{8b=)Offc%@6lWCtwEe1|Z4r3z~fs>r>w_q-;Pw1jkRJUPQPBaoup!rl3exIwFw=IYiu6lV z(u$IfI&x(56-UZEQZJD=<+w%TzV*9;9)9Ix9!P2B5vCuGH`-o zGZgOA0XklQ_;*fkba(CG*FIIHnNDzo)(*w^b`nlX*%q8>Kb9p~v7rkFl$q%nFGTgw zWE~jOsbEEn!xN zrdPA->cOBI8RyN}4FM$9yHDvWbQlKI`5m01yz$S^=gO}Z!e*r3WL&dbmB-6Lu@Mm` zQ*yh{jaLDj7Ap90ffauYIAEl{xHsoNvOEx9{amb_QAR#D$ikCrg~jl%{Ox|;p}(PL z^`YBZYfZmI$}GFF$F0Xn-qgcYobj9WXW^tqUy4bRdi_C>-)9*{kmbL07RYSR5yD&d z?p(q(h5>namI5*Z>#u=w!Cmk&$+O!L6}1>K0u_pG{WCLM0ny?JRG@n z#DTexFMc4{l}tqCMPG?&Bm?7IfIYP-dzV2e? zLM!JMpL+R-SF5!7RbBlX^&#J2XD#a4YYk``{V>jke;#L$Ypttv0NBjnP zxDnDXQmsa{{W@gi(T%2uS*)t-1^96 za!wHv3VtgPr2tv09U+u^`50m~(tq}C4^J|Dg4BICx%vdKU*LJrh=C1*dNd2h#KTJQ zyT!o0(t`H6nti$z4T}BCXgCMrIUcT3w>XlW%f=WjX{23?rRq8}V6VQ8&)b`06mTt? zU~4yWuE%rXY+II#(iL{fy?}a28$1gL*4tg%{R@exlr^drxSjBD>}!e7jS9o6hs|vu zz4Zd!G7HiT-Axnv!MuyLRaNCTeGwm}j(oQBS-oFsFaq6)-ExB;l@9Ac-vOzcRmc@j zy})Z%g$mA(@E7mVa7PaMv$62KtQ@NDI_ol*%}ur*yTfZPTNS6b(#_a@C1}%Pxotj7 zu3l5wL>W_-^neD}$y#_;b-uUwwEiSZ+D^b<#gJb?<23$nZjo{>W__P}`89=>dTwV> z;sMS%tS&U-Pi65ijm;@TO`sx2=;S7RzZM2Lf5E}E2VX~3MW?_rU3ZpM+-zH3W|z0qf%zEL;HNT zfR(l=H|iEIvuG5n)=U~($^_cXieqSq$r~!lQYPVVj^S~&fDRw_t^BS7YB+g$(;rWn^rOu{$0oY<3+qHUaX#A;mv+_me&jcmwXp=_Pg1N2>y8QqgL~yCnjaoewjF5;!*uCUEw(iQY+i7y z&r}D92hn550>}lY+kk3>@v>XttLC$oSvFQHg>yC^ZsR-Jmp&0QolhSx5%HSnsLB_& z+*WwD0LS8XA(o<^LjCgp?{xjq6H+B}ONnz05@_*lp`KApW^OwXQyYD`Hdm_FH;WV@Ici@%9+iL(970qEh)SX#1e76TmtN9fq_sZeD=EfuH^5E^Xd5-pqQ8^eXFjBt zPzym%yXzD(N1x~PvRqMh;{{{)hJcDI5DSXQG=1uH>U8V(ODjS}Udd1;F^Q6|iOOk=zT=(|_r{PF2sWqf)zLv@kSY2aEZdQr^L62_r~obuKt7EIZjyBX5Z6q!Bpv zmuM`AB=z{d6}>x3uB&5cu~U8^q9-XU-PZF9LtUbwZuUi->|XlgS2IXMLxc${eT5PH zLei+C^$L4~uEdVU-PzhY!NI#Sv%4zsEFBq>4vX?SZB5ix_~HFyky2uGBSBKS0lFkk zf8G0fDEPYjt+#b^gW4l=Q=pdLT_erZvGXh_wII?uAPs^Baj+! zGUI?At4}$gh_4Ly=H+U!t`eHL8H~-jnnDM6Gm)}>+jFjtW>mL~E!Wy4R~v%F+&)b3FBI#ZK7!fVQ6$GAl;`uB9c^d&lIM`)b*m~w2HW7)+M z8KqHcs7sqD;`<7K2(f1P#)blP@kyeTm5N`te+L*b%TV4z%G3IW^|U_nj+2;q6I>e~ zP8A*Ih+)M)%*-yOXdk-wpqMhFw#9(rNe^>`t4!5SN2umX_fIhVW~SOehd$I9HyIj8 zoHP$DUTaI+e967QvCdGfs}0D(pGS!p5{^`eEHN`{Aa!{1L1}Z{LL+At!d6!wqw6?p zEfjkY0SXhxr@gt`DL=z8;s@v~qxgQieyr4s!j!H>RpeZxYhF>4nkW$hK*%KrLj&hk zeK<29|9HxJuAS}lTmF6HqxtWXGtd0d2f^*keE}1mu?z4Ht6%cF0`*_EU9gY;Z4vH2 zG+>g*c1VWbLqGo5|M%1I{~bEof+-z?Bi{0}y~y44Q^Vho+v?buw}!Wwy(XC>d$|^g z3$7t#L++MbvCOT?yqTAGE$JG6g=?1B4<;wU=Kx7zp)LX8JO)TuqN6a3Nf)6U4ZSY_ z!|596Br?KKhkOAZ}{TxE^2q!52FnJZF zyT|P6-^gn!@3?kbzRCO?L(xqLB7%5oGil{9*p*Wh~6Dx&zhf^*Wvd}8q0wV=w`woTb~Aw-~yOb1mWGSvmdc(#PQ?w3K49(pcv zL;Ao|Y$p?m3bvBkQu`#e+Lk7#MV`my`#F48O8%$x;CEShy2*}R4|?5_y6A#-cKf<( z+su90CqSwuYTP8;Q}B|dQ;_$05P$*f&dv+5pL&k+Lr*62g8Q-TLRjD0gYQX*L>p4` zBL`bC;3$2zM-9IvfvJ9uzOXQeHYk;ZXoD#+!biOxwF)V;!abC`=nr5GV(t(ZqXgt% z*n5aff}^e?T_RK_NQ`8eL`x6(1WYFB<18m-l7~aW#yN#CxJG1(NnvIxlc?fg>;42UIe)7+PBiu?0gdtINWmf~sxng$>YSK}M9mJPzf z-WyQa$txF))0I^jXg=450uWR@m&3XuBDg>)qrJE}bezNp-#xJQ%f>l$c0odswImL) z83w61MJAXEilI|XFq9Tj#HLiESQu!j$x|{Wna~W-&WT2ZNgRI3vepvrGdVKnOP0*> zhDA=alyqbBCRf2k%=mNHxlH`F^>ru^x7|kra&y$wH)#H2^m(}p&bcjEki2Fi_Cn!F zQ|`@j(x}l-XU44)(h;r&HN}fmU4mr|e2MEAE`S$)atjyGM-JBItcP{t%D`ao7ZGPt zJg&9bo9QE9Ne^Y`-9mU@C;q84AL>R*7%ySZ0REP2^q;7duXMmV(ub=}0y0kD@Fw|= z>WzTFU<=sN!77nFkb3KY>D8owXHxu_=s6LdSxykDXzz}cztzu>9} zzWEiR2*Y{}%#6hPE098T#V!<%#Leqhmc~@%(Rlo}*#5{azyz}U@n&s(=NAx!sex1F4$`>>L)adj$t=A6ISudVdf(q|EJp5w>TP9r?2ROtz;z8@4a41S zmHk-}28($DD@JKeIjvR%ACX>4K%79jgVcuB62t(0Nse5H{CXm=@vhC65Hy$t4{SO{ z6apqM&_x@p-jo|I-4D2_RYu$E2Zl6qmhXzvs`m>2<1*ZNB!+NdJSj}L!4eRn9qb}a zo6#thiON>9(+hZqg)my!B1Jog9$;-}z^i!STDwnX4{BLeudpdjqXCB$iRx;xZC#@c zi`M#QWXUaByoUah#ts^)Bsxq0zl zm#w_lxgQzbtLq|Ob};L7rXfHM)kBzwbCDv1Iy`bOUKlJrW~oEDHA$0<{0dlpmxE!S z?We9s(!WA!V~Q||FvAf@sp=In;P!HdEvjm&$fQMn#PeEh1SJ9>rzA$aiA6KMfwX|j zqbKTJdGc|8km#eV>r1b%irez}j&i*2F>Kl5UC#w^!u%$q`=CE1`$BtJV6WsHv-mmKU_H*1C6o9V4=ojw3PM%k#vT~l z1lWd0MTL3D(?=vdZVs=6HYV23mF&5y#jS&3VNo-To^wWZjmBk%CkK5kj{u1}xUQow zPXXkz$rVx&`nc(wHxY+2k<#t6R}F6YAF6=rjE8l_pTm*_T6YB+hyeDHT`~kI%}J`r!4G&$Un#hS zXa|Zlfb&UIW3?#}=}8Z}stu7km_OtEi%PZ~2@L9o7DJi3O_AY%@xLqIi-z!n`5?15qQ$V zr*r2|`j^5M6N|LeK}LDUwYrwGhV)KqgN>9iqGO0mvDUP9SY^7nmQv6G;4TLWov{;2 zZH+LvJKF-)qLgKCy;5yiH3AW}gy4rm19nOqk;4c&WY)H$)zWRt+8P6k70Vkw{jioZ za7@6qW%KJ%8rlU&o!QidN^G-o!roYDiRkH%_2g|Kfij+r(?Eh5B$Y~eVvX?o-G{eY zMK63K#4__6t}SDsyiS3sWM{3nYt1Lzq{_5?bKDMZMYV&-CgKOWNBgk^VL<8&-ZxB8 zuXVT*LJSnzcmPhPq>803tSTV{4K+DY2%AW7C`Q~Zbfgx&+3jV)(Yq#hM7uihe?=gvhchVO$@1(C)DOD-xV3bo(g)ZujnFh6?i;FaVIPf_&7ea#u zwN$&j!fY5tuzVoD***DUp634st$m-(w9huxD+5C0gAZpbyaBqpBwMOaWz>3$1SOvcmm^^Q^6Eu3 zHD@5(063a8WTro{r=%~2flE>0==C+ZP@LV6d@3O36?c68Sxyt~vU%kyVV_H<@+g)J zmI9cS-zQ?%)rOp^50?Z3}k%=V~;J+yR?BZoQs_fSko5Rkndbhc9eeP{)?JXO4ya%$a8+oYoSe9pP4sj@$#9N~)dTxM#4j#wsDj{8mJV{Jtel5(!k=0I zmyw2A0X|{$hAb=jFKQusYCDgf!Q@A3{`&{%_JkNO=E%@`2az479%O^_3FZ+Hm~M~W zPA1@cn#k3^D0TWTo=3u4=)v(OYj0!Jg$W;6wSCHfw%@2Y%UU8_ape;to%xFR8qOV> z=i+PQinB>PiHzc?vgCs$+(#BYQvoC2vxY6yP@yc;kj9j0)`ztRK2bfV zhVk54X*9`+Gy<1v#ci%7Og_YU|_%&S)sePA_a{f(C0 zZLK^YDGPqdtBrS9cig$|^kXaHhJ(C_8_E+I!Ndhi=Rm9bjbObv+bYv2sQ-UBd#4~# zqi#vCY}>YN+qP}vlx^F#ZQHi(Q?{$-+?bfT{df0EcYLv5_tT2~<;sU;vLMp}j%8d3?wYE|9sYh}g%{>` z&magS#%|lR-aNDoip=>nkZ8-li||JS^mQ77K0&Z;M{^D-61qU-U}?)Pipvq*#d^+W z588S-Mt2;QoV>xPWG77E`k`DKh zy=EeVh~P6TB)SrEeltJyFZO>jQ_9x(;x_+!F+EHG0N(#YA;~|}!G97{H5{@@*ki6c zk1+#hMg!jjlu(5f8ww;!#TJuBCYq~Zs9hw{4O4YfkLt}2z*!ZQoy%eXoJ zQj9B_aiFDqEMlbOEKRuCp4mfB6h|tQc5qh3T+8HKt=BPXIITyrWy?5ig=<)J@LRIc z46hGL7DAgCH(l5`Ka)f9ET{K2+fHsdhx+PZTg}rT_3rCt-?O>5ep>6`TVG}0wQ0=f z-nN`)H{x0?Tdm8Q7Nq7*slVexCWT)Bkxdmn21Q2f@vCN?=W8Renyq~;9dH@ufJ^DH z{4G(^vHdlXW&2sa(VF!UHp#1-_PM7?lY`>2EdxP7(zoXLH@?0mY}@A*AH zzvQUf_Ze>4dx78YYV6vJ|19|KZ;-nqVGIE$JKbF1mk;zZ`m zPdgmKjVUg3)1!D3qM!osIdedS5^>-tPDE3rg7>s9G@Ob9LC!1ucDyn$z4N#sz)((HZ=a`HR#3;eh)jT6hK`=IydJr z#&B|FOV3Te4&l*|d2LRLXsks+79j{s8|Bs;o0d)oNJnrfAfHw$SpEC{D*SC=WJcx# z$XPdwNKDJ3F$DIh1?UlR3H5=F?g>yu2{AoQ+`q+LcQBxf9eKdgKZ^!1@=(c++2f}x`+!Hdp zjXmJ=P}rFfsk+{*s&gr1|AeGY_LO+3LnWD0irXvHcaHyFeFHql>QgJsHRP8?G~^ zT{|T`UZpL4V@I?uR38HXPfAZbw!$anx7R~lhcdH<1MG0aeru+%OF6lgpFBrre^?Fo z#oAcfQVA@DOkxnU->m5Htb&}3`hr!JSucK-1L$QgH7Hq|QdmPKwXiAxu5gnQvYmHP zhic2b5cy|!k7_fYKccs)-{;Y;jD*`gG!3CU5`a{r(1p_soaM5-JZA|F>FG8{3w9BiG;HZ3P(7q}Ts_rzt^!c+ z`WuzC9PyNTW#9Jd1y9>mm*7WL3E;+}m(InI$8c<$G2ad<>Tf9pBsR)05^E%QyE4*v ztEXB1(UmVx&zdi~D=R?Q=Fk2Sc-@shJ?eEylT}g^P~WP(S5gFyR}XI&Y~Ky$Gt&Vz z?aiwzMKwG$8U;-&k zh-~sNK%^{6m&Od5Y2gP%>6(VJr#fiyF>=X)QGGzdipbWVP|HIi87rd7)iWt7k}&&!i1mc`k}$@` zKh3&(7p+Fp`sY~=tWncpS(tO4?MlaUrg!fTcXP2pm488qFQ@Owk9PH3h{_4S#suMY zZ0w!l?-wTQQ^6pv7b1EYq{t~s5$1Sre%qgK@xRudCfX)6{~=A^oSEuv?659Dze7p; zWXq!ne6vxK^K1BT8SGeILB%+4V8BeG_nHv2Df>K426lf6%M3K5U;`nSa7KUcT-01r z=I?Fo1TNFk@n^-w4VT38SV=O1Blro*$=8cwFFgtaezD#a??LCtxY7r4ic>hs-T_;^ z16m3}pA=P%4u4Gni~FHhnGvnBV`e~9 zuDZWQoRBC8?ix<|3D^WT`wyVxj%UE4-yUjKX~|*ALZ!cI2ub(cKQT3vusWSC8<@(B&(dymmOu(@p=zw`|sdT7!PIrKH7V z1|nwVTBLv(C-Nz}EGm;$)^MDsMTA#HA##x~P`y3yfO=N+^?}M=Fw4>w1AY6JrX6vX zTCyBZ+xb2Aff41+4?Q-ta=q!UwapA*ZI3HSRt?b4vSMDK$gEqv5+RN}l=?W2=dbrC z|7d7A8HzfnZ)ns%(diFRrO_DcGt7`+hMVYD05g zrL@u1`xU9Qk1|tQwmC0*MrA!|uF#ib-E}3f14}LKX#8m`v78T2(m(Id&6xW5Jo1-& z>C6lrGfc>*Xxiq3p$1;KZ+jA%ThTZBZj|n;&7d4Xn|)v#8hJeepKRvzWC-ES8bF)~ z4)|6CDFnDp*8N>VNOB&q+g;&RKA^R}i0o^8%!|?&Vx1XwC0iGNzFLlz8U=Y*QWCNz zRvU8l5xWQcbNdZQgr+VqlnQA$;z`6s?CPEpbbpN!fLFRZFZfalD?U!-5NQKdHG_LbU=PWK1X5LdA0gLmy^c>6w_@hUD zQF!wo^(q&(VT6Me8!vHQu!{NuzdFZ#ouP!8lw3H8>Ec9-iN}iJ&VAzW2VQiq|AKBl zg&nKR&ut_du@Fv-`xt?G>jm{y+Xw)}=-uJ^!Xv|v2cSZ= z)M(Jl2%)RwlA9TdNt?^mB_y{pdUXhpE?MjTEBxzqtYoC57bD>w8Wn~#GQ%2Y4I6bc zRVK03YBqEJzFEf=sOYxf&``AL(45pIDJ@xR%Wsz1P`zhO;s$eTXEy6uOs*0jb-jU< zq|L&Rp=~3N#FxAVVDyevosiKJM+68k=_h# zc)Uc>3!Cb72&f+gyW>6LHyFTCeWx(mL*eN(r>KB?stb$K_qd({sk z16=$G@bx2P5OC>J_F16K%}W5jks89u!%V9^_4b-W^Bq#FGtqZrp2q9WF7W$+Cqu?) zc+*_I=pvbzSR#-1ZnsWjCw3EaTF>`R%upnKT#8pWEyQBPT$p=6w8^iKl&=71OToaR zAxqDOw?(%NbwmkP3ijFNFhyWS=$6wwPdjM&CpF9d%nlt|7_!g zd~nAy{s|0V{x1Xu|C?1BD)RP6A}D#7zcH7k8?9YasVkGpHISqHt>Mj zpYkcdw2xK@MijOvDz9hoW3xtR$bwzraG<07A=?J+^1Kp{Z;`hWmd2v^+z#+6?L9h; zQ>kK;qjUC628zWK-f%$Mm zE%s27fc(stDl6<-*5G0#>8xpplcMx;JjaJbJxM@QV6DMk{G^&4J!GbY-l*@ksji3@ zU0JWBSNXCtHX*X7>ABE~(Tu2Gn-2<=GXWo#reJlFJm#YDyTyrJinhmnXjaNXSi7GZ zqvv0bZQa{L5*ODtO{^*$53Shjlnan3^WPN*aB9kCu|p}?DzpFw{y|Xo?^rIud$bJu z7lTrz|L;k?GN}WWjAw)bU^gjA)p))z6n2gG>c<|Dq#HQP&=INZ9 z4(&NJ^5GunX}+;7_Dw#jid(-lLN%WaeVopkLELPDy^v26)8Z&vq7xQsLvWJX) z{crqYRX)V&X$&R~>?TF&o>^X7L>P9J-wDdiLjJA6TJ7ny1t8O2_J}YG(>_*8?fm9E zF}jOa9RK5$-pHw9io&WFjLAD2|59|`<(RFT!=X4FMG|?;d#z>RrZ8hggrC0{({b7S z0V=`XKF|BsEcB)T9O{;*I2UZ_Udwk~8U$>2xA_7*D0ioNo$JN4$FB3~2- z?{-y=&1#!eyd}0-MM|R9(QoJK0x03KrjyQ#n-r?ld}Nr?c*Z8@t2P6XC&h}pxhUBr zWqa~Sc&vA&C9~O~Xwoi_lh{U?1DqBIwU%!o1@mvQ0Jxl@C;&5OqSY~YnJ5+u=9 z-dw7F3;_24vi zo~tTH$kO4h#I?ZJt)V>{2vCp=aVgN@9G5_trMcqd^2qb_a_ zw##8Ke6X&Y85h#h$1xH0rYQ+=kL^d3IajDpcbk#6fty4~xwcyDi%=R=*XwWc$0p^U z9l(mKAa?V?vW5?vS&)+&-z??=UNw_HZpx+E>tYQ2ea}DxHE82cTXR*7LcLQIMq3Ss zRHY?9#1#C#S3h%$| zjQ?}uIR)*+G$q*oben^sU9IuIA3GF2007Sa=C*&D82$gxqk4^LYk$=G-sJ~+;KOi2 zA(#C!WY8}V24~$Myfx$rdSe6vM7zXB?`bGPWnR|)+j~YRn$T!7*0$phl!G{o^xoZ< zq^E8wmuRAU*?A}_BtkbOg{&DNG-Q=vrHB?})}nGFdA&7OID3-IAPU8)LCl&0rQzV8 zX&D#7jZEr6r4dRYCDleQ`jPJVn#xSuq-1Wync(FuWRY=!mWfKOF)*Uq)jGFiKdod3 znqXNl#pH$JnV3eME9?7xZ{PQGPJ*2Wut`S^-l)Qa3@Yz30@XrS<;{Qr7Z!A@QPXD1 zN?g6WEZxMSanJgG@tM~v-@XV`eCC3S+%N6Hf{pM7undp0OaX)8Xi%(^#JUk~u7_ZT z7>{R^+(PDD|MxgVy+n%X9zvu7Fk>9)OW3}Mf+>IGYt=g_S_DZk9zbeA3`qRZN^Vs} z>{`o_H7_$QQWyaR!dV%nS_-=sxo9;?YATZYgO3c~$O#|WxO&ZXNn1{@mX@j4&ta4B znN^i04HF*3MTo$dG*Wg{zCIRvS>YiUSSwDhni{BP7MUD(0P7phbz>~%NactpqkYqU^}t2c!81H(Aic;Lb8?O+Ds0LxkHQIh>&gnK2QPotvcgXAeK zPDU**2y@JO*9t@{=COkT!Y^gxgl3cZYLF*Q+Smv|keLDiMpG_~b;`Sb3n6uC07`7Y z2@{CI*P1#F`R$Oiwh)t04D4W^gnnoveUPPE;lbj*urB3zP4cn<5iJkl;6@N&n3111 zy%aT@IVt&HDUs=9(3=*R+)#98djzH z18(+L);3GD(cXa(TQ`lKinwsQiecWTuxZHOEo*9W8q{zu+^dfFk02)5@E1{B3nH^DT=JC> zOJxRk9z7Z0;r@%a*J?pYIM}>xDBu|B-;Ev$Ko`xTJa;&svWV~68dt^$7adJILxX1QN z0P6$pOVSe36FHB8N_{3=J9LCWi{~uy69H#3;{iPTo}3r?n#^J zP;^lCtCOXD)ZF1$OgG1Rbt32sIL{5XAR~J9(<>F7e+DVtxgyQs_c)iD2S6#&ow!Jf zV-WxkITUlyew#VxFz;E~#N4918UX1_*f}szbq6?<@!|dorRJ2wRH|di(6)DTeT8&? zp4|2!WYgj=RAb4@-i0v(!$_kd5J(LILM7p|!>Y}a`lEKPtLz2+*!kXpN!RJ*WAK+3OhAW)LeeOkW+9Wc^vU!7R zy3Mk2&H#*?Muzy?Vyd-31~|iLEm!LJdd$+(69Y+ti8M?W(NdFDA_02)yJHKKI~$Ed zK!->`KBSuFev07X-Yw;UJ(HVqa#Di=VR*Z-6Br=TCW9h58U;K32?r!!GDlN&K=aqX z;CBp6ZH6}5XTa1rkn<<1GfadU8_k&@wOpL=jIb*U+rDA+&>)e`H&DSMxL?EJ!Tmcw z+)~3fRB2{W+*2xwgZ7a$8s^cDL891zF6299T+vUunwL?jZ675{(#;f#+TGtq@79}G zz*J>X$}Gu0R9tduB}AKe%&!W3-wov)Y7f2JRmIwdrKy7+!r>MV&I(Y-_bFblrhM;B zOPYpnbS}M+DQ_QZF|3vUlj8;ivR?$3dK1RpMf@w(Aia#%B&p=ZrXdPaX0A%C5^Q-V zDipnl+lE{r$1ePH0WhMwfQhUNueC>1K}ilTk5v3Iw0wb@ao~t_p=n7jZPoRx* zaA%-lc7OevyUoYP1{>ZJMM4LVKo67nb{=8+?>0Y>nz)U^K?smWKv5N^90n(k+&KIw z)|Ag4R>5TX*z#Y>Df#X~xA76Fj6%UL`beU1siIbR2^s9_pmPrIQdODYxE5Ek;+C2d zN8W3j#~gN^_+BzM1LS~E$2KJPRRBEDiJohKR{@2uDi7k#^+^UXJ!2waV{PhaL_8qx zJNy3z;r|77F4Kl)O4tAZ0;T}~{w?nOU!cza4@`XC)3(?a_iw+|4Gz;tH_Vz!{$vYt zPrkm7A4p!g+nV!j<@kD{KuXFyDo9jnKj8U&s-fB?{w2OPF>@26ca-LU3K3E*06>9? z>ZDq?X`p=p)F38%u+oW2#~48dal{Hs5>duxYgLMck-zhaF9(D z45V5gwIyC+e{|I)?Y2}gLptkFB`+MLa-NXxwYAo}i%(J29S_`4Lv5elT&do-%|3P{ z-U>g9nDl*mn{Xb7A*`{y-bO|zUHdi5LR~76|>wPg=P1lIdtZ$dT#_PBq&g|?jrIKh?&qc zPpQ1`3@Om$`;>$aCaaXH=-482>6%5_AwkI<$ue0bQLX8spXfW4L_1q|Xwt*S!D)WB z8(imAvzL;I4uF~P=nmGS(@-0EG9<(qgZDbKdNIwuS5xMuAnOat771^SkgS=D_MvC9bHC`N; zGMD##yU*H~Qs?h^znGBc2Wn;F{(Fprvu!NC17G~TZRW9hlb$$kRDJ`2Z4&d#7Ceidju2k!^Dsdk2q9l-)MLpROMRTG8W1F(g0TlQMl9WEHL!_lK8zg| zT~=MGtQX}gX>kf3&w5t!%Vwo^p9yZmWoj%u*EWCOR)&ml=Bn~A<(|cAp8Sr&ERwU- zAmQ0DNq`9DyYIU426cRKPGceuuNpQiL86*lQs|;Hc9Qr>OhNL1raGg9qnE(AI^50= z2V*vmbtvjTi-7sZeOxJr7OS1#RNghwfFI7PX?PQy?Y@LA{%H?Q6SL74(O$R}Y#l!T z_pIL;*4wY$PXbRmglH5v8LUg^K2u%?=8hQ!G z(~IseM7FoVlPlsU@)_?r*l$X3+gQW@Iw^Mq_q>XI&F*B+ygz zT5)vgm26KyhbV&kBoE{kG;56~J{33cpLG;c6+kv?CJR#Otc$2h5QBU~^K=rw+b|b* zcVEo4snRI`dh1v$Xbge;S&+X%1Ea&>3~G|Dh-;WG2pq;sX7@a2;}@Nr3hSgY$XAZt zz!Z)X`Nobj(=x~n?9}HiTVNEV=XkMSgAQqS&PxD!0G0tF6(IuSm9Ee*z?HzjdE zMr?qY+gAu=_FYF8z83ejR6=1$OKRz`RqpJGUu+D^3$PSz??Qt{(uY9C99ficNQ(9) zwZlt67xE=V>d59Vah5Z&Fs=en@;D?b3(FIi4XFYX9S}Gewj?Z*A^_7bz_4NhSDX|J zBm;C8^H&LKEqVapG=QjDNn(A`?j;rlTq`U5&AK&$G6a0$8xci-wXj)PVA>S~7do{* z@yI*~u|XRJy01~g+Y}6V5=GDR&{wBuC8R-)Aq zdO@G@qL!AGR+J8?#<`4*C`ztD6^tMinuC9R@O^_o0^aX{5a0vjdqRE}Zqj2#oq6ov zih+->142YPDWQ(9Q9`H4s&eBBsR2U&2&DCwTVvPE`qEtNzNgse(59Ggk|o@K3CK|@{Hj;1>^;8{@owGuRl3A->a zGvxf#*l70;q^w6n6463}E}V`;#ilxFZbIY@QZtdEqgAzrL^odyD7K;#ki<3r=;lGFl@kE6_3Zt z%*53lZhlEh67qx0$4XsUZ*4NWpS8#If-@ zzo{v|%mCNL@#I9ethHhyAjGDPu?l;2-38rr#vWS%@`48RXgsXek6_7rZ?I|urMHjw z17RQPPQ(S(+^zwRkS%rtGQCbBc3c;1YC)Qs5uLaeQd*6N(|)<%=lt>zUckSm$8#61 zF4vK77p^APv;}@>1>q~qj&+hY0E1Exl~wc*T%wpuji?y_RIqXp9?U_$Fge7~FL#)9 zQjDk~$`~QYhhyQ1q(Pcl19P4Y*2QF_7C0Lm&5{a5h*MIZURZiuK7N@RkYlM z@C_pBsM!y&V}er?Ah|ikh7(Fi_bNzkNvCQWzZiIaRSWsi}ujPsXb<~GUVPDx&9MC7A?^` z&J_L_OGCk@HI!9VIi=;7!X&OM`j>}}y%`{>1Y0SPUKCZL+FGOLhzizb`X$l1UWA^y zp;F47I7WJrd-x`cOz&ahxc?y7P(%i^W6_-@AcZhJIqa$4B2>{7y-Y{Ejq&bmDnJg-8Ia}wG1L5Q^IvRLZ_lRrn4q?Z>D06vY zD-#Vi5jrfmj+P^12qmNFzP}+KM4R9t@_Jxddp4Nh@ui?_cvX=2%|8Ph3zsq|x+W}+ zT?AJNzfguR2u&TxO9FLC9OG|8%J2G2Ul=as#T~(H6Fd?Hoj1S(PeSy;;fut-sRiffR9pf5MBl0l%LK59KaR`gu zx7$mB1=&;#{0OA@r7Zmd5s8!F+5>G3lA<22u5fF=wZ+vBO5ZQ??#qMi&T znx+P=)aK+!pg+vC!1eWx{hv@KGN@V^+L=L-vKk)ktuN6GvP1=`4;1y`os5VBMmlHR zj<9lH`66|nC+G$n{vCTFtG=j)YqmYoVf-K=y^Be~*#e?U-nQ!Z&*Cv?TG3+@QUS4C zqn&EdX6d8-WkxRhQ5d-p;4q{hZtB4C+>?Kly{ah)JCk-F*Z6T}!mpr4Mekrbzi@uunhES22Qy3578u++LF1a4*^D&feQsLE z7TyJ8=zEGL1DVA zQAXG~2bz3lCSiYwq+_$KssHg$|=mKT|C^dfR9oSEe-oV(L zp$Uf&CG`)EB2~$uRj;`a82U0}{zZ+`HXw9XwDqvNZRyV9ghGJv&kSV#Sc3(|Nhh%fBH3turb^&=XAZDbKLb7YUigVH9iOZ|y>>zv)MOJ&kQb3H|^Uo~JV zI8N#v|L#&z#;Pu%+Y0++)y1njMf5ssj$rFm`NuR((!%khI)>wSZR0DBu&s^T< zoS}0+vX99*vn%(1hLl!%5=gkrZ4DF^39=J#2{s2PtWRi&MHq3EAdXdNVwzy5oOE@b zrI!SnWp~v+XiZG^)_Gms@DB3tXP99J;j4Ncrh&bQl4%ZMFhM(N_X{8jGOw#I3}8)1 zbMbhIPqCqn89R{FdZUwlMLzGTwmV$`mfo{%G5R};5bE1yHAH9VVL(AaK%q+Jn4V3P zAhcS1SXYQrs9Icd-Xv?~-@}8Cm4eLJAGwTc65BBbQGO6dWkn0$N-IK0wG;=O9ljffY8FPXHiD z0!JFDgaQg+aa$lJ3KOe}>(<;4whJ~^LbZ@TCC$vICW_*loMUjR`z8S8n}~-K?}htW z*RBVA=Dg?H^;nXnJFk0cs7Y2?NOQ8mIoG7BT5^R%P(VRn$x}ynkcC*Nu1Ol5$F7Py zCny~=st;N$1)zCCBHNe{y2FyuT|q7y_&T;`yvm ziZjLqAxb`mqfrCt+u$Xtq)=h<@+uJJ9zagR+3ZSrftuC0;t5)v99vkD>JT(*reDq4oO!!OnPtYix5Yacdzmx%(g{MVe3eYV-?oWIJur&8)JyzVLzwt=I^JhTOU-%ov=g(Qh7T|Xmq)26} zDq;htDoV`b{X5}4VYyT%{-`$RVa4{c0=m&`u`?cPLLp$KDpzoqZ7(rR|_9^e~ zohYZy`Y~aIA9>tFk`$Wa)9n zI#9D4x*gA}5D4njb7ChnP8@xiZTRY2JFioTMa1xC5U48 z8u;R&KeB{wAmb{8g^h@(In5&*4D44Nrt(tO-USU}SG!`OI45?g7hM9dI&1OFRN z)vPxKM7%quW;-`{JvMXMvw=R2>H9z${Jxw&6Tgg{g;pOM%f+`k4T0!3N%uyV07{Za zUA<1-ytOuN;Sy@A*-=mkbZXy2RYF$-P42D04pH^oesm<6&zUKlST^9PTdUksM6zFO zKl5T|OM!WkO(~8cED}CF^I>c2x+ud3EQzpxmTt?Mezy*#Ns+(~_=w(rJ8!P0ey(Z0 zm&*yns|z>6cZgTvuD?7|%^{&FOJ<%XBMF$}|Kn*CwIfb)JE54;)c@j;3c$_=cp{xb zEr_RVh<}=EQN%~kZ&oyk*@G{IB8dMU#`o-F&x789J5~a{i{J z=kG*25sQYdGhatOYnUz_(5NX>ai0c^+(>$y#Px$MCnU0;_SJa69m@UZ%Dzso4{TQ} zb%RLSLfYV~1F($kY>_Ie($j+nPt^Dg=NBuc@6E>LX$AcV95FCL>?mmtPYif9s~eJi z+7WGN$J~3z#3Q7Z{Sk1rh!f~9-h`18QVRoVkT$?e?@67k_KFCBi_7%;{RFzc_wz|w9{<-`6I%>g z+7RaoTG?zSc8h(qyzJKx0XHzMDmhgf|GYuThL}@@!!Fixv|l#nmad)RV{gbLT{}Mb zm~i1k7ihXbbxy6pyX#|MSS9n3q>pv*qZw)C2%9TT%gMwG<|GA z7u>GunM;aByFlU@A_{_&RuJRmE)%9dgRz8*yb_Yi9+SskqD;`0b_6Pp^?U9YI8E4?RT~pVsj($=rTxI;8`o*dHa&);b6 z#uLgPo|)MZYy}MgoI`i;yZde2c%3Hg_63O9uq+jmvbNsb`08ABgr%bMBOt}76=YuI z<8C9I5dLzF0ngAe{Ut<4gL_u-boD=xrP|JpoC>Oyqa!7T%+^+z`^|zCl;%?7V)zO7 zd|=?aB6lb+uPa1&{J0T|XD*)(m7KUmrF?%G{1ArrZ#h%YWRA;LC?aM`LTw{c zGsA4sRHFm>NK`Y@GJwwS4z^ut^?YQgp8)2wU(q|1S^!gH(!8#Q{3;{Z|HzEt@LIrf zm$C*(NaswKSod4F60xyuF?Y`u{#YWroPvS!``W~@Ga}rtv$f^wHTe%rxs}0XSKH?h zK7xOM1k#XDv9$6$(2lN^-pli~z+7^zdjmPvAOKu!Oc9Ifgt)8jm4Y zYgT+iDN^Jhh(KBQ4@6Z}NcJxjRF(^Fpx9fsoqQdfjpP<9Q0rI+6RnDoj0)K(J}T(( zD#iqvB;X-HKzmpF@`%qO%hwwqDw=dgRfIAJUjXE_x=WGKG(NR2nC?@N6c@(-r_pf0+1NfGj)luR7#Kza=$AY5Nb z*F^W4*gV3 zqEF(_bkb*4EA%P6mGcd$%O+Rvi;KfUkW#nXkO2r#W!mZ`F2+`BW%s0qCm_{)FT!n$mit|UqLSlm@2VjWF-AvwK?Tm z6rAn>rLXivZ*)Z~Xh2 z;wFwuIiS_lSg!yE@=CDBp08RH(4kr=C%j%m1|Yxg{843sDc-z*g-AT3B%tlMCw3Lz zG?(h!ZU5@PLUqbpe-S#Ly+7Nn5pUB3qzkSJo_veDkCxa*JwX2R_47ILc$TzQm$NE% z1o>^AMH+Y@@Z2}5XoZDd{kMl+j8fr6c3JtAfX81a=cLQOM^TI4#5a5N673eU&v({$ z+=x7+XVbudT|hCmzB4f>%yC%V2U@KZd!i&hh%(gXspt0yld>S6t77P%oJshuK9CG9 zx0*_c$?9D4@{3;a`dpO%l47j+=j$&ZvP?s>0T>Ooo)ERInuF9@&>z43W`zAnX2A_! ztBOXYX%ey!Nw`=X_yVy4PJK6cm=jc)8D*GnN>g1C`h0;UW+$jjK=~QLW9{FOsk6a* zJt#(9G{5(4-I8XxLKtVjNvujiI5~UQ7E0wl(KNeYQhUA)y@?V?HR~E?DKF{>ba#+~ zY{C42B^ndt%V_1){>OhSQ@y`7riS^;Xh&lSzR1jjK*f-Uu>fcvDl5MgpH|Khm|gmS zt#3;?NZ~D9ai>|T++t)2ZXqJHKw3Rp*fd|GgL#-?Hu}Vm;EDu^ftIgeApV7m3<`g0 zsKLUZp)Kj+>fd?)d01BW%7P$iJru_DV`I=iZ#2NkWt+rDAt>>6<^p@j29@#OP6neG zFAnAg0k`Kci1`y@S+AD?*phJAj~LaQEBN7p5`$-?9P-6_gL8R^3*-o{H4Pl|N7gby zGgIPn{aHp&zip_LLs6-|JCaZC-Yr|h9Yl8Y04W-k{j3p0Y`Fm{IC@sW@N^_Rc*#Tc zB*?o=f$7M02aWu$-wPS%**>p#v4m1qd7STuFq`mGMk2D>$08u|Ff@?lkd*?}K@AE* zZ>2c!uiPo)H&elDui6j+OH(!2XqWa3o?V0z1p|cXJS~<>mpF!wM^7YpXlm^tA+2LV zo!LJes;yvK*=xa6CcsAw3cO>~>`_ zsLpzhQ=ZbXOn_rq49$lVc4c+Uyj)f54e8k||!pE_1cR4D^@wht;3*_$N+Qm6* zNHeJbMA24ApB$+-RRbmC-iT{Ha{yQy7pm&4OQh&DYKVy&hmq@ipN*Vb^S7HJA{$}x zoRhDi5f+IKe%(B#XsockRL8tqc*l6nS&7~zD{NA@~X9oDdJvZ_4pI>A;$a#|yvNDcS zN9@l-v&m&-#YWzFcV^5NV=layyE3Q~#u0btCexl$d*DyJb3saV7bj{A{GN2F7!dCy zwGkLrg0u~kf?{mWhNi-VaWPguJqv$wm%cJf4HlmeRFAxxl6)B$%JAq z-U@(vt>z=RYlx(jl6cGCD4;?&k$uA&P?5fXcc_qxq~xk-P9Ss2w+WD;6PWEv3dd@SMBy9-*6pH*e6JngnhTMAn1x!Y{T}4dGV6xC+n*ER5S8xq6J^DIf2)=yv@v@qJj?k2f(2}dd#k&K`oGf zMTO5CdzL(AClEfW_ys*fg2~~eCw|~t^D*l?8d)>ynRVIQVFQRHt+|uvNYN&~2}(3) z9`dt~QcumS7Pr}gXheN8 zh2MU|)i3VB4?kTY{PEqYDqL)GjxkL{n z(1-xJx!CmLUXyw&p*nY;Jp6)f_*8Xa7>yAwfTeb00J{V`664*B9g-QlHQ|jTvdW9; zh=1N($n6=wnR~|^E5#}d>kZOumycEaIowu))z?vBFuyq?`B*)AD`Ju9tHeyqnVuiX+ z4XIYH)84*Lk?0CVTCHBIaC2?D@{rbGqiE92Dn80#r1&?gxi2SIS@Z&?(%}CvvSNaj zU1wx*i9s9cN;3w8x3B45F$nJbJH_D<1z~giDrh3ItLuoU&{-bu%TnBn8iVg`f`#9P zA0>oVEC+6z^h)M~69#X$Ct8IFgVGn5XScD%w@f&?zIGprFp~*{?{$@l|Jyvs5{VbS z@tY@B%p&G9kZ1v=H5mqNl1WE^1|r9&J{xg+_3M}Jb2sa}NK4Z+#duX9?6mv0=&)L_jHpU2OiZDc;}&g!G(^Nn2!9hA#GU)JiO8+AmYvK8x8<@XjRSW6oPD_%U0VUhe)t`kjN zppJGwNDUI)kGAgKUY^~Z+}+z>gpZzXX8-OAO0&wgro%@6aTW)f_Q^bnq8I4HXPXL6do|QGSBCm)&(Ch;I#+Cxm9B}xC1%BZfteI$a z=+dg`-xfQsrZ4^Rd}ScgS}ALE+}NBy{mS%q^av zun&!=@Vw#LOG-DJiKDd(=8tm_3%9OhlrIH_a;*N?Wp!l}DM(61AyfxAO*}1HK8_%9 z_mdWn@QACt5f ztSyH6LphJ%`@hgOobixGiGi$#Jv}e9w(t_K##Uwyr6T0eLN&=Ma8~q3Pm1BDf4rR~L?5R34ze3Y}EHFD|%ryYkgx@SCjY9EIk>ZLu6tlS6 zppwf?7H9tqX#nnheWoM$MGt(B#fPq1fM3`DG_!kRP5^W}+C=3woq9S*-NmMtx%)_z zWj~l2A0~A34hD)TuUv7LzI!`D45Qd}{fFtH#JzLr|P&SWS;UDRifKI%Q4<_NWdwvXPNENvLpu9wg^$HWnp-D)S)& zyed7AB`R8_BmS`cVO>ZXS{B5sVHxn2@QIfirgBR?&DS!G@zh&6ll)Q=XrNccN%UNq z=Vy(xvw8W_ULs#1s<4!Aa7qYt^k=B3?JSL$?S1d}sDWX&2LY8w(%vrhU?9&}7JNz+ zT-*%!KTR3nz3$0wPl}!HKEcq{hy$9h#)KXS_b6^A;k0L^eW>Gyl9#AwO`@KjH(>FJxsk>P}!8M* zp7A|N?jIcV5lrNip)M@Eg+Gu{!UAi6!FU3>L|gSI%Wli;K!=L?OL0HP@Eua7J}^we z5`G9kl|&BZ)s7rXl(Mq2J1O6fqi+y$MFkj=nOtank0ayhgvqvwbmNhh?gz##W2moL z+b5Ro>DYTo(G7x`D%Nq}vj~vf8)h7S6lBQDMZI5Xws$UsAS+_6)l0P;S+4acVm9K|&QJ)kK0>rKzF<#XE*48-|c zUMBvz+E5(Mky!j}rZ7-7Me8AH{hv4f@FsQ|o&9Ex;E(xZ!whVQn%g`(GQFpVu?3c7 zJ)v7C-xN3lFT6uvF6lVF$9IRnVH+^UW%luRAEzDAN9JiNw{)`ae%D(qPxv<$Gt`Dw z8g}>7r9(i3<4$o%|o`>wPW_%yv0xM<+_Y5oDz5~$`Jn#q3rC$fXG~|+VkQVJ( zQb+hIvxCi|EUTamClo6e5@$2{2nezgcJ z)*hNvZ(I)hI%U+mv3wGFxuZ&H=ZqJZOfd$$Di-cDF}C_44$PkpAlG9Hzb~{o9$sU- z<4Tmr#Z6N+M%hI)*Qz+lUIIs@B*-r?TRAAE)7VBZv@uz`L94%8vR$73?!!-x#D?L+ zkLxjBkr5|L#Q;&f^GR0<6B$mJ zO7m4QQ?BAeyoRwfl;u(1l8oHv6eELt7%o0FVwfnIQH218*_ZkXiM7(v`xUJAkWTU0e)xC&uarAFAPa z-YrS4w6kwkjwk1c`J+&6itj-rsSoFcht4UUW)4PCYwc<$;&1Hf%{f=luU~%*I}jk) zNzk^-Neudt1;A#l%~^paI(R^GMq#;y?G)65NY5-$v+`O!Pm`3~!B8Oc-qRXEb#AXm zL6@aA3Gx+rm;Q;CR3m2Nr$g<3OkmC8S*lMVF2ly-N$~c)c`U5nN85g2+4DFXYEq;Mbt5SBu!qh zs|WvTpN!o82$cBvGMJifecJ@rLX{soh7G^fpAc(;v?~dDNPG_%kSYQOmUl-TOh;)80`$bK4 zOv6G^)UsxBKL^l}1!D`Ca%ROITs1h z=?F^Qb3y)uD-^h=D;o<9-pF)vyx*6Nr))C-(B9LcxOjSz89iRud)3nJoHUj!P6+8r zdy4;-Wq(BsD^mP=_o{hmD$`OCqc6o0%}P}nqC!Jfj#MOfpTf};*%vz_L1Vq@50|?& zhxnA@#YO2ZX4Lz~YGzPj`=+I1cc==}Xv7atVN&di(W?XKh^ExV)99Zp8 zf8*y>C-6*kzUvjH+RFd+h0P~OUJ*7Kw{Nm=$fJ9YmD9OfOtRAsrZKkw^s35iJFJvJ zb;^ZXzNjSkI+=J|<{9q=EP&oXAC=#2G%wo(dF-IA%C*$zBa8*6l@x9Yb80Pm)Y-+W zzVfhXfCDh>rcQtawb#AQ;rAfu2qm@k_b>Ng26K)ZCsdcy9h3Htp!+^cbl%UgB%_UW>m)g(X{icUDZ6kcI_8iq5&#=XoW~7Lt#;r1!@|dhl^s<$KX`gLdD=M3ho2ioH-d`9sToMe@RhtFc|#txe^Og} zMs7Ig;R^1FHs&o9pkC2xR}GbJsRk0>bbV3@h2pqjD-=q1L+N$uNiKAui5aPo!(l{Z z=w~=4J124@oL>bO?xrrPeg4Zq=;SwK;O^e!fTcX3MyODfN1Nsy4%Ef$G@FZ$XaF!h zkSb`g1KFSd1k~nvk3BhAxka!wcW#?w3j`WRy*bC@`1OraibP6fT|!V=2jz6sP8 zV1X8cJCe+cBpi|T=huNQqaoLS$M!pY6)ropVdB?{ug;UPkj?n8^$ z+%_=^Rf@1L2#y7?`VVc6y`m79T5v#JG%AtP(rf`i-J+sVIep^Sn**<@_o&$F_0zvLymlDAC(Khq!qWD5?wt3Tjj0LEgnwP_Pbvxy+B+ z3tLwHI{D~Rt4|890<-LH9R&e5L?B!m8;+5qgzJp>H&ncy##3@}=&LH(P;x7cf0V); z`sLPLPMy_y6KUp!P_7Y2Lx0I#&|&#%qg-0C>JlOF;3;&1`jrbqe77dB^2@|`?&+P; zb1TrwI?M-OS0HrF4Ul6BXvqlpDVI5{`0rT7{fgoanSWx}CqWrx|1+CZn`U=Aj>Gk9 z19ez7#F&kKanHcqsszMyQHFKK_+a%4!}>D`nuNyb2AkZN+ZD3%U3h}q!j*M@xX|QU1`1OOCv(?% z6|V;jhHczptUTt6g&v`s9RTdA72~gOcikHMk82bz8Tw2q*}Z{=gn%+1Lv!dxi=IIl zcsSV*p0aVr9i#7S%(B5Qg0OoSfmcDhKT$JDKtpF>UkdukO&4u6Iabc zGV+n;(bf*<5Cqy)s$- zZeorKqu~hkC$WN+z9AL3$v<{37rx~Kgpqfz25=_6w_iJpsD8jfWiexX3TtA*YZO1< zh?onvp(s(bQ8wH}q+B|$|11@pJ%NzV9yp;@cmzUM{`(+Ie#1~!&8Sfx&L?53ZUBTi z6<)_xoe`fX5&o4SH}chpJ|>B@FU&BLuEj;0L2=Bj^yiTk%!InM50^0}>Hz(mam?8Q zcx4n-wjJyN&{N_F-fThx;~!vtB0n@U7|Skan3o8G_m@)SW?}z9!WNgYWwv^7G=%!D zJs*U>a#_83H7KJ8m3wk$#WuyCPdH7E_(GV!JCSzIW2Cqc{CsELiXShyb{1`xj3=h4 zXJ-u3yDJy~Y#)c<)k1vGTwZU0>9U*QhHTYgh9N@`bRN5FulV%{n=zK^uv0w4$FuEB zi+^?R>-IbVEptJ+{Hu5^=tulHahv`G$Y3&ag)gGq;eC ziiX9SIaLDtPU0aN4hU!G<3DH2naR`!+9Z$3=U|~Q^p8AXS>Sf@)MxsIF~D zxp>qJY6UDGMsXQh9L)@j`cO2^Mzmg4*6K`6HmX!m91=Y8KW|?!c^xq{X`!K0K5wC# zx%m)xSGEwmL}dY?C2JoLm(yV&$uDuk+eyCsL*Rc+b=_Oa^jiN9`2R%P{?k+8?9Y&# z@GsI<|L4GH;S;Nt$_dgv0x>711B5|rt~q@JAV z9>r)KXy~SQreyEsXJ_x=;~u1MZ=7rCr0)d6j|T_L4@^?~2Tw3mb`GnD$E8WfMeF`m zZ*Y!?{})=|KU;*|Ya!?6WjLS#4U4T36zA-QdMgd$C>?MvOR&c6~q!0z*c|821qp z?dn6^g#;2J(3OYH+4YASg0<`F<1D4OjqUcYFRAD(sVVJw@GhOOTe@fg$-Viyyutn8 z6M2hddQ#Z?K>k-CBSeJS4^lk(d*n79a&s@KGDfof_%yE4f1Vkaxzgx~Dkhs(Et~47 zS$8Xad_tC^F)7#im|!J9$ljq}dW{U7HDthb^G?+c1{&TxE6{{5?L3-^gTp8r%Ygorn4lOhQ68Iy!uZO~x% zrQ4Nc%V!mLOgA(&s%&t@?`G0`Y*rANvXe&Duw@c_!-)GfyKeyyjO-%kA=2K%F>i># z+JyUD#k5yX#c_D9iXyp03QkrUgYPjNnPJ=ec9h!t>9_z(+ojNMnrZb7U%TU+ z$&ejGjurL#i;R|b?;baPy`Hqy1Q%q0lCUxHZ&5p{of-N8I}K!-U~C?I5C|N413BZQ zs@&wr{=5T-z2anBDN-#Gsx*03`vd2-KH2MjW4y%Ru36_Wr8kc8OXuE64hoIrSqBen zo2pcFTT@puYd$!MDqFwITvam9AWZNsV3_9cUw3K4qE)cp`I0F;VJSH+=g)6&Ubuas zh!+;F5bWA#gJeN*Z!DH7H}db_oV5Qe$q%x&VVjNE}fD|)afC|;*JY7s-fTgx9( zJgUHv#1}ajLu#D=7+A)DHs%H$0&8aoqSvj#6*dgD!Reku;5HjadalXM0+AT@ROW?F z_>TCo*Fn3N$rG*+tjvkVUBuxqVvLZbuJjq66X`ojq^JFQW%pWI-2|OvO;XoW?pvt- ze#vWrZtlgkst0DJ)~K8qZSjQE)R;TRk4L|#C#2#&9}3Cve73zQtEatGf?29fF;$GT zazOKJz~HO-5zGHMC;LH2WFC`g+ZG|!p?@@ZsvbjX#?AX)4fv{!&E>KE*_nCy(f^a# z#H@Qb&M;rw^SxGCOWwr}0`ausX=~^?IEM)F1%Olr*c!@53mTTZC2I6n3`M^|Ws4D^ zeE#{a_Tj)rfMmJ{Sk$GYuL?{v!R$O0x`dy~ve>Ua4LO^H;;AD_wiqXwZ_DsoE-6WG zr4mcz41zHd;}0DYN?*?!q^7Z4u@NvQ5ZA)P)}Ik-!OOI4rH2rDej9~VJYd09@7IpZ zBsof3C9PZMXhmt_5e;t|groKbBUBnARBkD-={p&%&LFB6U0dER8Mz>~IL68?if=K@ zrTcR&Aub`hD+HRbVlL^6z&8V0Gv@qlpT?qGewz%g?*Vy&`sqjEzjI7%VYhoD0n?LU zt)HCneM9nnOwDg*`nZRME?K*jI&wuQqKBw}K-Y%><1?oLkD?Ky`EwxO>B+L&g@*yQ zBehJ9j3$qooauH>gKVy>h6u)m+O|SHM14xVuLOE{8mW=Wrnh^>ppOC(C&T5iw+>Ax+kDFo61j#^+JcU-_$-j)! z=mKj!!{O}$^@9i{so-6K2TowU)3~{2XAcyQ$KKfl>|&-%^x)g>n=QJ_Ql9p^nLZ!W zPHsV)+i*S~QMLPo-2(M=2SMtv>Y-tR-j%|uRwJkxM8VV5PUQz^(0)udEzj-D`@yJj*ggwbX#HBsBd_4M1!+RWU#D z$k+JC)VNf{N1MaLd9NN>3RL(&EX5}MK{cCL97~GuX(r-TqDVwNFBi%m5G!~>=He4A zP^{pW1&xb7Bu?XrfW}s7Ns^8wY4|f4m)obaP}}nL4d1TlcLYL~JA~c*H)Kb~CTjxN zg3mu#&hQ?UOb&k-D!(Ri;mE)yonBEn2|{2{~9=qhv~HlH;JBTF)>3(x~O zio@(1NsHNkJ)1PyH(wvD()0@csmZxmAW0j+1m~*{ZmKBIpQqFYPe)qw66i3u%l#VxZZI+f= zg)Cb(a>zfN@%%%J7^&PG>?9lD66p6j!zfJgJc0*MLM_3MnIq(|dok#Ufe@RRW6an1 zgVq;abXr!an^e}*cBXy?rp8<9%8;v4OJW2$)Rq=+Xb|e^YWPY9@+x>AWYWj&IS4mz z6gXQ%=^I6R&$)a!sjaI}Qb7G1wmGq&GFM0lQbwdK$&!EHodL3cA)La6XbjB&#tNP% z-xpnyDl^0jTd98dMXU1 zm8MFnC&(saS42~rvl>7J5Q;)DNyhaK%_kA+)Iv>|jp>y=g$d}~rRD))5L)~hS*;g4 zsx&9sv?+q=Cm_Aj^P`sz(IaEIuB41u)UQh^+n(OzlUkiV+iIeNu(eZZOZ5S=`xy`% zDQ*1XfhxG?a?73CuTT8tB`84*sA_vg_5Qvs_=G|DIc5xsc>gCJ>WPgUQ*!ewGyMC( zvJqB)J2i0OkcV3gB5pK!v;O7E$TP8K_}V#rO#Wt#@HCSAHyd1bI`q3{Rz^MQqLjiT z);rbCLT%w+tF-PIJ3~9bO6qljoHV4t9>VVTHp)lAEw_T%9FFd~T7&wEA7eX{E>{P; z$X1GIlz?TbdFHmOxGaRiBID4mIn8g%L-@ItS1+OZNUo zus6f~JD)64sLCRzZD3#ghErtW?es0@*z7%vXyHMqe90!>ptZAiLa#@PXfqC6mJS-6 zwin+r8$n30@^B{yfrpK7VV2<%d^Qm!K~P$*|JZn_If-h&gD<#4Jg}kP6%7x-drShE zx^uV9f_efz%zT<|XyR5H1gctq!>BXzsdDrYxB=XIv4PL>h>xs!tOWVgBD9|-CwDPF zXyM^)Tv~Z6cv-+2E+}r8*&X(lFZM#`UrFR{QJSO-R^W0t-DXMVa3ENV^Lad zO=lmo0dw!K;jxVCKEI?)KI>emmm(4wAg02`>-R5)c?L4y=Hc6%(pDI%WC(>>3JntQ zb3p~yU=Cmo7!hJ;Yq$Wrwtgs*gY4g(=^XtUy^ZM*2X^wnBJa@n&%b2`Xtc-`o0^k( z9$g=PWns%y;<|q|n{SmT9``X^*)QO8p;^n6IZauU#Xv`4^+y^B5Pj-uQU_py0cS2_L618D;#msUNxpmfwq0$x9rrEjM+=uX-& z)#;9<0G&gm?YRd!fnGec?yzP9T|wa%K5rl-y{ZwB5}y3df3g?g;Q3M!XWUex`d?oA z>omD&@FI{|eg z|GS7*@xk*;+3Di#QO0v#{COd@*?GD5&mA%KNjmDHrzQHK7Z(%|hqor}XMkA`;6k$X z;Fg{bc!wP27_WXId8ZKi6rXhj%MThv#8xJ7i0)r5)LEB!icfxB(@-Mj>bkJq1HjRA8|DcxUU)0T~6{pwk&WE3cIy6aIJUaj1Ahj5U(KfSvVm3 zU}=_}D;|;skDL#XKb0E!>UP1`wAmx^j-QBNTn3)Mf`54PE}S}2iY5s=9K`MT_bon!{*)p?g zMuYZk`Tx)0P7aaYXZZ2v|wLU)>9i5Mvc!(Uxq?ycdM8geL#4Z~# zHWU#DSHGp6)$b(sIqHu9x`yOC^|4-6>IRo%;fWmczs=JA+izkO-!eW$PVy=5mo%a!u><@(eO%Ke~6l`uq3#8FgYGr`JD4l$&1 zR+CA-&Pey~Tpwd|GL6|chwn~je|XHYK+PaN7H>31KjYoNw24VKdd#wlVke&pKQ7|L zI_5Qzo&OSWTK6LOWHZ7m`l#ZBb?au_;4#K4sPj6oe+cs+iE>-?Lb?tf5uOg64jr`X zC|}rOfNzUsiD8Y+9389^%ZSa09anS%*^Y=F&2&1>qUok>mX>KaS%Gyk`L3YZ=_1c8zO4qECBI5+=}^_26^sz^TBqG0f5~g@(R@Fbpn`VvQM07H5r5uX zr+h%gF)_jyKekXcO0+%)UE>*4T?Ms<$eC$SagCcESV+7?;)biwF%(->XyczgzZ85% z3d%A_k(Zw%&6M4%w zdc5bIv)v1JL+T2$EjRZ8?(Q2v+xG#|%@27)u7R~Z>4y5?(J~@izaSrqvC73gI5V&i zc`5OXx_C_VCnG!XgY+4W8}ViGQlkaxg}SOg$7uE+8+#S@QsmV!S;sha_nXP8A$5%! z=^njpUiGhy_!b1bm*7by4r@9U7tYj=r5>Ey9SsKg|;0-9P4xy!r{y)Mx36m;iOE( zaNnpny@bGew%XP%$?Am)PnXhWuw14ZGY>i*bCt?@3Q9qPVcWHT`Gri^yyoBAL(Z8l zcFljAcq`ZIv@r~i=yW?ixw;^YmK!b{D$fN^D+UQ1w{6(ZX*&&Q2PfSZG+U`Rb@y5> zgyL@{dd~!DRcpF|XA3vWRjXv1_f>Ii2Sc_0o$sDg+p0)RuhD!m-F8MjJmc8kk?3hA z-i%4>sG8t2O*?cKQ2s_i!a&Z?MeILVfbP6_*L%$KvkC% z3)7@x_o1SGN6OgSx?cg&AjPmOaMjxTEOe?Ag`8tW=`HkjGe zY;5Tjo7#--?CRmamRQy!3sxK$2AUq^uxGKBW*oaU&ttbV?KpEf&6~KUP2xlNFdtka zn{m@bYWAA2H$<1tns|QCsV>o>QKTxS7~Q0CCGE9TQ^B$Hm{!kBcuvAPP8K*bW3T~> zYHwIBYq>YKfVA6~ikz8-*!p+jjis6+4Ie>i2b|e!4{Q$2c|m`mr*cs8Lj)=&y&2tl zMe;`l#>>EusJEfU=m*>YZN9DPsKa5odc@?Yl=L^sCPI>{Y%6)4#axzKt#{1Ke*CpH zMJ&yjlA-TRd1&bD$Th>t(&dw8N-?)>a#yv5&`rXB>BxiW;pxJkWH?xH3B1h*!JEB?!A z4+ca#xLkt_d~dlm%4-2nUuT^=G4+@u{R#|+3cJtR?=bV`Ts<{|vp<_SW^dg0?8Z^8 zIid8ab<;lm_!64u)Kzsg0;{cE(EKMCti!WDQr{91LqQqR-$BZWYf#iaK;76WVo(<_ zj~6*v=Zz?FD-=ghR&fYQa!(~?<7qE=DVly~jHW93CD38LGd6zuS%g!r7u2+c7Qj7Q zOqQD#17cYeKvOCwEyEW;{0uJo6H zoz;qF^8OHye0%KY;t;1W8|x!JtvuTKc}|e>xR1KvC`EtWG@qhlaLpcmz^QOC<2`9@ z%lt96?lP-!Hi+>HU~6CQg&QZ_A&_vRhzULd6X*95x*cjnb9}THM&r5B-RO+ zyxB|BjSX}gs1wn0niN*s-St2c82%7gp21Rww#`Ht_;C~xa@L^6>obb4QaISBt|RQ5 zFmOp(T6ysw!Y6wmj53S1Dj=1Y=KYvYLK;rD!(3IircA{KvTY@hVlS?^ix=Z>^BW6?$E~GM>(rI!=CHPX$ zye5sfUJ%+I7<=2yjuYiPhbtS+pTJ)Np(+%4;JpHY;inK9D|~XK-aF-Q(&xT3f(GR zA=jQ{B5T_W1srHVJ;G-dT~To%WB3Jkbmrq>L5;=aN_7T(L~1m--~geD@*~DNONXJT zGD{{pcuKb!Dy6Ihk}U3OrH9ZSqUmui#RPKY;2zRGFsI_edl*Ovf17*>NtDAZvxJ1e z%YCvj{Hx-O_ymCY^!vp?L}ed11Q-*&KLmvR4>Bc+}XU%hV73+p2k-o+Ot;F6sR zqqAaiq`w8lqyh3eUkA}m2KU?02-d@a2ycf%{*h_}dK3?sd?c0ARf76Xibcir@FRaG z8r5Ubdpgv4t?oxrLJazc`y@t2?sG_6#z^tj^VTbji^|PL5O#IEG25OM%81I{NW7cz z^UF$R!0^MeQ5C{mn~!$~BhnF+byEwS>#DsOCnjP+1X%Wj-1|p8wIIjG)@oHF#4GKo zcLufAf+`$=ya8)-LC)Y=(5W+a$!vtUm@NEaQCxBzEC%_Z1IDfxpfagmjf@P&3xjE_ zX%eaN`rZ$4C|5o$#i64E$u^_GB9B@De-qkW$A=ATQ0DnX8a`-KJE&FTV6>zWMA1jG z0T9=iH%4;S`CCx|3NY=~grsnVY+Q4lz0Q^SIJ5z*+A4k~1S+fOU0CeJ0vN=N`y_QQ zSnTud1YviDLF*c2Sm@?OIDo2btK}UCNf#20#3u(7cP!k+n@e#}6KciL1b8&*{Gcda zR13+ukNe6>OSw%ESgqlvSB7Uwtuxt#_gC$CJF|C_z@TqWZtn>U1B76pdE_2;ws!GG zNr$F1x?omKk_$2Yi5IZTR8(P=t=2?X>=kHkBQe$5p%#9SKj~U_w_%hq&6F59t5$Cp zV`0hOfp54uk{$LE5zzHzG8AJb#7tz;6Ih=(b_1}Id*@@p>lEuzL8%ACLl`65fR5M1 z8u0mXfob5|fX&iOzxW+l2I)%4(dD1vBQc2#X*B~R!78GcD)GEU!rrYHVD2jf=mbJX z+xI^{TSmE`1L~vOOH8qTrIeQo&HG96&&$^HywX+y0O3SIhD#_+2xT3vgR=Ej^Y{Vw ze;9n=DZ&s03^<2v6|UZ;Zf-*HCD9pB@#_b`^_3=95l3 z>$>K)KnEb9a4lpE06{=)72H_w6;IyN18T_@IZdzt1Rk>zQoZyNQUDO&WM4pXznxf7 zprYPXRTyWa1GB>>%}6#HX`kUT*9+$))WREQnx@7@8k@6ax6ecKik}RtZs-P20dtW? z+OS49>;ur+IA^SItZLk28nFO0zX0UyuK{h40$ve3_?cdoqNgtTz1d-E+@9wt;kG>- zq(f?0KdXxxop1qY_vtpxd1j#r0x=rX+rATfgCdZjQ2f)P@9$$bb#m*EIj%3Dl6Q<6 zOoQ3)2;g5Zu$h>Ji_WFUfNTd=X%L^XCp-ju103YKaK;s&=zx3$#86{UQsD}J^C~%E zqp#s`x{ca4c|%d$2dqdOz$pBv3;5WjnzU!dP2z!KfTyn-!T64kKU9LeCcbeyyssDZFV%xq%V{M-ofID@lns zf=n5+fPd0ax*VzpjL!tQ=W*7+PW(FwDaFLB=-4+d=epV&3b%eAF?rT_%s%kn7k_*~ zx&wskK#?2lZmYXLA=|&OtQ^j1ZcEl#9C!P+@cKI?Uua4obV(se&D0Uz)P9+9MFrYH z4rhkE5X*Ps?mG|O_}yxZVUSvw4U)%)ip#vvEF(CrjVpzZMG~)&NG={|vq)BslA<%P zeKxFe_ek%Ee$pJ}ixwlVr~rU_a>amq^|Z&YquvYa77uARDbJ3Yc=JMZYIUQ>-DiO_ zR-1JgLn~Dad#1pL;rt>#%5NRHla|_HlQgQ?uxEQYb1VbxN(gduWQJ7)lUUM4RFzWBeI+H z+oU%Oa|Y5kZp+FUasxNz0uHl4QlL-FjRiUWcuUhXBu_!|6{Y0{yr9Js@9~3|J-7(8 z4hqN=GEw73_h4T*iSX?-dj302Jj8MoT@hnK=O4o85Sa(t0g<%nOrW9><7BpOmH6T% zA^S9KX6h+Ns!7<=gegA)Y>lolXsct8ewfm@Ig!>$IS;oyBO_<+ePsFt(h%<(=ysqK zYs>TMO@6fylt@UQtmhZ5g+1pSQ>#H^P~WVj^Y(0el)zm$E@Ct-t-oz)aURU%ay?oA z3y53{>mQuqSbaoa4NDFdtrgR_>W-VJxn1WTHg0!|<JzUc;zcM;7zl<_^uQG z>We-t+r(q=2uQFUabiY?i%%UtZ3K0NEJ7=(B72HAQ6=C5cI;mE8Rr>!t+4p0=QUN6 z>}f$$02LI{9h6&%sgf1yRZA&LFpp)hH{%Rzm!j3a_ovA)aoD=W0hI+(+FhI3Q3Q>@ zgAo=5qAl&!BUBeyP2`Rj@ahzr;Nfn7jwN4$90qNn4Hr!Mc3{=tcm~`#MBRy^;vFqp+3ch6xUA7KlgG$JO-4w;)iKv z#^^sc}zd;4Sgk?S`@a8eVOaCqjZO$m#qX_LHu*t&MI9_dBb~Eyj@N6Hp zML6+(@Cq0!Rn(rV;GwqM}u| zMUFrR8hg^FP3{S&4f-&zW4^lu?5l~3Lv=wOrbyy@XS*pil#}MQ2OrjM)qrCYQPmTS z;+TH^*0rali@xO&Rms3rcbkR4I)`&2S+Yk)hiJeu7Ga*6&w-iO#pq=+c9nTNU>!oc z#-#1c(3X!BD3lv}kbIHnnT1d+^OmapJxx=@ohI!*#HuS)G9~HnPH$_cY4iS9^1yu; zYNM@pXW#TTUN9Fvl^9Pz9R5<{zBi0VR6HJ zJ+^^(jiP~Uv*MmrniUE?Zr9;m0~TwV-AhW2Go$qYs@3v2>71xI|Dfp1@oYYP8>&pE(%T zlcWNB(gu`kgn+u;USrBsJppx!eh+IA9)|!wy;y-xah~&of{bNuC&WuqoN7bsx&nOJ zoE^Xgiw(o4y!6#j`LoY>u82=E`j;5(#3&zr?7eKOZc;aIk@e>@d1Av8(vtpKl`6=D zoNhW7n(uS~$c_qu=v~eLHpNEpT4-nI)mt*xwgmzmbyeFd1%oZ?0Id7n(b9 zsHq{sbS`RSc_m^njt72%Ckb0VaLaJvufV_F`p^8b{g?hlML`WRH`;qnT~01B`VePq zi~j0r?R$a`^6!D~$dJVu_wY$G6rm)4he^LIlof(9ETpy|LlZiNU}-l96J;_4HR@Ft zh6=rXcJtcPF!7;~NarwHuA`LW4AByg=mCRdz>$?>!*VRe+=;e!$jy-A@j+pw(DzRQ zVUpqjAQ~0>DP6Vsb>{@}t@BOsZ(;@K-SRkNss!{m+%~@Om1$@y$`K7|oCZ&pxs{3n z8nco;t|U5?-_^>=2@+ly^!&JLPGipB(K(isTX)LM5Xa7P(=mvPQ;G2rdP*DL;+GW%O+-%69jgkvwZ}1`8-`m2_+{LE;y8gllFVdb&2IBQ|wj8OpTJT z5EWPOl7|j9Sz_Kad5nJREh##l9Kb`$i5dcSxs+I?aPi@cY1(BVafd?UwDVv=pnB_Q z14EvZZOXMZ8$wm?!y0bfGDhE1jVFW~YV_M(<~&Yx^UtKy+7lMkNr(ADR+GB*LZ+c? zP0_Q>ArE$vxgjt=g8Wpd-rf9E$_pYQlV&Vi*SklEh?Fq-m-xlzP3g`GANquV4o&6t z;>D(c;RI=QJY*%<9&-Va0_0d}OJ3}4Z)7uO@VEd^Ftl(#?T%79QuUT32^sz(b`W?2 z|6a0z_qj9}{W^N|k5T9XT8a*B_A^Z@vw zkqFU$6R6$q8m!NqQK=3aXueNk2c^)wft@qE4WTfE!4~+uL1}EssGMVw=>gLjq2xgr z;xv%ncxCrfq%g}KmZFTXCLel1e{d5jLva8aF!;T1ExS&_tJ&Utt5TQ@Bn6`Kn!i~TJ}1?wmd>*RRr; zDl|62Cz*mYzrnwuu_`w*ef`+t{R%B6>iXGr1Oa;_~ z>@{t7MV$X#>|eQ(S=nZjzqaf0JUEu|t4>z!R;^9&x}It$=@cKD&GKB^qb{AvqRaK~ zRkAu_p5<|QKWv=M@@R-!OOngw{8(H#s`l1QMP7Xo(1zK~VYqD3vIC+JY=sBr)dVBF z8fRD!KhhOB=Sa?e&lx z$*zY*7b|t+Q`Md`SRSa2NJ>^Z!}sv-*`Ce)^Zr0PjxU+pLV_=!K>DUnpMu##^FotU zE1sJyyr%G%2d0X50*WTt5KYjDvg}gMFx!VJI4b@C%5m=&D&z4w&Xj;Gx8GD+o>}Nv zcxey1?O^aLx&sjh`DQLRF68f$X49pdl=#ohxJ3$yh~B~awkEtIA6PNK}zMC5XqE9d4jvFMJYuv%_Rr5H~M{U=){}Brps2t)P-qkYQs>< zrLK>fb$-OuALeFDeyGZ4sCr_|hTgoZs>N1I42$j*gDvz67c%YI#y!t$Vx`-8zoH1k zpT+BMA5U_$6%p-%aSrI}Y_8VK=)RMe_pMx?G9)#iwkbzC`t@9^f5JvwYP z1u6aL<-G9LfgrBZD#kuObc9aF=uz!KH<{E=7ggAUILU7mM5Zv9}-3i(_K@te=9tfV`5G=R`*VnwcCv(VT za_-z(t5!E_vA@55t=hZ0tFDU^on2jdICNmI+MpYiggdeGbHRhxPzi zhfS-@;pxDCwq|Xf%dSnNnAb?QAl@R%_*UYhmbNBZ_5Elcmu5IpSPxfd?}ebFWFp0c zxvCrXiSQYa0B^{Oppc8-XHrpK#ZGqlt0E+qyc z05Z)AuTRN!+VX+cBQb@ZHJrmU73EV{1|9Y-0SH%?DB8NsZlwPA5K3WdFRw7(U$dp< zN^z8M*JW6G`YXNrA?Tk2Ki+3sh_^15^d@(TQ(X1INk@P9RFwkbrS$uvZ(H)6Re&Kq zwec0uHWGbFH45zL^+0G8i3z_W{UE;%bB_+d2m;tM_P&ulWBXiG0`qE)qJ(y+Zgp1& zy}lud<6~l=MUVwGl$O-hwYvqCq+d{p!i9st{iKo5C*`Ylz=qjfOHOM6_+{?#3wk1e zi0_~zy36cnf#z8sg9lxYt-0x2`1Auon&7MpY=44#;vVm1c;;Pxzz_4LO#3_nI5u1a zO-MJP>XnP533zt@5$lQL*Zv`ckSHsw1U5O8LSjXNT`gm5xCZ0Fwdm*s=?_GkhUp2I|`m4+_z*|;T@1zS4IUpr?$Bwo%KpOZ$$??9Gv}V6;CLYYQapl8m9&`bg=#j!#w*C4$%HW<$=zea=wqfX{|$O)*pF|9)FzYm4e zi0`_lpdde^$bScCOt|<2{S$iPN+Flx|L`0b+D4F(!$A*m5un z52<`-E>ZcgWmb+2hHraZF~xH-uTZ0(FC3+MWMHs}OAifj>l}06?%hW>BRG%e$l<{a z2&Ko34}9ds;!1iBhSm|SJK(!){kFY@IO6h$-PLy34{!`c`t|L3KT2(Z02nc%c$*g4 z{HQ>3cRf@_a}~ev$+nLHT1Rk`cB&SySaO*)uyj{1UqqCG83E+m(2Bo|yTP`~g6V+0 z1EUiqQp?~QT2^f1k3`anyS-q89yG*%>7(tTbsga(>H%$;7?>8!5A_wlcAcn32P5bF zIerh3P5X`;L@@$ND3WZQ$&fBb&>Ln$tR)NaEf_@{uhp&Lf;qo7X!3QN?su9*Lil+j z^y}IN&S^mP@m2IAbQ)v$!Yb+jFw|!Www~ok6X{Dk)wLKrI^38Ry^0XDTPP5h^}W>6 zmod1r+Aei#h?k0*jw#YCrsEnS7idA0YWvxG_B?vcYk5+$_;1cLX3(a05Ir@>&M)o) zXr*on@l@pTs83u9zq60I4k@)1%MHcF(~ClcqmV5YR|b(^R57o7xKY7F+{%wY{^9gN zt46zicflghVm;p;^qH*`0+FEuf(tj4q>WbDIQKa%op!v^wM$V@G5V`6eVId zg?0r6vb58QGUOqHyMMT!_BW^5_FrufE^31N5I@i&)J=*|%K@FrP;6Zv0M0otfy|GA zehp+dj5(8Yb|mL;{amx;ZoV^q%1U+Jzm$dYqfALxT+%5`)qhpNeT9wZ+;O}rXTT;( z0BuXL=<3p2$P-@_<>KxI1*$7W$G|weU+;e@&O74l~|} z%;i*VCepGVvxa0^JOWyjVi~C$Zv{_`e4}9cfZMp++kryZf z(Zy;h<2r}ilf{`s5VP-*R2@-+;{pK7Wn7B>>~DmC8nDjVLe{YHf=Xk3bXD|i_t&eR zGx-bO3Ve`yn0%QH&=I2JvQN^f=bA4$`(c3jVu~NuNXctweJ+CymFJQTT*TEs&}Rl5R9{%ZH|WJ7q>rk_h@89fK4F<+xP%cEK?|mD(CJ zkQl39GS3|^7mF178^i_p)WIf`+3(fuPi{G~_@Eqc`^Gl#ro&`0(bJ3E)S|LVCHdc& zg6Y^@e}|1z@R|o5`HXP2pH@RWz43gQKX?yqEhpcL>2ApMayjYxmTG+xV>Tz=qqQV= z6VgqIANW<84Pe60z3jp<=Qav3DMU$aZ%GWUTDK1cW4L_<)w(SdMWBhhel#d~ua{Y< zFo3y~zL$p?fb>CtN=DqXo%cOVXdGXWm^>XeFxpYiz_n*It}tKD!#4a=@2BOfSGPCn zOi2-4#7S^Kjkp!L;$EeQ9kG|~5{scRIRUcoR=9O=l1!*I5yQTD?F<#iuDgv41}x!6abm=_tWUXt0?U@q7F zg}u{6mINt3k1v*_B$A|LA=MsLw1lLn(JgP+;PaT6h!j$4gS_#TsZ4UNZCt{)5|Y^7 z5i0h4JUnv&Jp~2GQ897_5H#^(OVq`P**P(agjxNdA#7|eVTk`j4;=f0waBIRkRKp|+ zY9j7*9nctR@4giElivJl-K_Y+(?lY=*506V<3aa9EC#4eqs*`}l1v+?=6`Q{d=eXi zp}JL@--T3utjF?jxN_axC9=H)ztUaRJ_b>2zJ_sF;vl~EX&0~KQ#OWr^>B-VYqJYl^4wEM}S#sZt3Ka9}tqqL-#TQ9lc=L5*7&20r9LD zJYAqff_M89yEW~r!TycO6UE>MIYB4!=lXcxkbyz=%ZhyLl-;gQ%k)*b*eYHH{-MID zc4HO;AAHQa+A@DQonALd%4PFA6{K{w8tA<4De7*bkD4Cri2jI%1ncP1J9^bcWfNn? zueZe_UapClN}pS{m5MG!5_y{nr*COUGI)kJ5nyR@;{v~aoYhM~y2D8SUA z3tBzBg0UE%FzD3ke86qhj6N?jDXUmw?+=#mlws%j?%#%-%rnpGIB*6QX<^zM2gy1H z_7)5C3zg=Tb;Wa}F9?XLVEnv^z)vLO{0GoVSwwH}p(}6@)2QO3h}59cG&%6VPlJnD zgPGeR<{*rv!)A0yq4so6`64a-CAy=x*;#Vi+G3^`co4XFaQf1{!mahao_kc1}VTnVEX6 zk@eBfVlq#on0X(jh!7!iLS!`NW=)Yhx7nf{hNkGEbdWk^Us)-JFvv&kD!O`6d0pZb zOfc4))q9_OVjwB?Z$ZE^g~x=Mq*99i0AU%A4( zX4!sKaNXN``no$Sj%}I^l%)LQH3J%+%mX@;h_jS}J!Pamd|Wgpnr-IcZ|d#sZDx|s zJbkZk4txi!Uqcp!MuGM4lW>E3(8lFpIlIn{b}MIROAOdIH=P4o!J?t~-7uKdSnfIT zeDR?OsJO*??AV&2FUaQzvAhN?dD)hR=jdxcq*bPZzKC(TN+XD)6qp>szeRXrrf?X? ze;QBw(v=NAmE@v(&)`zPM-nT+`_dr@cV$wM9(Og_J39K^=Hd5C(WO937K!iVv7lCAwN#Qj(|(5)}dVN$Z@_@$KidC0$P z6&HC}NzvXZEAtDGtF9<3e@2u}ewb4EA*vLOATNIJ5wTulHbq=c+Nk#4M-zYMmHqB# zxj=&D(6-gZeXO#csq%11T43_Pf`9-9nZxEdRYD|%0Zs$XruA@m%cjC7j!@76Tb?S> zV5REiXxGr%1goqFQZjQox}aH(|C>~#i)5Ggc&#PdZL=DD@`vC=IoTN*KKsbq0&vf) zOi9MT(gA-Reuy-*sFzy9YM&0hQung8lQBmG%AQJ zfFbxaj21p4;v)jzu{`?ik+>Ab0C_eXBvA`ImHApF%HfGPKk5fmi0LF~Q`fgx7GneP z75IZ&$g-Lka1JGq0VEw};qf)l#hj5oq z)t(T{wOH%xI2MVa2|WB70f_+Q^IqIh6# zP1I?hUcP^o$*_MWk!!Vzs^yW%o2~4Ohq6PNw-}ECvt39+A_v)$iR8wGMEw~$xJla< zc6nc9T@-wL?141;Ra`gko45G~L8U91;hQw#oVA<@bS7SY_vOPfZ3)L_ z7se>^c)-@v>wqsSS2h!t@SDxB$^D407x(UqIv{E|OS_J5i}yaXdTbBtEHZr&=@3z< zuA_iz9zVSaev4&*ecJx;hhXPnyKuDmbonkKfa|Np$196lpJ(l{`t2@PLshLRjXrD9 z+O|4AYjqR!O##AVBw^nlG9oa|mk6W{z6psA4?ZAAowmn&QSLdjVV}QG$JDho6_?NK zWv6A9R8r1)I8-9%z2{OPDYE(~F4M4SpAeKI#+twjeWI01!3%s~P2q|@#&9PF?$_wA(o^Mq6I2yYrCab%#^o^zFZo}I zpS7}Fl~|40!c|IQ@|CQRUR_c_K~;%+`08Y*9t$w#2$@jA_-1tx7uy}%YGtcMkaK%T zWhcX-2#9M5ry=dYT_Io5v`}A2LpdsaVqCAFhg=l_i=f%XYZpF*Q=Se7D=b(B)(vO3 zfPQB&h~(JJ!vfj3*iu!E036CcL2SX+h{4g2>2#V4QF4{cCvahBst@7Bf@@;P`@Ad* z^c#-ofQ-JLn@g*DzsggMXu0Nw@*H0hjRPU)H9GwerTq|28+9p$OqyW?p`Y(0v4wpU z?72|Vvp3w~9~n>CHwgV|Cvi4o_V@v#9T{OR6|ZV=`Ak`49ht56>6Zqtzl|l7*4y%cuH8 zgiJYQDK}IQl{91qdzPvqz*vA&StA`j9Ip})Yma%wE%_poCsz)w{}^^EjoY}ad?+h6 z-cG(mDP*$&up}zkjj=5XU=KAE05yI_QgOgxK=FJ?kT0EjgBP@OmpH`XfITxPn z(#G-;QyORp(4ik11~PuX1=rPDBxZ+HFXs%~*df65(EjdB03gsc7BH`Mz|y{nC&JurVcyt+4ZOlf=m z2k+WFH=f>}>?L}0;@(jnmSlKls7BEq>HW<-I*8z$0}#hokBoM67MiRrX?8=z!@#@QqIx%UijVi6A7p z7m{h-VV|kPI>#4S8TRAy+>JC2KSR^h>qd{zDk)2wI)gWBj~d1>_JKX@d*uoB1tFO8 z+vlF(h9ijGIXaUSx)yb7)c0U`474rJNfzFc5q4zLHjvj z-+2WAXRfEmfL|#`e|ZlTr^2AUCDVh6o>$(sDR8WN6BKQRoZFFVvVLkSS@RgW{;A}@EGfjVO%Z^ zwUBe;W`2lT9O;?IsenyYYSCvM7`l#3ahNWG{q%8TRtv020Ny~f5LBPMxqI1BctK^( z*w~HdDdHQ8?`bHAu!XI!%J>P%^m5;qXutYWzRc--4bc5E3QZ)|qEOoFSd6B2aafl3 zvG}x_(Gt$1UkP}$OqCJvDLf)8xcM?cb!?l3=tAHUYLtXqHpJTP9ze@QYrMrNK9QrD zfB{?BPrk%X1TNE8aCh7{pge7GsZLG-Tw1ttzw-H->y~70g~&ss6;Ngj9kKL|vP&6} z)hc0RT_UtrS<1CrIVP|1!k^@2Hia8=0uRrQtFP7dXxr^*6Y(i6`KqIphg2oHa#5&A zuR9@^A11r{eTZxV4(8%4Q$lsFrtQl3q5A=SWdFf%CId;tAZkhp~%0*KGx#qB5|Zu%q{D!i-U#%>jqIGj?cS-jFO+`+-BamwNSk6 z36@B95;Hl;wz$)dC>LIEC85uR8|w08Jj=GcV~EHkg?hV|9c6lH>Hh5Y0*)su4+CP^ zL67uWMCUN}FgG#-^%8U36`Ho^I&Y3exkd3(K;qb5SzilZUL`V^C%sB1?)+uD{thaQ zQgg8Fi(!K`>;ZR6oA5pk=eYqWFK@=KuNRa(%)Oh$aD^YLWb(di=7M8%^v=m(a0gew zaO?&hIOH!y1rkOJSX1zkBF5Y=`wt;9C<&|C70Hd?W+~a(yy_nIR6M{EF)#VjS`KkU zXnB(_;e-Gy>FqEUMAh<+MiNNa=qyZqQU&^H3`UVs8f7+f2r{!j^g-@Wk@Ge<(Oux^ zT$2mfcSIs7+UvvYgq`}@X3?D%4f$C8uIVA~R+K1ht9NT2n8j-xM+65xR%x;PpdIll zQX?2I;66p=)40XiyFTBG<=&aNZ#{YERK<5lXPkRevh~(t=WXEFd2|k%zL!#HXVqm8 z>AUGwpPN$Vv$8G0y;e%jRoj6nQzoHJeIqbwXT z!cJp1At{Q3(0Q#rnr9p$#Yx>xIjS*3i?h{}&(ILL<6`WGxF#AluH?HJXi7ej@GT~5 zGxg1U-^~fJrglzI+U!9W-oH@d-nu))hc1QS7P}+hmLd!YoEi-dBngdvaB_q979TLN zsH9;(e7_1)X})xfZNJC#DSXr>AS`u#g_{BgiakE+fD4tJD7Iz@vT_$BJ_7ClKpN5r z?np;BxsV}e_bNeJ8WrV+q9VvEnZQ?{0g?M&FH!-oJ)%axZ-}ndFEh8zjP#ocg9BwA z#>usRm$5-s+lAjc1gTU)-~Nv`USAu=>c#8Rc;K1^gGMMHNZmLaXh*=zW-*gD1eAhs z7ACz;6dl#C=Gsv+#i*C&#arj--)4x|3c&VVsJtf`g()v=hI@E>^olBe&t3lFjQ6A- zk#84Y$jYPm+lDKA#W(D8jC(d812f|_GbNG1+XEPzvrDq(UVhl%xRPxdEcV``c^tl2 zj{=`#BtWvD43G61fW*jWTz$H4tjOPrP@=mYp*1?#R@3W8-z=n-Ao((qbUgD><@;lP zn4F%Y3^bp_<{_&c(W&d95RnTDA*HpYo;hIbG|f36^Ga$l{m5yd#V~Znz%BEX$`yG} znpfGbSWS4D=%_ds`}<&}4_+7Cz5gQg462hRoPGr8HzJt|L3ck&z_y4x5V_AuG)XX( z&)i^qkPo$y*KSJsYx2$%IHUYMqL%9GCGEArBsV^sU(YM;(vVi1nYto(Qj1FSf<8IB z0SFNUiO#Jhx$vOQ$()W?w*o_L?Z?OId6Q&Kg# z#tNug(=n@xtJMn8+Q|EThuTa=5*AJtc@yxr|9fo(jj42E(O)K4Pi} z7?B4cGbUaGZRkqg)1f!3^DZ$6vD{x#PfO&y`I%0@XPFtj_<4ez1k^yD(3SX^2nx7> zym0vn%rICD>If=|NQEYe*?pr7gndmZcq1{BoJeSz&yh-IB9fnr+rapMv6w&)O|L>S zt0)GCP&^*^kN6{|8tDoNUo%Iadx*Q@IT2{e(7HOq93@XSiq+|{>+L2IDW(>?z~)Qz zl1V#ojbqo~eJ6O6@p;8f{Mt;V)(D9apKw0qscO&iFH*VOc$!|p63_VrHD`};)hhIh8U=fSu`5?YZiyYOKv6NK<+4JZFe$c65 znJZCf`6Bho;$E@eBj#aGK=(_YBW@L6`FYdG@mgVmnR1Z{(5QSj-YYVuHY-D1&TYx% zbmEPlog7-wV9MX` z&a_h(M`iSdazyvPg3lBoZW}E-sUl%sAb4)wwR7jwhR@ODAM#8w1p4kiy*b2JFy*4#%wMT(zv61I`VqBdINgvNOko<*}9d6k9JIF zf=aKce;~q@43Kp&DuBg+?_byLGC{dx5GNX;`-sK>y9U5AU$B1mIM`5o8kJ6(te;+AW{>6|Bphg>16uG{P28SaO*Nh%D9r zmQPf(i=M2W&NL*uC%PNaV2MCjpEfmE9n~D8)&P>qj?xL?!?xY`XAUK=-Co7uv=m&3 z;`zc!VmGo30{Zmdrf_^aQdqW&s=an9glWlPEGRr=_r_Odci%}2;32gnx5XpVkHga? zPAE zYR0`1$?3s`gKu8=8=F?zw)6)QeNqi&)jn?d}VlRo_QnHC@8W4i)i}Q*2#HIGSK6tW~BE!kEAXH z$!)Nu!~4ja3(sBB34)5>&|hZuX0xF7Nt)9cH-L)2QA>zBmRAVK-9X!Wa7{|D-Y<*_^J1LD z4{_D{0YT-q6}h+a6r@wKo>!8cD{_=)Q8ICOu*kk9RG|)JDtSOQd&hz0MCgbGD4O+7 zc?FqZrJb6t<;J%yR0U##LwO6Z4Ai7KU8ELfkPZa61Bw?vuz`pY;I9aO2$H|EGjeW5uYk339!HaE+*sqQK+T+3Qvw5a6!*MmWpe0iLL!PZUmSI!=gSL z=4LyQLb_I^vUo#Fm7!qk-g;RP4X~qx0sw|cKgmKUHwCENG_V?;ibadWWN{vgjz!Mw z-;HH(kLBNNePIsm$58{X%tPukREgarDo=(Br^`(u11h%M^7e$0mA-*148=YUIJGO4 z@*ovfL!iNPDI`mDC0{t=1jAO*b&waygepQh#7M>t8y3QU?jX)8BLBSE$LmHY44n4( zv`UCmoi!f1BOTnD2xFrg4lOO9hp&tv6%60?+9EPr`PI>ami$TZLN*_D-Yg7m@POq? zo_bpltG2#gJS_Sf>d^}n$HbSoU3HW3uJ-!kwku9HR}}h-pI1%cngnP!=wIGPaieO$ zk$%`_&z#RAG+nJn?{(G-s2uoG*t?>b#dc6Fmu!svvM-%(RVSv_{p96JCw>hH1TuNV zqE!PX=8+)wLw3tj1xg%c$A1pyQHxv74`LWgAv@BtIdGV9$iYVIzk z{%>_(zuZ`&aY6<6<28H9ot&biY>{IsVU01w6LV^2q}|IRVsRBAD?|jV67u#o@OJkm zo&Di@MraceJ@e=RY&G@DlJzP(JuJNkm2?|T3m^AebF1~L>Z`goRZ=RU!iscE2KMBT z-UnEyF6f#wPGHQIZBdnk9OX$9AFWl5Y24NlfrL~4%~y2`LpL!u0`gRw0n+_4Pcf&p z9ujrZhWe%sR<6P+E+=fVnuS*uqR-oRJT9unW?Eh#%=vqhyk7pesLaq`-4!QA9a{+p zO|uw{l(9;N*OuN8-&2Bv7XHRFkM_OCv$)1yUJ-A5B)GR^>$2ZJuWApa!%xCvS2wY= zdzYzdAt=(hH?@hw9MCGn_=bgv1vq`neETXhkpK`Kl`oTls}X=V4-xT9!L4^CIpxP^ zStrPqt%@kCqoCpDI|&cjru~nl70b`!B#im?PHmTql$Xc3f>dzxT7M_vem+pkS|c}t^>$lynx;V4Svu@>r=`*+v^5-gUZ$8v)P@mmaU zWz|Ys3uQ@cA+@5lRh6$!a4+8y^3<&FwiCpv95BD2rTcWddpG4n%CF=G`Z}p@dO&vI z5xrH2Q+q(|jc^{x5m{cnSnkdR#sX%@3?4uHg^^pW!m}$~;x8}O$-2x8InM~&#-%Co zCe~3TL-l>MBIIN`6WvRz+A%ITav<12_Y!6&_PHS$NH#f1(W#}=U_X#fJtk5`b}7Y7 zgZf#m!M*c|ePpqq%{fxV&5WSIN{=NiiU83EnqJaHi6Xh(qmX5)@8Bj#0xfiu%Q<%T zg=*>{htCaO_q=S+2%nN=XVJAA6r`7j&?`71X%?HZ?Bzs9rj3*X}2wV)^ru9^3WJ68D%cpT#%3)jn!s$ zP~wwqf)v-y`g$}WBMK0x)*;O2)_xmViqVCw`;`YR9ygT?trLS4U|0kCZjm^tsrvG^ zMuSJyeDrnM8nXcuS?vrIV9Ij)+h#UfOr(uKTil2l(r7k@hBG72jYEu!gP(Eeo9a`H zB|X`84lv;hFFXFPH%OWnUSV)sS7|Emk%hlSrWZn>60%Vpm~|r$1(vMFu54hF<)TA( zV)jZy1K&zUxC=&L!*|xlC0UwM%cGH|;hKvFr@JGk5Kg_bPUqq8+N?%Pej`{m=uF3M z+8!Ssqlo(jw$J%R6SnjNrnH`rh~N^_VGnME6l^cjfq{sK?3EB`o8lLj5^;4vbuPOW zBUxsCTG(qGd3B5kZ{VWHtT$w2GxE4do$9#%x`z@vxm=g76tIB}quez3>Ot@`dH=@B z3U;Vy6`;Wr16^fx-b_%gIJ5_CAyPQZl141 z{CH?hR!bc4j;h8_>MPrAahM3oygo8&$Koooq?>A%H!6|8zq~Q>=LeR)$>78284eQN zl*)5*#nhBbt{T7k&mx6NyCnD;rFMH%HQtv6=-+Hr?L5NtkLnrI-QN$f&5g-xlv{+e=SMvRSECLg{3|!Ho602c0?xQby648lEDV8 z4D@kf&ajLzebmNu6p>8`4c)>67~LCJ=G5cM1$-XrX^NCD1c-D|GVp3-1Hs6(M6lx= zT5ix}(-~Vb4-`&4)C&^p6(*@V>zf*HCl%d1#4q!!a!VW5#^n(9_tu&5jbB_Dxe1#% z_#iUbe%qk@g2Kd&{o*9$*1P2!?upY9E8()#Cgyl4`FeJCi$UU~$;sJE0q4PahATme zoQ!qc1Ono?4UW==#C$w!r?|^3U`)>*nt?ndxguBXJsd)aMu4Afnut!WzBzdPShXUt z3PGe2@W;KArrlIPIb)x`(r6jD0s0QRH&cEfEJfxx6}(YGT3HT;4Mxm1{B@^@G*^*+ zIyaxa)oM>qB>G`AE{aQ&od$8BZ$>3n#C3ySTt4i*w801F@@1lOjErkk@1mn1fdb+w zRd1CXTE5!{aD|s(;201Xpbfxa0FV1tORw`Dwu1hG1Oot2L9c%`p#S4LaY;!AX*mgb zCWV*(dh_e$y#y{!1fWY9Apw9#=wA)!zXy8z^KxZ*RV7hzWkx4Ar(X$)&!$-+K>rJZ z00{rO9P|qG=K=p4!Pvyk#Kzdf#>idY%EHFd@mEw!h@9HrQJ7CqZhuGF*g7$ISlBT* z8km~=IvOG3y{QP$rB$H*6jAwigoBB_vxS4ne~jYMqHXFILx`Zu;Xxe>)c_ zKTYCfYo~8z;$mX;t0on}a%zsG)f1ec3#~x?DWd|!urso8vbD9cv~Xh5*SD~-aMIUj zv~&NvDvu0Nfu0whpz9wU`BW7wkhA{AFtW8VwJ>x14TA2aOh^a~0FXHc0APbKe>I?g z&r^hrt%J3JlZA`PZy=gXo7V~;gK%>I0C;~9K(9c5U&bfn_zBUMQ}_)BS)VOoj1&N9 z7y$rCo^YV~{tL&>z`?-E3S{E{v`${R61_eG0MP#YSJg1}p3s~eKyl~j>|pu}=m{hw;yFmKDEtNKhrYBLM(dpTI}*{u%!0`akny zFw4`BC;@;;6HrJ!;XeoMZ1_)u*Z&z3f0pv2uHkwEw4HwoC}N+QC}j4(O8IxF{CD_c zW|pg?1wE$IV^%&bc@0}n;Kl|{1{St|N^(PmHc_imClNuFdDap&1&un>ad{nEgh>aGefnIAj3e0Ho$qk72&}4av;z7Y2H^oLcLL zBt~VBQjo>JdQ4gBHw@6>8^1svJr*C*sXGVq7z^>Qka_dpKpadBet|p&xD*rNwGqh5 zt8kzN(Gx50IQ$0k_Z0favNtiz$@m<^E&#Eg7Nm6de`G8DLrwi;qw-w?R~1w~T*~@-qkiEJ6R<5ri8k5t=|=cp5=> zwf`UilpB9$R|wjHG1CJ8-$4`S(+KJu`Um!7&ionv0y|>KA2bgo;rt30xcmqBV=n!7 z_+xScsUn>oCyXdi2hIFH8_*x&8Tj9;*UvSI{=e$j&*=Hjil`Q4tD^>5(rP$$lLu5$qoa`Y))}?eetOCb#Q10C?<**Zjm<|f5c7xx005PdpAG0QL-@~3 zn_nyYqu+C4<)jios=tT+b(rH6|BU?48vV$$!g0am1l6H^&>Roa{$~UF7gPT;^Iscd zKaHlx3a53P{^@a{t_vay{A4~x6B+FvsehjXen#tm&xS`?Ir2~?0ifG2S&*>>pU8Sc z_kS(x-%%v72>)S15AFX=)Ss*JV;r7B_Gj^dvXcWOPw0tSwy*y;S-;zE{_pp!k9oT`%k>&G zxdYHa{ppPktkDx`kFY=8G1)2FR2qTI0|#2^JzZ3*{{{r|uKw?m{&7gVsokFoIYAFt zh5-Q3wD+?C{ZB3bb4b6VJeu*>v+@4I0R2bv|C93nP%nNxneN{L-WU8?z`vFMbJYEM iw7|b5D8K)+gx`A}3bf9BOiKYkKIoaE`8_De0RJDFJU6=l diff --git a/dist/pcntoolkit-0.26-py3.9.egg b/dist/pcntoolkit-0.26-py3.9.egg deleted file mode 100644 index 6983ff9c71dbc30e5f707729f9a08f3a9f5462ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 201529 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 z zSN(nDvC~bQl#+?Rlim$3Vp~e?Ojd9gux7Q4wLQW^P=j*K;r=yK(4whRH~3rl3%hR8 ztsiq&80Uy9@r*g59d1smqqMK79oWFptT(bzmnKRN;0xT09j0cQgIFMospzO{|^~3?fOI6Q4dU*Z^ zuMaV0b*#~rUButm_Z!pE|Lsfl@~)>o>qvlWh+^x|2li1oVGoSS&I0Mgloe0%0DArk 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