From 4515d1137281127c9fe5dbdf70fc3c7889a9af17 Mon Sep 17 00:00:00 2001 From: Saige Rutherford Date: Tue, 27 Jul 2021 11:33:00 +0200 Subject: [PATCH 01/15] add brainchart rst files --- 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 | 406 ++++++ build/lib/pcntoolkit/model/NP.py | 82 ++ build/lib/pcntoolkit/model/NPR.py | 80 ++ build/lib/pcntoolkit/model/__init__.py | 6 + build/lib/pcntoolkit/model/architecture.py | 201 +++ build/lib/pcntoolkit/model/bayesreg.py | 442 +++++++ build/lib/pcntoolkit/model/gp.py | 488 +++++++ build/lib/pcntoolkit/model/hbr.py | 877 +++++++++++++ build/lib/pcntoolkit/model/rfa.py | 243 ++++ build/lib/pcntoolkit/normative.py | 1051 +++++++++++++++ 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 | 200 +++ .../pcntoolkit/normative_model/norm_gpr.py | 72 + .../pcntoolkit/normative_model/norm_hbr.py | 159 +++ .../lib/pcntoolkit/normative_model/norm_np.py | 229 ++++ .../pcntoolkit/normative_model/norm_rfa.py | 72 + .../pcntoolkit/normative_model/norm_utils.py | 21 + build/lib/pcntoolkit/normative_parallel.py | 1152 ++++++++++++++++ build/lib/pcntoolkit/trendsurf.py | 253 ++++ build/lib/pcntoolkit/util/__init__.py | 1 + build/lib/pcntoolkit/util/utils.py | 1154 +++++++++++++++++ dist/pcntoolkit-0.20-py3.8.egg | Bin 0 -> 162549 bytes doc/source/index.rst | 2 + .../pages/tutorial_braincharts_apply_nm.rst | 567 ++++++++ .../pages/tutorial_braincharts_fit_nm.rst | 440 +++++++ pcntoolkit.egg-info/PKG-INFO | 10 + pcntoolkit.egg-info/SOURCES.txt | 35 + 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, 8610 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/__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/utils.py create mode 100644 dist/pcntoolkit-0.20-py3.8.egg create mode 100644 doc/source/pages/tutorial_braincharts_apply_nm.rst create mode 100644 doc/source/pages/tutorial_braincharts_fit_nm.rst 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 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..f4b85aff --- /dev/null +++ b/build/lib/pcntoolkit/dataio/fileio.py @@ -0,0 +1,406 @@ +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): + """ Write output to nifti """ + + # 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,)) + array_img = nib.Nifti1Image(array_data, ex_img.affine, ex_img.header) + 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): + + 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) + 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) + 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/__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..0e5a25c6 --- /dev/null +++ b/build/lib/pcntoolkit/model/bayesreg.py @@ -0,0 +1,442 @@ +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). + """ + + 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 """ + + 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 """ + optimizer = kwargs.get('optimizer','cg') + + # covariates for heteroskedastic noise + Xv = kwargs.get('var_covariates', None) + + # options for l-bfgs-b + l = kwargs.get('l', 0.1) + epsilon = 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': + out = optimize.fmin_l_bfgs_b(self.penalized_loglik, x0=hyp0, + 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 """ + + 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')): + # warp the likelihood? + #if self.warp is not None: + # if self.verbose: + # print('warping input...') + # y = self.warp.f(y, gamma) + # + #self.post(hyp, X, y) + 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 diff --git a/build/lib/pcntoolkit/model/gp.py b/build/lib/pcntoolkit/model/gp.py new file mode 100644 index 00000000..e7af393b --- /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.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..908155bf --- /dev/null +++ b/build/lib/pcntoolkit/model/hbr.py @@ -0,0 +1,877 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Thu Jul 25 13:23:15 2019 + +@author: seykia +""" + +from __future__ import print_function +from __future__ import division + +import numpy as np +import pymc3 as pm +import theano +from itertools import product +from functools import reduce +from scipy import stats +import bspline +from bspline import splinelab + + + +def bspline_fit(X, order, nknots): + + feature_num = X.shape[1] + bsp_basis = [] + for i in range(feature_num): + knots = np.linspace(X[:,i].min(), X[:,i].max(), 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) + + +def hbr(X, y, batch_effects, batch_effects_size, configs, trace=None): + + feature_num = X.shape[1] + y_shape = y.shape + 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)) + + X = theano.shared(X) + y = theano.shared(y) + + with pm.Model() as model: + # Priors + if trace is not None: # Used for transferring the priors + mu_prior_intercept = from_posterior('mu_prior_intercept', + trace['mu_prior_intercept'], + distribution='normal', freedom=configs['freedom']) + log_sigma_prior_intercept = from_posterior('log_sigma_prior_intercept', + trace['log_sigma_prior_intercept'], freedom=configs['freedom'], + distribution='normal') + mu_prior_slope = from_posterior('mu_prior_slope', + trace['mu_prior_slope'], + distribution='normal', freedom=configs['freedom']) + log_sigma_prior_slope = from_posterior('log_sigma_prior_slope', + trace['log_sigma_prior_slope'], + distribution='normal', freedom=configs['freedom']) + else: + mu_prior_intercept = pm.Normal('mu_prior_intercept', mu=0., sigma=1e3) + log_sigma_prior_intercept = pm.Normal('log_sigma_prior_intercept', mu=0., sigma=2.5) + mu_prior_slope = pm.Normal('mu_prior_slope', mu=0., sigma=1e3, shape=(feature_num,)) + log_sigma_prior_slope = pm.Normal('log_sigma_prior_slope', mu=0., sigma=2.5, shape=(feature_num,)) + + if configs['random_intercept']: + intercepts_offset = pm.Normal('intercepts_offset', mu=0, sd=1, + shape=(batch_effects_size)) + else: + intercepts_offset = pm.Normal('intercepts_offset', mu=0, sd=1) + + intercepts = pm.Deterministic('intercepts', mu_prior_intercept + + intercepts_offset * pm.math.exp(log_sigma_prior_intercept)) + + if configs['random_slope']: # Random slopes + slopes_offset = pm.Normal('slopes_offset', mu=0, sd=1, + shape=(batch_effects_size + [feature_num])) + else: + slopes_offset = pm.Normal('slopes_offset', mu=0, sd=1) + + slopes = pm.Deterministic('slopes', mu_prior_slope + + slopes_offset * pm.math.exp(log_sigma_prior_slope)) + + y_hat = 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: + if (not configs['random_intercept'] and not configs['random_slope']): + y_hat = theano.tensor.set_subtensor(y_hat[idx,0], + intercepts + theano.tensor.dot(X[idx,:], + slopes)) + elif (configs['random_intercept'] and not configs['random_slope']): + y_hat = theano.tensor.set_subtensor(y_hat[idx,0], + intercepts[be] + theano.tensor.dot(X[idx,:], + slopes)) + elif (not configs['random_intercept'] and configs['random_slope']): + y_hat = theano.tensor.set_subtensor(y_hat[idx,0], + intercepts + theano.tensor.dot(X[idx,:], + slopes[be])) + elif (configs['random_intercept'] and configs['random_slope']): + y_hat = theano.tensor.set_subtensor(y_hat[idx,0], + intercepts[be] + theano.tensor.dot(X[idx,:], + slopes[be])) + + if configs['random_noise']: + if configs['hetero_noise']: + # Priors + if trace is not None: # Used for transferring the priors + mu_prior_intercept_noise = from_posterior('mu_prior_intercept_noise', + trace['mu_prior_intercept_noise'], + distribution='hnormal', freedom=configs['freedom']) + log_sigma_prior_intercept_noise = from_posterior('log_sigma_prior_intercept_noise', + trace['log_sigma_prior_intercept_noise'], + distribution='normal', freedom=configs['freedom']) + mu_prior_slope_noise = from_posterior('mu_prior_slope_noise', + trace['mu_prior_slope_noise'], + distribution='normal', freedom=configs['freedom']) + log_sigma_prior_slope_noise = from_posterior('log_sigma_prior_slope_noise', + trace['log_sigma_prior_slope_noise'], + distribution='normal', freedom=configs['freedom']) + else: + mu_prior_intercept_noise = pm.HalfNormal('mu_prior_intercept_noise', + sigma=1e3) + log_sigma_prior_intercept_noise = pm.Normal('log_sigma_prior_intercept_noise', mu=0., sigma=2.5) + mu_prior_slope_noise = pm.Normal('mu_prior_slope_noise', mu=0., + sigma=1e3, shape=(feature_num,)) + log_sigma_prior_slope_noise = pm.Normal('log_sigma_prior_slope_noise', + mu=0., sigma=2.5, shape=(feature_num,)) + if configs['random_intercept']: + intercepts_noise_offset = pm.Normal('intercepts_noise_offset', + sd=1, shape=(batch_effects_size)) + else: + intercepts_noise_offset = pm.Normal('intercepts_noise_offset', + sd=1) + + intercepts_noise = pm.Deterministic('intercepts_noise', + mu_prior_intercept_noise + + intercepts_noise_offset * + pm.math.exp(log_sigma_prior_intercept_noise)) + + if configs['random_slope']: + slopes_noise_offset = pm.Normal('slopes_noise_offset', mu=0, sd=1, + shape=(batch_effects_size + [feature_num])) + else: + slopes_noise_offset = pm.Normal('slopes_noise_offset', mu=0, sd=1) + + slopes_noise = pm.Deterministic('slopes_noise', mu_prior_slope_noise + + slopes_noise_offset * pm.math.exp(log_sigma_prior_slope_noise)) + + sigma_noise = 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: + if (not configs['random_intercept'] and not configs['random_slope']): + sigma_noise = theano.tensor.set_subtensor(sigma_noise[idx,0], + intercepts_noise + theano.tensor.dot(X[idx,:], + slopes_noise)) + elif (configs['random_intercept'] and not configs['random_slope']): + sigma_noise = theano.tensor.set_subtensor(sigma_noise[idx,0], + intercepts_noise[be] + theano.tensor.dot(X[idx,:], + slopes_noise)) + elif (not configs['random_intercept'] and configs['random_slope']): + sigma_noise = theano.tensor.set_subtensor(sigma_noise[idx,0], + intercepts_noise + theano.tensor.dot(X[idx,:], + slopes_noise[be])) + elif (configs['random_intercept'] and configs['random_slope']): + sigma_noise = theano.tensor.set_subtensor(sigma_noise[idx,0], + intercepts_noise[be] + theano.tensor.dot(X[idx,:], + slopes_noise[be])) + + sigma_y = pm.math.log1pexp(sigma_noise) + 1e-5 + + else: + if trace is not None: # Used for transferring the priors + log_sigma_noise = from_posterior('log_sigma_noise', + trace['log_sigma_noise'], + distribution='normal', freedom=configs['freedom']) + + else: + #log_sigma_noise = pm.Uniform('log_sigma_noise', lower=-5, upper=5, shape=(batch_effects_size)) + log_sigma_noise = pm.Normal('log_sigma_noise', mu=0., sigma=2.5, 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], pm.math.exp(log_sigma_noise[be])) + + else: + if trace is not None: + log_sigma_noise = from_posterior('log_sigma_noise', + trace['log_sigma_noise'], + distribution='normal', freedom=configs['freedom']) + else: + #log_sigma_noise = pm.Uniform('log_sigma_noise', lower=-5, upper=5) + log_sigma_noise = pm.Normal('log_sigma_noise', mu=0., sigma=2.5) + + 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], pm.math.exp(log_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 + + y_like = pm.SkewNormal('y_like', mu=y_hat, sigma=sigma_y, alpha=alpha, observed=y) + + return model + + +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)) + + X = theano.shared(X) + y = theano.shared(y) + + # 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: + 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 + + +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 __init__(self, configs): + + self.model_type = configs['type'] + self.configs = configs + + + def estimate(self, X, y, batch_effects): + """ Function to estimate the model """ + + if len(X.shape)==1: + X = np.expand_dims(X, axis=1) + if len(y.shape)==1: + y = np.expand_dims(y, 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]))) + + if self.model_type == 'linear': + with hbr(X, y, batch_effects, self.batch_effects_size, + self.configs): + 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']) + elif self.model_type == 'polynomial': + X = create_poly_basis(X, self.configs['order']) + with hbr(X, y, batch_effects, self.batch_effects_size, + self.configs): + 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']) + elif self.model_type == 'bspline': + self.bsp = bspline_fit(X, self.configs['order'], self.configs['nknots']) + X = bspline_transform(X, self.bsp) + with hbr(X, y, batch_effects, self.batch_effects_size, + self.configs): + 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']) + elif self.model_type == 'nn': + with nn_hbr(X, y, batch_effects, self.batch_effects_size, + self.configs): + 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(self, X, batch_effects, pred = 'single'): + """ Function to make predictions from the model """ + + 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) + + samples = self.configs['n_samples'] + if pred == 'single': + y = np.zeros([X.shape[0],1]) + if self.model_type == 'linear': + with hbr(X, y, batch_effects, self.batch_effects_size, self.configs): + ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) + elif self.model_type == 'polynomial': + X = create_poly_basis(X, self.configs['order']) + with hbr(X, y, batch_effects, self.batch_effects_size, self.configs): + ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) + elif self.model_type == 'bspline': + X = bspline_transform(X, self.bsp) + with hbr(X, y, batch_effects, self.batch_effects_size, self.configs): + ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) + elif self.model_type == 'nn': + with nn_hbr(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 """ + + if len(X.shape)==1: + X = np.expand_dims(X, axis=1) + if len(y.shape)==1: + y = np.expand_dims(y, 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]))) + + if self.model_type == 'linear': + with hbr(X, y, batch_effects, self.batch_effects_size, + self.configs, trace = self.trace): + 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']) + elif self.model_type == 'polynomial': + X = create_poly_basis(X, self.configs['order']) + with hbr(X, y, batch_effects, self.batch_effects_size, + self.configs, trace = self.trace): + 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']) + if self.model_type == 'bspline': + X = bspline_transform(X, self.bsp) + with hbr(X, y, batch_effects, self.batch_effects_size, + self.configs, trace = self.trace): + 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']) + elif self.model_type == 'nn': + with nn_hbr(X, y, batch_effects, self.batch_effects_size, + self.configs, trace = self.trace): + 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 """ + + 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) + + samples = self.configs['n_samples'] + 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 = self.trace): + ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) + elif self.model_type == 'polynomial': + X = create_poly_basis(X, self.configs['order']) + with hbr(X, y, batch_effects, self.batch_effects_size, self.configs, trace = self.trace): + ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) + elif self.model_type == 'bspline': + X = bspline_transform(X, self.bsp) + with hbr(X, y, batch_effects, self.batch_effects_size, self.configs, trace = self.trace): + ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) + elif self.model_type == 'nn': + with nn_hbr(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 """ + + 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) + + y = np.zeros([X.shape[0],1]) + if self.model_type == 'linear': + with hbr(X, y, batch_effects, self.batch_effects_size, self.configs): + ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) + elif self.model_type == 'polynomial': + X = create_poly_basis(X, self.configs['order']) + with hbr(X, y, batch_effects, self.batch_effects_size, self.configs): + ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) + elif self.model_type == 'bspline': + X = bspline_transform(X, self.bsp) + with hbr(X, y, batch_effects, self.batch_effects_size, self.configs): + ppc = pm.sample_posterior_predictive(self.trace, samples=samples, progressbar=True) + elif self.model_type == 'nn': + with nn_hbr(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) + elif self.model_type == 'polynomial': + X = create_poly_basis(X, self.configs['order']) + with hbr(X, y, batch_effects, self.batch_effects_size, self.configs, + trace): + ppc = pm.sample_prior_predictive(samples=samples) + elif self.model_type == 'bspline': + self.bsp = bspline_fit(X, self.configs['order'], self.configs['nknots']) + X = bspline_transform(X, self.bsp) + with hbr(X, y, batch_effects, self.batch_effects_size, self.configs, + trace): + ppc = pm.sample_prior_predictive(samples=samples) + elif self.model_type == 'nn': + with nn_hbr(X, y, batch_effects, self.batch_effects_size, self.configs, + trace): + ppc = pm.sample_prior_predictive(samples=samples) + + return ppc + \ No newline at end of file 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..409cd12d --- /dev/null +++ b/build/lib/pcntoolkit/normative.py @@ -0,0 +1,1051 @@ +#!/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 +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 +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 + 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') + 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') + + # 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) + mean_resp_warp = [np.zeros(Y.shape[1]) for s in range(splits.n_splits)] + std_resp_warp = [np.zeros(Y.shape[1]) for s in range(splits.n_splits)] + + 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): + # warp the labels? + # TODO: Warping for scaled data + if warp is not None: + 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) + mean_resp_warp[fold][i] = np.mean(yw) + std_resp_warp[fold][i] = np.std(yw) + 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...') + 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}, 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 = ['Rho', 'RMSE', 'SMSE', 'EXPV', + 'MSLL', 'NLL', 'BIC']) + else: + results = evaluate(Ywarp[testids, :], Yhat[testids, :], + S2=S2[testids, :], mY=mean_resp_warp[0], + sY=std_resp_warp[0], nlZ=nlZ, nm=nm, Xz_tr=Xz_tr, + alg=alg, metrics = ['Rho', 'RMSE', 'SMSE', + 'EXPV', 'MSLL', + 'NLL', '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') + 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...') + 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}, 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 + + 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') + inputsuffix = kwargs.pop('inputsuffix', '_estimate') + alg = kwargs.pop('alg') + + 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] + feature_num = len(glob.glob(os.path.join(model_path, 'NM_*' + inputsuffix + + '.pkl'))) + + 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[0].transform(X) + else: + Xz = X + + # estimate the models for all subjects + for i in range(feature_num): + print("Prediction by model ", i+1, "of", feature_num) + nm = norm_init(Xz) + nm = nm.load(os.path.join(model_path, 'NM_' + str(0) + '_' + + str(i) + 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[0].inverse_transform(yhat, index=i) + S2[:, i] = s2.squeeze() * sY[0][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() + + if respfile is None: + save_results(None, Yhat, S2, None, outputsuffix=outputsuffix) + + return (Yhat, S2) + + else: + Y, maskvol = load_response_vars(respfile, maskfile) + if len(Y.shape) == 1: + Y = Y[:, np.newaxis] + + # warp the targets? + if 'blr' in dir(nm): + if nm.blr.warp is not None: + warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] + Y = nm.blr.warp.f(Y, warp_param) + + Z = (Y - Yhat) / np.sqrt(S2) + + print("Evaluating the model ...") + if meta_data: + results = evaluate(Y, Yhat, S2=S2, mY=mY[0], sY=sY[0]) + 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: 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 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') + if alg != 'hbr': + print('Model transferring 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') + batch_effects_train = fileio.load(trbefile) + + outputsuffix = kwargs.pop('outputsuffix', '_transfer') + inputsuffix = kwargs.pop('inputsuffix', '_estimate') + tsbefile = kwargs.pop('tsbefile', None) + + 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) + 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 + + 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) + 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) + + 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]) + + 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): + + 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')) + 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')) + + if testcov is not None: + yhat, s2 = nm.predict_on_new_sites(Xte, batch_effects_test) + if outscaler == 'standardize': + Yhat[:, i] = scaler_resp[0].inverse_transform(yhat, index=i) + S2[:, i] = s2.squeeze() * sY[0][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() + + if testresp is None: + save_results(respfile, Yhat, S2, maskvol, outputsuffix=outputsuffix) + return (Yhat, S2) + else: + Z = (Yte - Yhat) / np.sqrt(S2) + + print("Evaluating the model ...") + results = evaluate(Yte, Yhat, S2=S2, mY=mY, sY=sY) + + save_results(respfile, Yhat, S2, maskvol, Z=Z, results=results, + outputsuffix=outputsuffix) + + return (Yhat, S2, Z) + + +def extend(covfile, respfile, maskfile=None, **kwargs): + + 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())) or \ + (not 'dummycovfile' in list(kwargs.keys()))or \ + (not 'dummybefile' 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') + dummycovfile = kwargs.pop('dummycovfile') + dummybefile = kwargs.pop('dummybefile') + + outputsuffix = kwargs.pop('outputsuffix', '_extend') + inputsuffix = kwargs.pop('inputsuffix', '_estimate') + 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) + X_dummy = fileio.load(dummycovfile) + batch_effects_dummy = fileio.load(dummybefile) + + if len(Y.shape) == 1: + Y = Y[:, np.newaxis] + if len(X.shape) == 1: + X = X[:, np.newaxis] + if len(X_dummy.shape) == 1: + X_dummy = X_dummy[:, 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, X_dummy, + batch_effects_dummy, 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 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..49b3f03a --- /dev/null +++ b/build/lib/pcntoolkit/normative_model/norm_blr.py @@ -0,0 +1,200 @@ +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.bayesreg import BLR + from pcntoolkit.normative_model.normbase import NormBase + from pcntoolkit.utils import create_poly_basis +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 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): + self.Phi = create_poly_basis(X, self._model_order) + self.blr = BLR(theta=theta, X=self.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) + + # same for the optimizer + #kwargs.pop('optimizer', None) + + if not hasattr(self,'Phi'): + self.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=self.Phi, y=y, + var_groups=self.var_groups, + warp=self.warp, **kwargs) + + self.theta = self.blr.estimate(theta, self.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 '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 + + 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 + + yhat, s2 = self.blr.predict(theta, self.Phi, y, Phis, + var_groups_test=var_groups_te, + var_covariates_test=var_cov_te, **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..280adb25 --- /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.gp import GPR, CovSum + from pcntoolkit.gp.normative_model.normbase 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..ad60a508 --- /dev/null +++ b/build/lib/pcntoolkit/normative_model/norm_hbr.py @@ -0,0 +1,159 @@ +#!/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 os +import sys +import numpy as np + +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['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/build/lib/pcntoolkit/normative_model/norm_np.py b/build/lib/pcntoolkit/normative_model/norm_np.py new file mode 100644 index 00000000..29834d31 --- /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.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..275ba4f1 --- /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.normbase import NormBase + from pcntoolkit.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..30cc3a16 --- /dev/null +++ b/build/lib/pcntoolkit/normative_model/norm_utils.py @@ -0,0 +1,21 @@ +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..903ed6b6 --- /dev/null +++ b/build/lib/pcntoolkit/normative_parallel.py @@ -0,0 +1,1152 @@ +#!/.../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, +# AF Marquand +# ----------------------------------------------------------------------------- + +from __future__ import print_function +from __future__ import division + +import os +import sys +import glob +import shutil +import pickle +import fileinput +import numpy as np +import pandas as pd +from subprocess import call + +try: + import pcntoolkit as ptk + import pcntoolkit.dataio.fileio as fileio + from pcntoolkit import configs + 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 + + +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', + **kwargs): + + """ + This function is a mother function that executes all parallel normative + modelling routines. Different specifications are possible using the sub- + functions. + + :Parameters: + * processing_dir -> Full path to the processing dir + * python_path -> Full path to the python distribution + * normative_path -> Full path to the normative.py. If None (default) + then it will automatically retrieves the path from + the installed packeage. + * job_name -> Name for the bash script that is the output of + this function + * covfile_path -> Full path to a .txt file that contains all + covariats (subjects x covariates) for the + responsefile + * respfile_path -> Full path to a .txt that contains all features + (subjects x features) + * batch_size -> Number of features in each batch + * memory -> Memory requirements written as string + for example 4gb or 500mb + * duation -> The approximate duration of the job, a string + with HH:MM:SS for example 01:01:01 + * cv_folds -> Number of cross validations + * testcovfile_path -> Full path to a .txt file that contains all + covariats (subjects x covariates) for the + testresponse file + * testrespfile_path -> Full path to a .txt file that contains all + test features + * log_path -> Pathfor saving log files + * binary -> If True uses binary format for response file + otherwise it is text + + written by (primarily) T Wolfers, (adapted) SM Kia + """ + + 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) + cluster_spec = kwargs.pop('cluster_spec', 'torque') + log_path = kwargs.pop('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)}) + for n in range(1, number_of_batches+1): + print(n) + kwargs.update({'job_id':str(n)}) + if testrespfile_path is not None: + if cv_folds is not None: + raise(ValueError, """If the response file is specified + 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) + qsub_nm(job_path=batch_job_path, + log_path=log_path, + memory=memory, + duration=duration) + 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) + qsub_nm(job_path=batch_job_path, + log_path=log_path, + memory=memory, + duration=duration) + 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) + qsub_nm(job_path=batch_job_path, + log_path=log_path, + memory=memory, + duration=duration) + 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) + # ] + + +"""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. + + :Parameters: + * processing_dir -> Full path to the folder of processing + * respfile_path -> Full path to the responsefile.txt + (subjects x features) + * batch_size -> Number of features in each batch + * testrespfile_path -> Full path to the test responsefile.txt + (subjects x features) + * binary -> If True binary file + + :outputs: + * The creation of a folder struture for batch-wise processing + + witten by (primarily) T Wolfers (adapted) SM Kia + """ + + 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'): + + """This function checks and collects all batches. + + :Parameters: + * processing_dir -> Full path to the processing directory + * collect -> If True data is checked for failed batches + and collected; if False data is just checked + * binary -> Results in pkl format? + + :ouptuts: + * Text files containing all results accross all batches the combined + output (written to disk) + * returns 0 if batches fail, 1 otherwise + + written by (primarily) T Wolfers, (adapted) SM Kia + """ + + 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': + 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': + 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.stack(mY)) + meta_data['std_resp'] = np.squeeze(np.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 1 + else: + return 0 + +def delete_nm(processing_dir, + binary=False): + """This function deletes all processing for normative modelling and just + keeps the combined output. + + :Parameters: + * processing_dir -> Full path to the processing directory + * binary -> Results in pkl format? + + written by (primarily) T Wolfers, (adapted) SM Kia + """ + + 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. + + :Parameters: + * processing_dir -> Full path to the processing dir + * python_path -> Full path to the python distribution + * normative_path -> Full path to the normative.py + * job_name -> Name for the bash script that is the output of + this function + * covfile_path -> Full path to a .txt file that contains all + covariats (subjects x covariates) for the + responsefile + * respfile_path -> Full path to a .txt that contains all features + (subjects x features) + * cv_folds -> Number of cross validations + * testcovfile_path -> Full path to a .txt file that contains all + covariats (subjects x covariates) for the + testresponse file + * testrespfile_path -> Full path to a .txt file that contains all + test features + * alg -> which algorithm to use + * 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 + """ + + # 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) + standardize = kwargs.pop('standardize', True) + + # 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. + + ** Input: + * job_path -> Full path to the job.sh file + * memory -> Memory requirements written as string for example + 4gb or 500mb + * duation -> The approximate duration of the job, a string with + HH:MM:SS for example 01:01:01 + + ** Output: + * Submission of the job to the (torque) cluster + + witten by (primarily) T Wolfers, (adapted) SM Kia + """ + + # 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) + + +def rerun_nm(processing_dir, + log_path, + memory, + duration, + binary=False): + """ + This function reruns all failed batched in processing_dir after collect_nm + has identified he failed batches + + * Input: + * processing_dir -> Full path to the processing directory + * memory -> Memory requirements written as string + for example 4gb or 500mb + * 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 + """ + + 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) + qsub_nm(job_path=jobpath, + log_path=log_path, + memory=memory, + duration=duration) + 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) + qsub_nm(job_path=jobpath, + log_path=log_path, + memory=memory, + duration=duration) + +# 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. + + :Parameters: + * processing_dir -> Full path to the processing dir + * python_path -> Full path to the python distribution + * normative_path -> Full path to the normative.py + * job_name -> Name for the bash script that is the output of + this function + * covfile_path -> Full path to a .txt file that contains all + covariats (subjects x covariates) for the + responsefile + * respfile_path -> Full path to a .txt that contains all features + (subjects x features) + * cv_folds -> Number of cross validations + * testcovfile_path -> Full path to a .txt file that contains all + covariats (subjects x covariates) for the + testresponse file + * testrespfile_path -> Full path to a .txt file that contains all + test features + * alg -> which algorithm to use + * 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 + """ + + # 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) + standardize = kwargs.pop('standardize', True) + + # 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. + + ** Input: + * job_path -> Full path to the job.sh file + * log_path -> The logs are currently stored in the working dir + + ** Output: + * Submission of the job to the (torque) cluster + + witten by (primarily) T Wolfers + """ + + # created qsub command + sbatch_call = ['sbatch ' + job_path] + + # submits job to cluster + call(sbatch_call, shell=True) + + def rerun_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 + + * Input: + * processing_dir -> Full path to the processing directory + * memory -> Memory requirements written as string + for example 4gb or 500mb + * duration -> The approximate duration of the job, a + string with HH:MM:SS for example 01:01:01 + * new_memory -> If you want to change the memory + you have to indicate it here. + * new_duration -> If you want to change the duration + you have to indicate it here. + * Outputs: + * Reruns failed batches. + + written by (primarily) T Wolfers + """ + 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) 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/utils.py b/build/lib/pcntoolkit/util/utils.py new file mode 100644 index 00000000..4b9ff03e --- /dev/null +++ b/build/lib/pcntoolkit/util/utils.py @@ -0,0 +1,1154 @@ +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 + +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_cols=None, + **kwargs): + """ Prepare a design matrix from a set of covariates sutiable for + running Bayesian linar regression + + :param p: order of spline (3 = cubic) + :param nknots: number of knots (endpoints only counted once) + """ + 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 site_cols is not None: + if type(site_cols) is pd.DataFrame: + site_cols = site_cols.to_numpy() + if site_cols.shape[0] != N: + raise ValueError('site cols must have the same number of rows as X') + 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 + +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 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], 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, where + + y = sinh(b * arcsinh(x) + epsilon * b) + + and a = -epsilon*b + + see Jones and Pewsey A (2009) Biometrika, 96 (4) (2009) + """ + + 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): + + if warpnames is None: + raise ValueError("A list of warp functions is required") + 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 + + 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 ci == 0: + fw = warp.f(x, theta_c) + else: + fw = warp.f(fw, theta_c) + return fw + + def invf(self, x, theta): + theta_offset = 0 + 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 ci == 0: + finvw = warp.invf(x, theta_c) + else: + finvw = warp.invf(finvw, theta_c) + + return finvw + + def df(self, x, theta): + theta_offset = 0 + 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 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 + +# ----------------------- +# Functions for inference +# ----------------------- + +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 + + witten 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. + + ** Input: + * job_path -> Full path to the job.sh file + * memory -> Memory requirements written as string for example + 4gb or 500mb + * duration -> The approximate duration of the job, a string with + HH:MM:SS for example 01:01:01 + + ** Output: + * Submission of the job to the (torque) cluster + + witten by Thomas Wolfers + """ + 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: + missing_subjects.append(sub) + print('%d: QC is failed for subject %s.' %(s, sub)) + + 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 diff --git a/dist/pcntoolkit-0.20-py3.8.egg b/dist/pcntoolkit-0.20-py3.8.egg new file mode 100644 index 0000000000000000000000000000000000000000..5bee1e5b76fbac6870695a4799cf092909329600 GIT binary patch literal 162549 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�_(-W|OA*`Pkh)iB`GQGMg(_48#StpeKrR(f? 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 tb!GW~>im30_#d literal 0 HcmV?d00001 diff --git a/doc/source/index.rst b/doc/source/index.rst index 0e08e020..220653d6 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -36,6 +36,8 @@ Predictive Clinical Neuroscience toolkit pages/tutorial_CPC2020.rst pages/tutorial_ROIcorticalthickness.rst pages/tutorial_HBR.rst + pages/tutorial_braincharts_fit_nm.rst + pages/tutorial_braincharts_apply_nm.rst .. toctree:: diff --git a/doc/source/pages/tutorial_braincharts_apply_nm.rst b/doc/source/pages/tutorial_braincharts_apply_nm.rst new file mode 100644 index 00000000..4ba69a13 --- /dev/null +++ b/doc/source/pages/tutorial_braincharts_apply_nm.rst @@ -0,0 +1,567 @@ +Using lifespan models to make predictions on new data +----------------------------------------------------- + +This notebook shows how to apply the coefficients from pre-estimated +normative models to new data. This can be done in two different ways: +(i) using a new set of data derived from the same sites used to estimate +the model and (ii) on a completely different set of sites. In the latter +case, we also need to estimate the site effect, which requires some +calibration/adaptation data. As an illustrative example, we use a +dataset derived from the `1000 functional connectomes +project `__ +and adapt the learned model to make predictions on these data. + +First, if necessary, we install PCNtoolkit (note: this tutorial requires +at least version 0.20) + +.. code:: ipython3 + + !pip install pcntoolkit==0.20 + +Now we import the required libraries + +.. code:: ipython3 + + import os + import numpy as np + import pandas as pd + import pickle + from matplotlib import pyplot as plt + import seaborn as sns + + from pcntoolkit.normative import estimate, predict, evaluate + from pcntoolkit.util.utils import compute_MSLL, create_design_matrix + from nm_utils import remove_bad_subjects, load_2d + +Next, we configure some basic variables, like where we want the analysis +to be done and which model we want to use. + +**Note:** We maintain a list of site ids for each dataset, which +describe the site names in the training and test data (``site_ids_tr`` +and ``site_ids_te``), plus also the adaptation data . The training site +ids are provided as a text file in the distribution and the test ids are +extracted automatically from the pandas dataframe (see below). If you +use additional data from the sites (e.g. later waves from ABCD), it may +be necessary to adjust the site names to match the names in the training +set. See the accompanying paper for more details + +.. code:: ipython3 + + # which model do we wish to use? + model_name = 'lifespan_29K_82sites_train' + site_names = 'site_ids_82sites.txt' + + # where the analysis takes place + root_dir = '/braincharts' + out_dir = os.path.join(root_dir, 'models', model_name) + + # load a set of site ids from this model. This must match the training data + with open(os.path.join(root_dir,'docs', site_names)) as f: + site_ids_tr = f.read().splitlines() + +Download test dataset +~~~~~~~~~~~~~~~~~~~~~ + +As mentioned above, to demonstrate this tool we will use a test dataset +derived from the FCON 1000 dataset. We provide a prepackaged +training/test split of these data in the required format (also after +removing sites with only a few data points), +`here `__. +you can get these data by running the following commmands: + +.. code:: ipython3 + + os.chdir(root_dir) + !wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/main/data/fcon1000_tr.csv + !wget -nc https://raw.githubusercontent.com/predictive-clinical-neuroscience/PCNtoolkit-demo/main/data/fcon1000_te.csv + +Load test data +~~~~~~~~~~~~~~ + +Now we load the test data and remove some subjects that may have poor +scan quality. This asssesment is based on the Freesurfer Euler +characteristic as described in the papers below. + +**References** - `Kia et al +2021 `__ +- `Rosen et al +2018 `__ + +.. code:: ipython3 + + test_data = os.path.join(root_dir, 'fcon1000_te.csv') + + df_te = pd.read_csv(test_data, index_col=0) + + # remove some bad subjects + df_te, bad_sub = remove_bad_subjects(df_te, df_te) + + # extract a list of unique site ids from the test set + site_ids_te = sorted(set(df_te['site'].to_list())) + + +.. parsed-literal:: + + 16 subjects are removed! + + +(Optional) Load adaptation data +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If the data you wish to make predictions for is not derived from the +same scanning sites as those in the trainig set, it is necessary to +learn the site effect so that we can account for it in the predictions. +In order to do this in an unbiased way, we use a separate dataset, which +we refer to as ‘adaptation’ data. This must contain data for all the +same sites as in the test dataset and we assume these are coded in the +same way, based on a the ‘sitenum’ column in the dataframe. + +.. code:: ipython3 + + adaptation_data = os.path.join(root_dir, 'fcon1000_tr.csv') + + df_ad = pd.read_csv(adaptation_data, index_col=0) + + # remove some bad subjects + df_ad, bad_sub = remove_bad_subjects(df_ad, df_ad) + + # extract a list of unique site ids from the test set + site_ids_ad = sorted(set(df_ad['site'].to_list())) + + if not all(elem in site_ids_ad for elem in site_ids_te): + print('Warning: some of the testing sites are not in the adaptation data') + + +.. parsed-literal:: + + 11 subjects are removed! + + +Configure which models to fit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now, we configure which imaging derived phenotypes (IDPs) we would like +to process. This is just a list of column names in the dataframe we have +loaded above. + +We could load the whole set (i.e. all phenotypes for which we have +models for … + +.. code:: ipython3 + + # load the list of idps for left and right hemispheres, plus subcortical regions + with open(os.path.join(root_dir,'docs','phenotypes_lh.txt')) as f: + idp_ids_lh = f.read().splitlines() + with open(os.path.join(root_dir,'docs','phenotypes_rh.txt')) as f: + idp_ids_rh = f.read().splitlines() + with open(os.path.join(root_dir,'docs','phenotypes_sc.txt')) as f: + idp_ids_sc = f.read().splitlines() + + # we choose here to process all idps + idp_ids = idp_ids_lh + idp_ids_rh + idp_ids_sc + +… or alternatively, we could just specify a list + +.. code:: ipython3 + + idp_ids = [ 'Left-Thalamus-Proper', 'Left-Lateral-Ventricle', 'rh_MeanThickness_thickness'] + +Configure covariates +~~~~~~~~~~~~~~~~~~~~ + +Now, we configure some parameters to fit the model. First, we choose +which columns of the pandas dataframe contain the covariates (age and +sex). The site parameters are configured automatically later on by the +``configure_design_matrix()`` function, when we loop through the IDPs in +the list + +The supplied coefficients are derived from a ‘warped’ Bayesian linear +regression model, which uses a nonlinear warping function to model +non-Gaussianity (``sinarcsinh``) plus a non-linear basis expansion (a +cubic b-spline basis set with 5 knot points, which is the default value +in the PCNtoolkit package). Since we are sticking with the default +value, we do not need to specify any parameters for this, but we do need +to specify the limits. We choose to pad the input by a few years either +side of the input range. We will also set a couple of options that +control the estimation of the model + +For further details about the likelihood warping approach, see the +accompanying paper and `Fraza et al +2021 `__. + +.. code:: ipython3 + + # which data columns do we wish to use as covariates? + cols_cov = ['age','sex'] + + # limits for cubic B-spline basis + xmin = -5 + xmax = 110 + + # Absolute Z treshold above which a sample is considered to be an outlier (without fitting any model) + outlier_thresh = 7 + +Make predictions +~~~~~~~~~~~~~~~~ + +This will make predictions for each IDP separately. This is done by +extracting a column from the dataframe (i.e. specifying the IDP as the +response variable) and saving it as a numpy array. Then, we configure +the covariates, which is a numpy data array having the number of rows +equal to the number of datapoints in the test set. The columns are +specified as follows: + +- A global intercept (column of ones) +- The covariate columns (here age and sex, coded as 0=female/1=male) +- Dummy coded columns for the sites in the training set (one column per + site) +- Columns for the basis expansion (seven columns for the default + parameterisation) + +Once these are saved as numpy arrays in ascii format (as here) or +(alternatively) in pickle format, these are passed as inputs to the +``predict()`` method in the PCNtoolkit normative modelling framework. +These are written in the same format to the location specified by +``idp_dir``. At the end of this step, we have a set of predictions and +Z-statistics for the test dataset that we can take forward to further +analysis. + +Note that when we need to make predictions on new data, the procedure is +more involved, since we need to prepare, process and store covariates, +response variables and site ids for the adaptation data. + +.. code:: ipython3 + + for idp_num, idp in enumerate(idp_ids): + print('Running IDP', idp_num, idp, ':') + idp_dir = os.path.join(out_dir, idp) + os.chdir(idp_dir) + + # extract and save the response variables for the test set + y_te = df_te[idp].to_numpy() + + # save the variables + resp_file_te = os.path.join(idp_dir, 'resp_te.txt') + np.savetxt(resp_file_te, y_te) + + # configure and save the design matrix + cov_file_te = os.path.join(idp_dir, 'cov_bspline_te.txt') + X_te = create_design_matrix(df_te[cols_cov], + site_ids = df_te['site'], + all_sites = site_ids_tr, + basis = 'bspline', + xmin = xmin, + xmax = xmax) + np.savetxt(cov_file_te, X_te) + + # check whether all sites in the test set are represented in the training set + if all(elem in site_ids_tr for elem in site_ids_te): + print('All sites are present in the training data') + + # just make predictions + yhat_te, s2_te, Z = predict(cov_file_te, + alg='blr', + respfile=resp_file_te, + model_path=os.path.join(idp_dir,'Models')) + else: + print('Some sites missing from the training data. Adapting model') + + # save the covariates for the adaptation data + X_ad = create_design_matrix(df_ad[cols_cov], + site_ids = df_ad['site'], + all_sites = site_ids_tr, + basis = 'bspline', + xmin = xmin, + xmax = xmax) + cov_file_ad = os.path.join(idp_dir, 'cov_bspline_ad.txt') + np.savetxt(cov_file_ad, X_ad) + + # save the responses for the adaptation data + resp_file_ad = os.path.join(idp_dir, 'resp_ad.txt') + y_ad = df_ad[idp].to_numpy() + np.savetxt(resp_file_ad, y_ad) + + # save the site ids for the adaptation data + sitenum_file_ad = os.path.join(idp_dir, 'sitenum_ad.txt') + site_num_ad = df_ad['sitenum'].to_numpy(dtype=int) + np.savetxt(sitenum_file_ad, site_num_ad) + + # save the site ids for the test data + sitenum_file_te = os.path.join(idp_dir, 'sitenum_te.txt') + site_num_te = df_te['sitenum'].to_numpy(dtype=int) + np.savetxt(sitenum_file_te, site_num_te) + + yhat_te, s2_te, Z = predict(cov_file_te, + alg = 'blr', + respfile = resp_file_te, + model_path = os.path.join(idp_dir,'Models'), + adaptrespfile = resp_file_ad, + adaptcovfile = cov_file_ad, + adaptvargroupfile = sitenum_file_ad, + testvargroupfile = sitenum_file_te) + + +.. parsed-literal:: + + Running IDP 0 Left-Thalamus-Proper : + Some sites missing from the training data. Adapting model + Loading data ... + Prediction by model 1 of 1 + Evaluating the model ... + Evaluations Writing outputs ... + Writing outputs ... + Running IDP 1 Left-Lateral-Ventricle : + Some sites missing from the training data. Adapting model + Loading data ... + Prediction by model 1 of 1 + Evaluating the model ... + Evaluations Writing outputs ... + Writing outputs ... + Running IDP 2 rh_MeanThickness_thickness : + Some sites missing from the training data. Adapting model + Loading data ... + Prediction by model 1 of 1 + Evaluating the model ... + Evaluations Writing outputs ... + Writing outputs ... + + +Preparing dummy data for plotting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now, we plot the centiles of variation estimated by the normative model. + +We do this by making use of a set of dummy covariates that span the +whole range of the input space (for age) for a fixed value of the other +covariates (e.g. sex) so that we can make predictions for these dummy +data points, then plot them. We configure these dummy predictions using +the same procedure as we used for the real data. We can use the same +dummy data for all the IDPs we wish to plot + +.. code:: ipython3 + + # which sex do we want to plot? + sex = 1 # 1 = male 0 = female + if sex == 1: + clr = 'blue'; + else: + clr = 'red' + + # create dummy data for visualisation + print('configuring dummy data ...') + xx = np.arange(xmin, xmax, 0.5) + X0_dummy = np.zeros((len(xx), 2)) + X0_dummy[:,0] = xx + X0_dummy[:,1] = sex + + # create the design matrix + X_dummy = create_design_matrix(X0_dummy, xmin=xmin, xmax=xmax, site_ids=None, all_sites=site_ids_tr) + + # save the dummy covariates + cov_file_dummy = os.path.join(out_dir,'cov_bspline_dummy_mean.txt') + np.savetxt(cov_file_dummy, X_dummy) + + +.. parsed-literal:: + + configuring dummy data ... + + +Plotting the normative models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now we loop through the IDPs, plotting each one separately. The outputs +of this step are a set of quantitative regression metrics for each IDP +and a set of centile curves which we plot the test data against. + +This part of the code is relatively complex because we need to keep +track of many quantities for the plotting. We also need to remember +whether the data need to be warped or not. By default in PCNtoolkit, +predictions in the form of ``yhat, s2`` are always in the warped +(Gaussian) space. If we want predictions in the input (non-Gaussian) +space, then we need to warp them with the inverse of the estimated +warping function. This can be done using the function +``nm.blr.warp.warp_predictions()``. + +**Note:** it is necessary to update the intercept for each of the sites. +For purposes of visualisation, here we do this by adjusting the median +of the data to match the dummy predictions, but note that all the +quantitative metrics are estimated using the predictions that are +adjusted properly using a learned offset (or adjusted using a hold-out +adaptation set, as above). Note also that for the calibration data we +require at least two data points of the same sex in each site to be able +to estimate the variance. Of course, in a real example, you would want +many more than just two since we need to get a reliable estimate of the +variance for each site. + +.. code:: ipython3 + + sns.set(style='whitegrid') + + for idp_num, idp in enumerate(idp_ids): + print('Running IDP', idp_num, idp, ':') + idp_dir = os.path.join(out_dir, idp) + os.chdir(idp_dir) + + # load the true data points + yhat_te = load_2d(os.path.join(idp_dir, 'yhat_predict.txt')) + s2_te = load_2d(os.path.join(idp_dir, 'ys2_predict.txt')) + y_te = load_2d(os.path.join(idp_dir, 'resp_te.txt')) + + # set up the covariates for the dummy data + print('Making predictions with dummy covariates (for visualisation)') + yhat, s2 = predict(cov_file_dummy, + alg = 'blr', + respfile = None, + model_path = os.path.join(idp_dir,'Models'), + outputsuffix = '_dummy') + + # load the normative model + with open(os.path.join(idp_dir,'Models', 'NM_0_0_estimate.pkl'), 'rb') as handle: + nm = pickle.load(handle) + + # get the warp and warp parameters + W = nm.blr.warp + warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] + + # first, we warp predictions for the true data and compute evaluation metrics + med_te = W.warp_predictions(np.squeeze(yhat_te), np.squeeze(s2_te), warp_param)[0] + med_te = med_te[:, np.newaxis] + print('metrics:', evaluate(y_te, med_te)) + + # then, we warp dummy predictions to create the plots + med, pr_int = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2), warp_param) + + # extract the different variance components to visualise + beta, junk1, junk2 = nm.blr._parse_hyps(nm.blr.hyp, X_dummy) + s2n = 1/beta # variation (aleatoric uncertainty) + s2s = s2-s2n # modelling uncertainty (epistemic uncertainty) + + # plot the data points + y_te_rescaled_all = np.zeros_like(y_te) + for sid, site in enumerate(site_ids_te): + # plot the true test data points + if all(elem in site_ids_tr for elem in site_ids_te): + # all data in the test set are present in the training set + + # first, we select the data points belonging to this particular site + idx = np.where(np.bitwise_and(X_te[:,2] == sex, X_te[:,sid+len(cols_cov)+1] !=0))[0] + if len(idx) == 0: + print('No data for site', sid, site, 'skipping...') + continue + + # then directly adjust the data + idx_dummy = np.bitwise_and(X_dummy[:,1] > X_te[idx,1].min(), X_dummy[:,1] < X_te[idx,1].max()) + y_te_rescaled = y_te[idx] - np.median(y_te[idx]) + np.median(med[idx_dummy]) + else: + # we need to adjust the data based on the adaptation dataset + + # first, select the data point belonging to this particular site + idx = np.where(np.bitwise_and(X_te[:,2] == sex, (df_te['site'] == site).to_numpy()))[0] + + # load the adaptation data + y_ad = load_2d(os.path.join(idp_dir, 'resp_ad.txt')) + X_ad = load_2d(os.path.join(idp_dir, 'cov_bspline_ad.txt')) + idx_a = np.where(np.bitwise_and(X_ad[:,2] == sex, (df_ad['site'] == site).to_numpy()))[0] + if len(idx) < 2 or len(idx_a) < 2: + print('Insufficent data for site', sid, site, 'skipping...') + continue + + # adjust and rescale the data + y_te_rescaled, s2_rescaled = nm.blr.predict_and_adjust(nm.blr.hyp, + X_ad[idx_a,:], + np.squeeze(y_ad[idx_a]), + Xs=None, + ys=np.squeeze(y_te[idx])) + # plot the (adjusted) data points + plt.scatter(X_te[idx,1], y_te_rescaled, s=4, color=clr, alpha = 0.1) + + # plot the median of the dummy data + plt.plot(xx, med, clr) + + # fill the gaps in between the centiles + junk, pr_int25 = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2), warp_param, percentiles=[0.25,0.75]) + junk, pr_int95 = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2), warp_param, percentiles=[0.05,0.95]) + junk, pr_int99 = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2), warp_param, percentiles=[0.01,0.99]) + plt.fill_between(xx, pr_int25[:,0], pr_int25[:,1], alpha = 0.1,color=clr) + plt.fill_between(xx, pr_int95[:,0], pr_int95[:,1], alpha = 0.1,color=clr) + plt.fill_between(xx, pr_int99[:,0], pr_int99[:,1], alpha = 0.1,color=clr) + + # make the width of each centile proportional to the epistemic uncertainty + junk, pr_int25l = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2-0.5*s2s), warp_param, percentiles=[0.25,0.75]) + junk, pr_int95l = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2-0.5*s2s), warp_param, percentiles=[0.05,0.95]) + junk, pr_int99l = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2-0.5*s2s), warp_param, percentiles=[0.01,0.99]) + junk, pr_int25u = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2+0.5*s2s), warp_param, percentiles=[0.25,0.75]) + junk, pr_int95u = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2+0.5*s2s), warp_param, percentiles=[0.05,0.95]) + junk, pr_int99u = W.warp_predictions(np.squeeze(yhat), np.squeeze(s2+0.5*s2s), warp_param, percentiles=[0.01,0.99]) + plt.fill_between(xx, pr_int25l[:,0], pr_int25u[:,0], alpha = 0.3,color=clr) + plt.fill_between(xx, pr_int95l[:,0], pr_int95u[:,0], alpha = 0.3,color=clr) + plt.fill_between(xx, pr_int99l[:,0], pr_int99u[:,0], alpha = 0.3,color=clr) + plt.fill_between(xx, pr_int25l[:,1], pr_int25u[:,1], alpha = 0.3,color=clr) + plt.fill_between(xx, pr_int95l[:,1], pr_int95u[:,1], alpha = 0.3,color=clr) + plt.fill_between(xx, pr_int99l[:,1], pr_int99u[:,1], alpha = 0.3,color=clr) + + # plot actual centile lines + plt.plot(xx, pr_int25[:,0],color=clr, linewidth=0.5) + plt.plot(xx, pr_int25[:,1],color=clr, linewidth=0.5) + plt.plot(xx, pr_int95[:,0],color=clr, linewidth=0.5) + plt.plot(xx, pr_int95[:,1],color=clr, linewidth=0.5) + plt.plot(xx, pr_int99[:,0],color=clr, linewidth=0.5) + plt.plot(xx, pr_int99[:,1],color=clr, linewidth=0.5) + + plt.xlabel('Age') + plt.ylabel(idp) + plt.title(idp) + plt.xlim((0,90)) + plt.savefig(os.path.join(idp_dir, 'centiles_' + str(sex)), bbox_inches='tight') + plt.show() + + +.. parsed-literal:: + + Running IDP 0 Left-Thalamus-Proper : + Making predictions with dummy covariates (for visualisation) + Loading data ... + Prediction by model 1 of 1 + Writing outputs ... + metrics: {'RMSE': array([704.24906029]), 'Rho': array([0.6136885]), 'pRho': array([7.63644502e-59]), 'SMSE': array([0.63500304]), 'EXPV': array([0.37380003])} + Insufficent data for site 8 Cleveland skipping... + Insufficent data for site 19 PaloAlto skipping... + + + +.. image:: apply_normative_models_files/apply_normative_models_23_1.png + + +.. parsed-literal:: + + Running IDP 1 Left-Lateral-Ventricle : + Making predictions with dummy covariates (for visualisation) + Loading data ... + Prediction by model 1 of 1 + Writing outputs ... + metrics: {'RMSE': array([3939.29791125]), 'Rho': array([0.42275398]), 'pRho': array([1.86615581e-24]), 'SMSE': array([0.85019218]), 'EXPV': array([0.1786487])} + Insufficent data for site 8 Cleveland skipping... + Insufficent data for site 19 PaloAlto skipping... + + + +.. image:: apply_normative_models_files/apply_normative_models_23_3.png + + +.. parsed-literal:: + + Running IDP 2 rh_MeanThickness_thickness : + Making predictions with dummy covariates (for visualisation) + Loading data ... + Prediction by model 1 of 1 + Writing outputs ... + metrics: {'RMSE': array([0.07307275]), 'Rho': array([0.64482158]), 'pRho': array([2.29573893e-67]), 'SMSE': array([0.60735348]), 'EXPV': array([0.40563038])} + Insufficent data for site 8 Cleveland skipping... + Insufficent data for site 19 PaloAlto skipping... + + + +.. image:: apply_normative_models_files/apply_normative_models_23_5.png + + diff --git a/doc/source/pages/tutorial_braincharts_fit_nm.rst b/doc/source/pages/tutorial_braincharts_fit_nm.rst new file mode 100644 index 00000000..c8db8725 --- /dev/null +++ b/doc/source/pages/tutorial_braincharts_fit_nm.rst @@ -0,0 +1,440 @@ +Estimating lifespan normative models +------------------------------------ + +This notebook provides a complete walkthrough for an analysis of +normative modelling in a large sample as described in the accompanying +paper. Note that this script is provided principally for completeness +(e.g. to assist in fitting normative models to new datasets). All +pre-estimated normative models are already provided. + +First, if necessary, we install PCNtoolkit (note: this tutorial requires +at least version 0.20) + +.. code:: ipython3 + + !pip install pcntoolkit==0.20 + +Then we import the required libraries + +.. code:: ipython3 + + import os + import numpy as np + import pandas as pd + import pickle + from matplotlib import pyplot as plt + import seaborn as sns + + from pcntoolkit.normative import estimate, predict, evaluate + from pcntoolkit.util.utils import compute_MSLL, create_design_matrix + from nm_utils import calibration_descriptives, remove_bad_subjects, load_2d + +Now, we configure the locations in which the data are stored. You will +need to configure this for your specific installation + +**Notes:** - The data are assumed to be in CSV format and will be loaded +as pandas dataframes - Generally the raw data will be in a different +location to the analysis - The data can have arbitrary columns but some +are required by the script, i.e. ‘age’, ‘sex’ and ‘site’, plus the +phenotypes you wish to estimate (see below) + +.. code:: ipython3 + + # where the raw data are stored + data_dir = '/data' + + # where the analysis takes place + root_dir = '/braincharts' + out_dir = os.path.join(root_dir,'models','test') + + # create the output directory if it does not already exist + os.makedirs(out_dir, exist_ok=True) + +Now we load the data. + +We will load one pandas dataframe for the training set and one dataframe +for the test set. We will also filter out low quality scans on the basis +of the Freesurfer `Euler +Characteristic `__ +(EC). This is a proxy for scan quality and is described in the +publications below. Note that this requires the column ‘avg_en’ in the +pandas dataframe, which is simply the average EC of left and right +hemisphere. + +We also configrure a list of site ids + +**References** - `Kia et al +2021 `__ +- `Rosen et al +2018 `__ + +.. code:: ipython3 + + df_tr = pd.read_csv(os.path.join(data_dir,'lifespan_big_controls_tr_mqc.csv'), index_col=0) + df_te = pd.read_csv(os.path.join(data_dir,'lifespan_big_controls_te_mqc.csv'), index_col=0) + + # remove some bad subjects + df_tr, bad_sub = remove_bad_subjects(df_tr, df_tr) + df_te, bad_sub = remove_bad_subjects(df_te, df_te) + + # extract a list of unique site ids from the training set + site_ids = sorted(set(df_tr['site'].to_list())) + + +.. parsed-literal:: + + 362 subjects are removed! + 356 subjects are removed! + + +Configure which models to fit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Next, we load the image derived phenotypes (IDPs) which we will process +in this analysis. This is effectively just a list of columns in your +dataframe. Here we estimate normative models for the left hemisphere, +right hemisphere and cortical structures. + +.. code:: ipython3 + + # load the idps to process + with open(os.path.join(root_dir,'docs','phenotypes_lh.txt')) as f: + idp_ids_lh = f.read().splitlines() + with open(os.path.join(root_dir,'docs','phenotypes_rh.txt')) as f: + idp_ids_rh = f.read().splitlines() + with open(os.path.join(root_dir,'docs','phenotypes_sc.txt')) as f: + idp_ids_sc = f.read().splitlines() + + # we choose here to process all idps + idp_ids = idp_ids_lh + idp_ids_rh + idp_ids_sc + + # we could also just specify a list of IDPs + #idp_ids = ['lh_MeanThickness_thickness', 'rh_MeanThickness_thickness'] + +Configure model parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now, we configure some parameters for the regression model we use to fit +the normative model. Here we will use a ‘warped’ Bayesian linear +regression model. To model non-Gaussianity, we select a sin arcsinh warp +and to model non-linearity, we stick with the default value for the +basis expansion (a cubic b-spline basis set with 5 knot points). Since +we are sticking with the default value, we do not need to specify any +parameters for this, but we do need to specify the limits. We choose to +pad the input by a few years either side of the input range. We will +also set a couple of options that control the estimation of the model + +For further details about the likelihood warping approach, see `Fraza et +al +2021 `__. + +.. code:: ipython3 + + # which data columns do we wish to use as covariates? + cols_cov = ['age','sex'] + + # which warping function to use? We can set this to None in order to fit a vanilla Gaussian noise model + warp = 'WarpSinArcsinh' + + # limits for cubic B-spline basis + xmin = -5 + xmax = 110 + + # Do we want to force the model to be refit every time? + force_refit = True + + # Absolute Z treshold above which a sample is considered to be an outlier (without fitting any model) + outlier_thresh = 7 + +Fit the models +~~~~~~~~~~~~~~ + +Now we fit the models. This involves looping over the IDPs we have +selected. We will use a module from PCNtoolkit to set up the design +matrices, containing the covariates, fixed effects for site and +nonlinear basis expansion. + +.. code:: ipython3 + + for idp_num, idp in enumerate(idp_ids): + print('Running IDP', idp_num, idp, ':') + + # set output dir + idp_dir = os.path.join(out_dir, idp) + os.makedirs(os.path.join(idp_dir), exist_ok=True) + os.chdir(idp_dir) + + # extract the response variables for training and test set + y_tr = df_tr[idp].to_numpy() + y_te = df_te[idp].to_numpy() + + # remove gross outliers and implausible values + yz_tr = (y_tr - np.mean(y_tr)) / np.std(y_tr) + yz_te = (y_te - np.mean(y_te)) / np.std(y_te) + nz_tr = np.bitwise_and(np.abs(yz_tr) < outlier_thresh, y_tr > 0) + nz_te = np.bitwise_and(np.abs(yz_te) < outlier_thresh, y_te > 0) + y_tr = y_tr[nz_tr] + y_te = y_te[nz_te] + + # write out the response variables for training and test + resp_file_tr = os.path.join(idp_dir, 'resp_tr.txt') + resp_file_te = os.path.join(idp_dir, 'resp_te.txt') + np.savetxt(resp_file_tr, y_tr) + np.savetxt(resp_file_te, y_te) + + # configure the design matrix + X_tr = create_design_matrix(df_tr[cols_cov].loc[nz_tr], + site_ids = df_tr['site'].loc[nz_tr], + basis = 'bspline', + xmin = xmin, + xmax = xmax) + X_te = create_design_matrix(df_te[cols_cov].loc[nz_te], + site_ids = df_te['site'].loc[nz_te], + all_sites=site_ids, + basis = 'bspline', + xmin = xmin, + xmax = xmax) + + # configure and save the covariates + cov_file_tr = os.path.join(idp_dir, 'cov_bspline_tr.txt') + cov_file_te = os.path.join(idp_dir, 'cov_bspline_te.txt') + np.savetxt(cov_file_tr, X_tr) + np.savetxt(cov_file_te, X_te) + + if not force_refit and os.path.exists(os.path.join(idp_dir, 'Models', 'NM_0_0_estimate.pkl')): + print('Making predictions using a pre-existing model...') + suffix = 'predict' + + # Make prdictsion with test data + predict(cov_file_te, + alg='blr', + respfile=resp_file_te, + model_path=os.path.join(idp_dir,'Models'), + outputsuffix=suffix) + else: + print('Estimating the normative model...') + estimate(cov_file_tr, resp_file_tr, testresp=resp_file_te, + testcov=cov_file_te, alg='blr', optimizer = 'l-bfgs-b', + savemodel=True, warp=warp, warp_reparam=True) + suffix = 'estimate' + + +Compute error metrics +~~~~~~~~~~~~~~~~~~~~~ + +In this section we compute the following error metrics for all IDPs (all +evaluated on the test set): + +- Negative log likelihood (NLL) +- Explained variance (EV) +- Mean standardized log loss (MSLL) +- Bayesian information Criteria (BIC) +- Skew and Kurtosis of the Z-distribution + +.. code:: ipython3 + + # initialise dataframe we will use to store quantitative metrics + blr_metrics = pd.DataFrame(columns = ['eid', 'NLL', 'EV', 'MSLL', 'BIC','Skew','Kurtosis']) + + for idp_num, idp in enumerate(idp_ids): + idp_dir = os.path.join(out_dir, idp) + + # load the predictions and true data. We use a custom function that ensures 2d arrays + # equivalent to: y = np.loadtxt(filename); y = y[:, np.newaxis] + yhat_te = load_2d(os.path.join(idp_dir, 'yhat_' + suffix + '.txt')) + s2_te = load_2d(os.path.join(idp_dir, 'ys2_' + suffix + '.txt')) + y_te = load_2d(os.path.join(idp_dir, 'resp_te.txt')) + + with open(os.path.join(idp_dir,'Models', 'NM_0_0_estimate.pkl'), 'rb') as handle: + nm = pickle.load(handle) + + # compute error metrics + if warp is None: + metrics = evaluate(y_te, yhat_te) + + # compute MSLL manually as a sanity check + y_tr_mean = np.array( [[np.mean(y_tr)]] ) + y_tr_var = np.array( [[np.var(y_tr)]] ) + MSLL = compute_MSLL(y_te, yhat_te, s2_te, y_tr_mean, y_tr_var) + else: + warp_param = nm.blr.hyp[1:nm.blr.warp.get_n_params()+1] + W = nm.blr.warp + + # warp predictions + med_te = W.warp_predictions(np.squeeze(yhat_te), np.squeeze(s2_te), warp_param)[0] + med_te = med_te[:, np.newaxis] + + # evaluation metrics + metrics = evaluate(y_te, med_te) + + # compute MSLL manually + y_te_w = W.f(y_te, warp_param) + y_tr_w = W.f(y_tr, warp_param) + y_tr_mean = np.array( [[np.mean(y_tr_w)]] ) + y_tr_var = np.array( [[np.var(y_tr_w)]] ) + MSLL = compute_MSLL(y_te_w, yhat_te, s2_te, y_tr_mean, y_tr_var) + + Z = np.loadtxt(os.path.join(idp_dir, 'Z_' + suffix + '.txt')) + [skew, sdskew, kurtosis, sdkurtosis, semean, sesd] = calibration_descriptives(Z) + + BIC = len(nm.blr.hyp) * np.log(y_tr.shape[0]) + 2 * nm.neg_log_lik + + blr_metrics.loc[len(blr_metrics)] = [idp, nm.neg_log_lik, metrics['EXPV'][0], + MSLL[0], BIC, skew, kurtosis] + + display(blr_metrics) + + blr_metrics.to_pickle(os.path.join(out_dir,'blr_metrics.pkl')) + + + +.. raw:: html + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
eidNLLEVMSLLBICSkewKurtosis
0lh_G&S_frontomargin_thickness-3808.5843810.314419-35.106351-7579.6599220.2529341.087225
1lh_G&S_occipital_inf_thickness-3468.2969310.230447-35.096839-6899.0850230.0300630.430915
2lh_G&S_paracentral_thickness-2977.8981550.337686-35.035891-5918.287470-0.0010400.755307
3lh_G&S_subcentral_thickness-3471.6674670.332549-34.990710-6905.8260950.0729700.560048
4lh_G&S_transv_frontopol_thickness-1565.9163980.358683-34.900294-3094.3239560.2705021.269709
........................
183TotalGrayVol146369.7418180.615736-3.067824292776.992475-0.4900893.996252
184SupraTentorialVol152270.6366050.345575-1.442556304578.782049-0.3022172.920578
185SupraTentorialVolNotVent162984.4677980.347517-1.014633326006.444436-5.03521563.806125
186avg_thickness-10627.0076790.581347-36.109891-21216.506518-0.3438041.197945
187EstimatedTotalIntraCranialVol168794.7121190.253537-0.262857337626.933077-5.15192666.531844
+

188 rows × 7 columns

+
+ + +.. code:: ipython3 + + blr_metrics.to_csv(os.path.join(out_dir,'blr_metrics.csv')) + diff --git a/pcntoolkit.egg-info/PKG-INFO b/pcntoolkit.egg-info/PKG-INFO new file mode 100644 index 00000000..e7a3d8a5 --- /dev/null +++ b/pcntoolkit.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: pcntoolkit +Version: 0.20 +Summary: Predictive Clinical Neuroscience toolkit +Home-page: http://github.com/amarquand/nispat +Author: Andre Marquand +Author-email: a.marquand@donders.ru.nl +License: GNU GPLv3 +Description: UNKNOWN +Platform: UNKNOWN diff --git a/pcntoolkit.egg-info/SOURCES.txt b/pcntoolkit.egg-info/SOURCES.txt new file mode 100644 index 00000000..e5a00b91 --- /dev/null +++ b/pcntoolkit.egg-info/SOURCES.txt @@ -0,0 +1,35 @@ +README.md +setup.cfg +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/__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/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..fa3c7509 --- /dev/null +++ b/pcntoolkit.egg-info/requires.txt @@ -0,0 +1,14 @@ +argparse +nibabel>=2.5.1 +six +sklearn +bspline +matplotlib +numpy>=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 From 0665da567c2e2d68625fad0241aeb33b4a2c3b46 Mon Sep 17 00:00:00 2001 From: Saige Rutherford Date: Tue, 27 Jul 2021 11:36:13 +0200 Subject: [PATCH 02/15] update docs with braincharts build --- doc/build/doctrees/_templates/class.doctree | Bin 2949 -> 2961 bytes .../doctrees/_templates/function.doctree | Bin 3168 -> 3180 bytes doc/build/doctrees/environment.pickle | Bin 227131 -> 236567 bytes doc/build/doctrees/index.doctree | Bin 4001 -> 4097 bytes doc/build/doctrees/modindex.doctree | Bin 322256 -> 309189 bytes doc/build/doctrees/pages/FAQs.doctree | Bin 2347 -> 2359 bytes .../doctrees/pages/acknowledgements.doctree | Bin 4514 -> 4526 bytes doc/build/doctrees/pages/citing.doctree | Bin 16720 -> 16732 bytes doc/build/doctrees/pages/glossary.doctree | Bin 2279 -> 2291 bytes doc/build/doctrees/pages/installation.doctree | Bin 14457 -> 14469 bytes doc/build/doctrees/pages/modindex.doctree | Bin 0 -> 308570 bytes .../pages/pcntoolkit_background.doctree | Bin 44708 -> 44695 bytes doc/build/doctrees/pages/references.doctree | Bin 16933 -> 16945 bytes .../doctrees/pages/tutorial_CPC2020.doctree | Bin 46877 -> 46871 bytes doc/build/doctrees/pages/tutorial_HBR.doctree | Bin 45211 -> 45205 bytes .../tutorial_ROIcorticalthickness.doctree | Bin 79195 -> 79189 bytes .../tutorial_braincharts_apply_nm.doctree | Bin 0 -> 60936 bytes .../pages/tutorial_braincharts_fit_nm.doctree | Bin 0 -> 35640 bytes doc/build/doctrees/pages/updates.doctree | Bin 3031 -> 3043 bytes doc/build/html/.buildinfo | 2 +- doc/build/html/_modules/bayesreg.html | 6 +- doc/build/html/_modules/index.html | 6 +- doc/build/html/_modules/normative.html | 1298 +++++++++++++++++ doc/build/html/_sources/index.rst.txt | 2 + .../html/_sources/pages/modindex.rst.txt | 50 + .../tutorial_braincharts_apply_nm.rst.txt | 567 +++++++ .../pages/tutorial_braincharts_fit_nm.rst.txt | 440 ++++++ doc/build/html/_static/basic.css | 109 +- doc/build/html/_static/doctools.js | 14 +- doc/build/html/_static/graphviz.css | 19 + doc/build/html/_static/language_data.js | 6 +- doc/build/html/_static/pygments.css | 10 +- doc/build/html/_static/searchtools.js | 38 +- doc/build/html/_static/underscore.js | 37 +- doc/build/html/_templates/class.html | 8 +- doc/build/html/_templates/function.html | 8 +- doc/build/html/genindex.html | 6 +- doc/build/html/index.html | 8 +- doc/build/html/modindex.html | 348 ++--- doc/build/html/objects.inv | Bin 2655 -> 3044 bytes doc/build/html/pages/FAQs.html | 12 +- doc/build/html/pages/acknowledgements.html | 8 +- doc/build/html/pages/citing.html | 8 +- doc/build/html/pages/glossary.html | 8 +- doc/build/html/pages/installation.html | 8 +- doc/build/html/pages/modindex.html | 346 ++--- .../html/pages/pcntoolkit_background.html | 8 +- doc/build/html/pages/references.html | 8 +- doc/build/html/pages/tutorial_CPC2020.html | 10 +- doc/build/html/pages/tutorial_HBR.html | 10 +- .../pages/tutorial_ROIcorticalthickness.html | 18 +- .../pages/tutorial_braincharts_apply_nm.html | 758 ++++++++++ .../pages/tutorial_braincharts_fit_nm.html | 656 +++++++++ doc/build/html/pages/updates.html | 8 +- doc/build/html/py-modindex.html | 6 +- doc/build/html/search.html | 6 +- doc/build/html/searchindex.js | 2 +- 57 files changed, 4388 insertions(+), 474 deletions(-) create mode 100644 doc/build/doctrees/pages/modindex.doctree create mode 100644 doc/build/doctrees/pages/tutorial_braincharts_apply_nm.doctree create mode 100644 doc/build/doctrees/pages/tutorial_braincharts_fit_nm.doctree create mode 100644 doc/build/html/_modules/normative.html create mode 100644 doc/build/html/_sources/pages/modindex.rst.txt create mode 100644 doc/build/html/_sources/pages/tutorial_braincharts_apply_nm.rst.txt create mode 100644 doc/build/html/_sources/pages/tutorial_braincharts_fit_nm.rst.txt create mode 100644 doc/build/html/_static/graphviz.css create mode 100644 doc/build/html/pages/tutorial_braincharts_apply_nm.html create mode 100644 doc/build/html/pages/tutorial_braincharts_fit_nm.html diff --git a/doc/build/doctrees/_templates/class.doctree b/doc/build/doctrees/_templates/class.doctree index 0aa5ab41adca44e1ae0e40750563c841c8247444..a758947ba33fb28d35d232d2dbef91fa18ea7025 100644 GIT binary patch delta 67 zcmZn_pD51Kz&f>UBZ~*4dZ2!2acWVqesN-EdTLQ=Nk(c>T7FTAeo<;cezAT@YHmS1 Uh`BkPQIDCqv?Oiw43@WS03GQVPXGV_ delta 69 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 S!%|ittnN9&tu^8Hmj4CuQ4S&i delta 38743 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
1G|JvFi)ZF+hx`*FZJS?kAcJ!;ed@_4qe&JQ`> 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 GHvWGh>(ojB diff --git a/doc/build/doctrees/index.doctree b/doc/build/doctrees/index.doctree index ee8abd940cacf4cbbab88f42564266b513b6a863..8f8b2a705b5d0a0eaa05dd6a7203c4a484c0bc93 100644 GIT binary patch delta 269 zcmZ1|->AUSz&iEYMwS{zbvOOc;?$yI{o=&T^wgr#l8n@%wEUtJ{i4)@{9^r*)ZBu2 z5Oecv#;q&~em#l>iRr08@zRp~qRhmc_@tu5%)I1`#G;bo__WND_`KXHZBzVul<_D_ zEGWpSgeXkSaEzUzk+CXcO~$&64H=sz$FONJZk^oD_MP)o#+i(B85c4xO>Sp57P?xR zp_O5nA(Nqz^BMN5Oi~<-3=BPdsYONkMe(V5$@wXndFfMnSW8RNHt*(Y nWtMrAalg2SGcPeWwWP8jwRlSHl#EAy#Tg1f`u^q^K37Hn$|G$1 delta 173 zcmV;e08;;fAfX=wfCQD6u>^bp8#*s&Z*65SaB^j1Y-w&~buVFVWNl$`FLP#hv!?;I z1Po0RRAu4rOw3Z*pH{Zewp`X>Mnfj0IIxMzb3Wg##SkXxnp)32tF+ bWpsIPWpk8$lxW^gb7&xalxW+tybeVHbOb|Q diff --git a/doc/build/doctrees/modindex.doctree b/doc/build/doctrees/modindex.doctree index def64f3d5f527b770ed8a3c5f2f96f0e7d33c465..26f90adbbdd7ab0deb77a9306047d239d3a935dc 100644 GIT binary patch literal 309189 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 literal 322256 zcmeFa378y5bvSNayV6RQY|CEDURiFR8p*4bZLq;IvMrxDwq-{WAYaI{yS+Qpnw?qC zA!!{9P6EV+1_EBd5KfzL{Gag#9PJK4YH20}ZZ? z2WHCvL3_S(bY)RxNjxCn+ZivdRqNsW&dzu_9xAun&FXlk4R|3QORAGCgn4kHR&KT6 zCH!7iFVFB_%O^Xt`eD30vA-EkVVO=m*e*}wr}@g7%1C7u_E=dTZ{J#J%!FHKo8biZ z+1XmIPtKH^TU%2Hx888s&UT|w+h1*O1p;m5&~2S*OjZ%kv1SV(Kk1sqo$<;M@iO+# zAPT2fWdp!m34kD{!oSnu-< z#Bvc}xrGlUITjpo8)!k3Fka&0p{bz6*cMdls)tQ5LKGORHwwd@m6KpN0HK${0G~+F zxddNUqv26C0seV-2fOuGbswa$>m00~}C3nupb+ z9=-!W{+swH;=q-&39{nQOl@0hwv3Z+IgGVd*gO!^BoR0atOPK~~^o zkn*9jEMB3wAdxoS*m%#l;a#azs)*!%jbtT~j9kIsNCL5hjR2G&!!i49cYLQ`$A3U| zd<^RtL=WKK4x6HiZV#^2LDCy=*2Nsk%xMaexnW*_C{>yL)50yL!62*YoZ|3osKV z$>Zlk!RI8KqzXQp;>prRy*6i1N=VNm;#IR@9pvrwzC$3$(*;(<1JycH7Mg>i?r^;P zW-hXCO%VFCNigFh4^+d06Ce?{KwJCMRQLyg`R|jpOyuH00Ixkai}PEyH4i#BoSJOR zl&kgmcuA{0IUlctH{qe#T6Ln@p1+3`XoE}&+w<|!`?#oYDWMFC4T@JM=POT(2Ut@R z&^(mlx&}wXg8>F% zmO;aB3%0TTw%yhkkH9Xujg9LP2#7)PUGrDJs*FU-!9FtOMdg!HfyHa6CIODYS`|b> zxn@8St;J@zL>k}_=>A}=jYP*ofg}$=r?U|oFP@!4nLoFM(=0Sc4NxpP4qG3IjwaeL zi>d)o`GGmCqEn%sG57>$lH&|VC+NO94mr^Df~A$;B4C^nIL2_e%F!q><%4W}+;824 zBZe`oGj5E>gwcfb8bErjj~;`6*f@)p;7>NxqCxnnczoieXhl`a^#BD#xfy^#1**4& zcCFxNUJ)fNqf4-Zk?11PPi1|PzSnRb9=c^w;G!#9?dl9OZ33`s1F$?MtIV)R6EB-Q z0Su6^){<0OXfv9U*KS%rlHE*EdCw~IiJE3yf}L_*gG1` zZ5oadoCTMLCFJZGa#(2n9VRGEVTC!1} zHrkDsKpn|z_S+`JdkYA`lv-;xoTyG!C+6=)y1bjyNV|@p4uni^S^dh5{%x!HY@)`LR`R@-HjE%%A+H>}|0@-45BXW+R$00hD7ffeQg@ zd@tUsL@fSPh_exXegTjv@}HYae)V-^XAY1GggCbt5FXp}Cj8a{ocsyD4aD-za$r=p6_wcM{N56d8EYk?36xln{$u zvGiFBOn#d~En!Q%nH9CsIU$Q2Lus7iby?b2%I}$AAp*wcV=ei3_}=}tC!?kbM%UMosFm|HTM1sV2w12pMI)PTu1;Ppy67N1S^L<99FR8eQ3gyYQsD%%zJ0M!!>)GMi? zu0Rd$wYaNcTfFyxlu!869@QV9x}>3dFDqKPPYsbp+hx1(%Wn4XsAgZ28Xk~S5{YI% zqncfc(^R(1AMD1zNHiWpQp!}hQ)}-#P_A`CJCh<_+v?2DHk$2yEijkbw|5$t)OQc8 zXey9kAt1lSIEH$WZxmipGfH z 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

nx%&;EG}NU8+>@9C2C4(dSF1N)@bMr~%0$ z={GB+(?rrWss2q^K`4@beE5i0*Pdk9_WCN5KL?B>R+o;u-c5lTaSPe~7;k?EgUvMF z{Q*S0(HeP6js+*hCal{+kZ$kPh{Ubxw?S#suRJHU;iK1G4Qli5ZHfxCLQ=gB>8@4SpwS1w6Trw;!b&x76yQPkk z3EE?>u-Qz38p74frH)%bT8O2Nzie; z3Om%wS6MPRg^X1cw+b9Ry_P53X>%!=aRj&%Bj$(puq~=#>JA zlVqpy8eH5ZT5VeoiSNnXwI#*A+iJ8t{5=X;&i{l%&)UNUSG+#TJ2`2^D}F<-5bt;Z zjp&y+qF-=8)D~P^3HzKwDG}}CG{t+UMyy(l4CKT^4h@J%-(Nd`wi!fk>YXu6shN`u zVX&iXR8wLU8f-7=wtYF=y01vf@w3qVATWP)kOF(ap?&%-D@EBSYjteEnQxN96eTn% z%U;e%Spt8OOszd6k(7lFrNofrRHqq|*ubwHI4p+b&mBOmAsLSgx$aPVrcY!2Ak<>#Am7IX-prbK-*$!Ybz??i$Pqn z$Fd9JexE{{&R2hu0yQY?Wf1o+XqyP)KAwb9IEbsc842QE19hbj;v!-)h|5P53*x>J zUb_ZydyrKw?0OGsRwLP8c_FjIEAUTRv;PKU4-d#L73CfB)q~fm4D7!2$?9@Jmd7nc zz8}1Vcsbm6Q=DwzRXA{MNE6bYApQ*&8WY~9>Wv+ExIcu<$^0iH1za%Qc%zv$z?pS8 z{E28jV+a{RHNhgB8^4H^q9!fT5Oyf2mNJnWNw5-U{F4yzE|bDp9XbCL2jayvM-Y!8 zv&{~rgpYkZ=aUsOJ1wJbx406V+I6kcMrKfxma^^GSvmie4lJ`7jZ$`@Ln({V@%D?H z!Weyej6+jm4##CbzA0l4XN;IUYv6g3`S=Dv2Kal81JNR2At=Gw=xaG8M1(Lp_Wy@x zKE9Ll>Fo}DvZWXKG~-Z8Nbl_lpzWj=o4PfjJc^`{Ufz3Yq!$}N>E+Ht`;sCGAuTAK z`FK!5=A%?ZiEvuYdmZjejz9Pd?tzMGKAoNxZeHM9` zvyH2#MATGp%3Cb3(QMx?;M>yapqTE%6cd zRazx%tY1T+LSsEefx2S7*gEGwj5hTZE}XH;*ofajVM8PSW(w35@otigAD|khalD@b zb;U81ORR@n=tF#x@uO7pG^U@VKwUBI{xd~eL_?{wjX4Uo=WIIbZ7oZOx3Mp~h%kIKhZZr=vX(=6{kq_e{AqLMK&k<{ zXL$o@=ud{lK)rVD3K>f)Sg>;~PGX-jlvR(%2=Yfb4f%@ZHh{-@bcR*R7{u$wrL-=`5<1h&^ow1{SSJ>IsIJ0Z&NgFQcdR2DEdZpH?!+A3! z1$Np@LmW2)d)(m4);`#4TYxRL{MZ@3vo_zHhT~_()j2c0Z%B5l5$@{_plwFW3%CCM ze{<{aVXt7L@KE>@Sqm^G=_rx*D8x0>JS&wr3Ol{FR}9xqi|cWH94GP^2O{lpjYMvA zC?&=<=SP}xjSZ}EXh4kX{T)EtjO!O}{e9lw`dfw`2ALRj=xBR9E{0MgtW@H7jJelW z47ZrCdNfUQ6sH_ev_}&j9@TOvB}NmcCe3I<9t=A)AV$-r4xnvDQwP`kMW-D101krj z9n_~gh1HYu|J;EjyHl0|&+c|8)rJu-To?HN;dOzZLcz`XpL7r%TTv~CQlDU@5*1Zf zSN(}m)m0sOeEl0I_sb6C+T#n!{h~uDq4YRs(kMM_;A0LA2&MOs18AGE{=#*EsUnU% zJz!P9F6!(ibg7d`{JmbuM&2y=lg?$wyR84nx<7H`rMxb1B}Zw614{Pj!27$GI+PNl zgJYj&bRf;Ya}aqkI)3c{+GcbNrU@Jq!M07vM;?|O6k>b5pA~wxs%62lj|1)oJX8*d^6q zYHj{hwg!oJt>8r=xGaOw9o$BR;cs!PN|UY1s93nMeBLbV^h&TA2dg~@uM;tG7Z$7) z)*En&?-Z;E;fcNgRH`(048$(z*vl8qOmOQ2Gi!u3c0U?&NIn-$>oNQ`6rP-ar-QiJ zV;JvfzTKge7{j+FfVLaM*wjr4<36kCSY`y=Sm9HSYDBA!Tl4< z7_6~>B9uJN<_9_DV*}GwR0rp`G;Vm(x*Hl zV&Fou1E#sYXDQD=76uO-$i$TAlkJpeSk8p3IcsE@!vZ zsFCmgJJ1A@_Z9q;)**cw3Fqm`UlQek2fa^M{vP(jNc-*4Vs8Be;&J`JY~fzK#C`WC-Y4y(pd6n*CWroPOs}N=&@}n}OOv7gQM& zC`xR*H{&?P8n+%LMUHbYDGBhr4)KU@GIU_-v7I#_)nuo|buS;d9mFQ*uZBNK8b_=V zieARd;Qd&s#FdJebOj-;F<&)`>p6<+98k0c5i@kxI+PMC7o3`SM+So{MYs)Ft6Sv3 zJ`N3tm5b#Lplt@lF&*hhLRGv1@DR-Juuk1Mtgf7Yu>)y#=U~S0HiuGe7y=hkz_GiQ zrl7P$3gsU^ZWE7Qet9>@#J3yL7n{3iMJkQ1>d-wug;E5^syg(ctvN6`_)1o)*P7!7 zj^cF=DB8k_$--}RC?zxprzVZ&KpxZ_8W5VpcK~fOPzHo|;_A9sD-Zpyg{6GC7tq4= z5g-s)^4AV@+C9|=r9S9T%HmFZPmxi6!>{>;gI^<}{OFk{g|Ed*ooHY$pyfMID&v*9 z#~j!rW)uG#yCHTq5uCg>e-W<7H9USf3``XYa@qizc(OGK2P5LHLT0JfO;wuaVqC0W zZ2NvEcHe|0znif*cI)$wh?GJZDsI`GD7rRHQ~bS4sf3g%Rr05x+1_Ul7XS9x`CQR;)pR0FURZd0jV z6xS|*B2r&qjRd1sAz037+HBP9pMqUIcWY~= z092F*Wh+2Rp%@$i|h{ zt#&V(=f5VrtX9&DWVm#eaP(5LB>I>3CTMmJkE^PPvbTpq#&!ckRf z_lO1ZM<39($Yh5k(SB(YIjzu5b1=Ln(%*#@gq8-E*!>{7;Vr5gW^L!F>GdZ0W>ocKsdZKWY}$@*jf*8lCd9<+=VI z;6pHc(0*MAE(9ODY?-GBb%?1)beYErHU|ftj#B^+y#M}*dUd*1AFmXu16=Xun_f$* zokTb#sQ#i?LF+lc5C5cfrbZBRzD_ewl#i$jql?i45zdsPS#bh%sV&iQ&eXxs5baF8 z7{8Ft6vq?Qb!Se=mLLxiFLqLCya8XKx_n3i(`iCnh}v1!m&pqHF&2-f$Z)HQzm%CR zG)gC(Rj4-M4bL1cRIBm@CT8-NC*dkdm)3Z*vusE$kJk7&RVuiwCr+AVQZzY7e-X(! z>V=Yf=4PCuP)uWiHDUf1tRQ58&&sCHB9?o;tT$|jLLcD#KLu%%dj(({8K1O0dNl=V z3}xBTXwJ7YSi_vpM{9{VL@>2K0H*4Njp;6`c^cCe1?q~a>0+dEJZ_^}r4bA$P*(&O zs8KHDL1VcWeBumV6|jHXEPOxJEREq`Fi^K66d$3grL$#3wfYD}hilqDO;n%y;6&cA zZB8f$i4@+YA!Qoj`vL_Zxo^C&``}vyHD$UF9!t>&PgB*>eITk$)d#;Ns!!bqyx~s! zAY{KHebA=?WG9%f>|R)Lsk7Q!!a&_dLY}IY?gde;?uFTCl_loXWiMkW;-p$X?n1>z$o z72F+RlZvC^b?l^~-=0+*c&2b}@w`inB<3-cSSWgYtB;h^Brqkx{%e65ASYMhpOkZo z-usG-JyQZTh_9N97Tf;?=4mXGZmLqPDyN_3%OE60mAnby7r&2+-HwepQsShBQtQxJf;9xx5U{%QL864*`W`Gs)%?Ynq8Lhy@RwZw&;dxk#=m>rRmh@#yc{rf7 z1>ack1PjCAg;1W~iUr?rw-x4X6`I9LC0}p!)x60@9cD3O?4B$H29(Fy;f@-^T4l5a z(6#FHz)=2FR+QIUu1{7=`4JDQ>}wT9phCbEg#->$t&CS7#xJVl7v3XgHI+xUv%br# zHW8afeG2FR83TYm4WLe!1dw9AIyqiz4&}E-5N6#1I6(G6xiA5n&)5mB*j8Zz(z+(_ ztV*^N24o4jeW6eD{}1)4Rx&qpC39m3O2)qd3#~a97k*G2a5dB5e>73rPuzWMp5ZKV)XY~3T_z>}k{~4*=o6U@|BPg<7J-s; zLAIM113NX`B2QQI9j{LfH`J>mBgp$Lz;eE_#@-P*?j@C<#A3k^P_a8Aw zmyVnW9_SY~2mUL81O7ilwQR}tVH_#2^3Dptpq!I`;+i}1T6Hjns;@_PM2W&J=&ZcJ z&-B&eX9C!{EK4Hie=~s#2r5z|9%G$;9eLd6(bKPrh@X$-xLh4?cn?PmyUOI-m zoX|zE`r=hVE8_+Rd8|3OWL^WDTgX)cKUaIXR9Be+xR^D*;}X!1bkb36J4t;VOohAd ztaRm|)3C+Vowgx?>PV-xAF%gxE%d(~2CPVo+1n%CQUE>+QIFw>CeG zmldpo?Mhy$QfyUr!u-@L)L>W8baSKqp1FLrBAx`#A`8X3H#!O+>~E!5ftfSxNo&;M z#CYgeBKQCWYu*^#;#;lkfO1>CF;FYeAiRM@lLky>3)MV)gE?rqUM~T7d3pi>;R#Ae zT~z_^y!RP!(98k0g!(%``;!vs$VaIkRodnhYs;Ta@Qq2-p861-5K%C8z1Arx; zb12n@C4*_k9l?kvG$YbgrO9Vi;+7shTvO{>i6)s9&iMll$lD|z2qeGQp_D~(yt_vxnG2T#O~8zU7zrN`odG8w zP&o|Pz%@_`{oSOFk8ug(DMM)y`9pGfLxmQc^M@`2PSg2A1^hzg4~c}Gj&p~`yw)(d zDov-{Aqm`R?$A7qKwOBzY^=A%aIa8xXI*jqQ-&|T=%K;(pc5Nse^e(77GJ5Ex<+n3X zBWrdx_PVT@|F9|lOR91@DS=lWKOkkFV2Rc$nKT*d`tl8xCdAb*}hg8=y&I0r}HOn1mOQH4`HX$Tg_Tc>x*m0nN&?Qn^sG719ZK&9yjj_=wtp65h;V zsAyRMsAx4-Y*C6!@%$4sO{?fVFYk?tKDb&hl!h^$7_X!EW>W-Q86Q&6w2}@k?@7L+ z(JIqBZMJ3{qG0=xC=8104APUf)@$9SgzgU=k=`!sqe|^27U|s?3lvABZZzK53bJ2e znZ`6|+Wf<@f{MvLHs*0`^Cck1Q|J4IKHxRaqH9Ce<78+)Vm)XDzlj1h)Ss!~ zk7cliU0W=}>`b5pAqKwkvt)Q&6#kVI5;VRSava{SmP2QqBx{F%+1*&8s-=ynsMg5O z!XYASp3Q#Pl{bjWQ|D*#W^J8lhFGKAPliSFN{V83=JgJO_L%&v+bB>&zItF@*Fuwo zdA%hGldyT!OpKVKfO z#5QvNKZvGe%st+RN9!%-rpvJq-`epBg=+hfVrnKS1G}#jp)I#@cK>V85N-D#!!Oj- z%;$J^v$bROI7Efx&HfT?_7RY__*_KVZRuFZTFGKc_KfI`#L$);g{QfQ^IwVD|Jk61y-)*2Ho!(16}9j9R{hY3SUTaaZ8 zG|e>Z5UO(8>ha3$F%)Mf7E#q28du5$+6j`4+1ZKHC{*Za*eMjKfprh;#2(NjVJA*Z z!X#`bG!r9s;wg|#XFGxTNISuWn%RjThyel%gwf4`F4Hhs0jOg&R%}s7Y@y50h?@?H$u@Ra!{~oL$WFuIzbh{2= zXqgkU+1l~alN{qE>;Y#GHu8@^+m4g5k5Hh75|pYNqir8%u!eQhiE7N+Y79dk;&%sr ziE5rUNS~)bT`}#`qR+ct;~Mz{M_(JzHfNurkf8B>f&z8L*VyK-F*RP&d;kALHA!Rl zI|{TdcDDYeTmdxiW+hdXT z;nn2(i`7Dt?+h9(z&S{L$TisnMUU_?+eE*&@#6iE!NEe4gZb^^@3i<^8^(jJ)ejg~ zjgyYtumQFd)T<|L7}~Jm$ie(huQ5_@di+QQxr};=iagHcc+2r8f2sk;rnfU!j^cb-x%i^JhP~j6% zRV^LH5Y?s#g#Q;&dFntoZ`QU8lvjhE?h{{tlnm5}iqq+8sGJ0Cd^sMR78qARd(lLx)$3WdA`V^{Kn$e=# z6pTKfs5~{Jd9xiddZA&SBHW(KKT4L%pSC6$C1{A@`$!7Dx2S4qzKd#8@cj)$<*E73 zo9&SAeYIif2~cduZwA}hPn+#`5hTR0{cS1O{sF35n(d-m&Gx;(t&vExf7=*;l&Cs2 z(|MyEGJSz^xg^i;-1ehA*ni4g?*%Z52>iEA#Fq&oV%YwL6m0)CRV~eSQLSeC9Ogty zoW(C2>mL)qXDc9EOY2-;&7@K2#Yjj4WTk()*8)JB}T zs`DEE27E0dW^-Z^4&j)MW?v*`b2BtS#ccLMMD$6Gh>48W+zbB}!H-+;pDeSPSd0lB zsrNyj@H>{gs6Vs~*p7#3(Lmez06U?8OUdIrh@k~b?BRl&55FogdsKEp^Ix<)Q z2XdC`Y%SP-6O_|Z6U^bQ1PcKKHz>fKu-43t3}C9zsLkBy9|i6C6)5E&jsL=~@sB}7 z=L*WOqdpHuLHBH~%p!HzAxXLLB6s4g04gNL$A5B(F?SjSCte6U%wX5sSrCT6!~0Lg z(@S>Y36m8#xdnI9o?V!1HYfawj7EgmZ9CD@=Qgbye8z2b=vGigSFwvPzHcwi?aC z4|5iNumcwQ4`3mF7)czxT^>gA@h;H9yOs>JtiwpoxiEiHUIg0)`XIOgKQC0LpJMzC zLTYdz_4GjBbaN2)hT><@SNN5Ggs0f102}MGtWtFkQ+b~QCPbnuhA9)vYw~Cl(JOde zl0m*Ti1&>-+C(}YvELP9{$toPIsfZjbkIT@!{|2R{)|umcshZRs2#*^;vXPgtNnYq z2?h3Nb@~V2<+~1ov_&}Y3I{!i6Wj0$D%5KfJ6=&*pn5AI}Jj8 z-9H{lB?v(qAn0GL%k&1~-AJXHbN;gq{Ur93{egj6dwpTdfCFJ?BJQ&tkLB2!-<3au zA2wEKGzvWWW3$?S5)8C(Kq9BgZ%E7M!M1!>UzbG?)9j6Y1FmAp)Lb1%z#cmw9amp8 zja)tQjQpv{O_b#p?BVfXV|+g(h>W$$`K@C4!8)wQU$Up>aX9dHNdlG;S$;)Z9;>eh zCZMS~o&*W}$68bK|H1^y()0h|?M)`uu(*nX&19PO0vr{TFQOOEopy`+!JgZd8-f2+ zgfm<7h6E%d?0;Qb_N%YCt!G}Eu9Kl{`G8dWZk=a6Q1l?G^QPV+Z&v%z{oz zLmy)6sr&G~2^@&@VS7V!KWh1$|62z`BX$&{Ly$C!5~s?1kK3eMp$$3p4x|gO<#A+flGkE+f(*nti^=2@4(J$4TLAjTVt za;otbPXx+@*s?DtL^4ufdx`tQAxz30=enDn=A=*2+%FD7o6#U12Kr_Tc5K=#Cc$t# zc1aQd6B27edm@++n515c#|!m0YXgJqA6~U8{?!~E$X}Mvzlcpg;9%bZ9&tM{&3y(b zncY>Qq?c)}?Fm9iPg?O527^RvVBO6ptf+7H7#_kT%i|$jP%*@_c2-Jw)+dB$tmqa$y{`|B@GQV_y>ReS zesr=1$4J#{T0-9Se8$p{(&smxAO@pdj1)XX$PgoIq-@yHziaTS0T?X14qtm!{)l|D zws9>4aQin59kaH7*Sf0)4qtm-VSJ=i;J?(NwCM}lBVw|n5rJo{7^GMu;s`El99tl$ zR4I)0_ro9L@`L?iVZ3bZX!@xG!I zI$X@wLgzmUP4Fbp>=v_EdxR{|+Yt7rKn)7@vSj%wbIr2+gYfbsE@<)~-Aq6)W(P~` zF+XFhhk_>!3zj*5RT4hog>B8s$insmz}hwo+kDxS-9mT_wkpnh;T^)(H$RMh6SuZ) zJNO5kCHytqg;1=IPfS8o3Qhp7>||$is+o8qoYHCjIe-*!TsEbeWXMF|^k7t^0p<75|g!%2owT2fiUSTclL4OzVJDoD*gygD*E);v6FE$7RE za_*&HDA{86@+xP+lBx^s!T4)M2x5r`_y*9cnO=-Saj6p z%I!d3WT?>A;T8(i7*&#OXh&~ku$|h`4^S=9NWGT=bw(X}ym?|QOWf6O8}nZgO{7lGd`C}9*xqugiEt538An{qj?!| zX(oWTZZ6GwY*n~4yhoTf(8SJhb7>AVT^dOs3yv{0E~PaIObM}n2A~9@avJ_g>(Dfq zP%#ot5#=LV+UR7%p^+4WL!%_qmgzW$<`QU#a%e8Y_i+vlN3B(-*5Z~;>5OlW2TjWYxASb%o?#MU9)b~a}KO&*Dow=3*HH2Am1^tLOIKrY%)*SR- zkIZBgW;e_H0EG&zlix>y8Wd)#llLa%n3`(dmxNSUFKd2A^s*0ix#{J9!d8V|=Dos{ zS`}W$>SdQRZDa+Y=2U09skv`M)0CQf0^i4|IiXEDJ=A7(`#|P$L%DItShW_sx;MH( zHk&kC+fN9aAV`JuqH1%ltWxBli=Vct{H+iJxr!Knx|~!Bs49P@O6?YQ=owkIAe1!^c1$sJUg^BHWXQ?GuiC0dUkN`X2fm1gSo5~?K{sS_#Cwn*7pgPI(manUm8 z&x0niHZE6CXw$~!G78k7FjM2Q*c$JYS$S0wI$^V-ITy1^6fJi8_ZIE z6gws^Fc#X!deqro4VLzbDoM*+^h0XlBkaI|yYgC%3S7AiX|va~ z9RH*>laDajW7KXH<-sqo@_s(W^3-JR8QfYAX>ZK}B=2HQoTdz~Hk6wTixu2|2^S9n zt-Ijr$-bsn9UU5HpV>c8W&dPV6{^j8esTiNg!JIvdtehL$r)*M{oc^nP~X-;+;RY0 z7+Ir?l}2J*|F+85cwr#ae1C<%R($?{fD+y71>Wn!UGzHM>#ln}UT+!BksK1>94WPB z7mRa`b^@Z5b95EHk8_T=cy{9)t&#gE9TKDjXuONmqXa3{ZjzU@Mru!fqo_`h(tAVu z%SMuvs#qjR$=^zfn9Fp#vj|DI09~eARH+mrUUEh)hf2B z%$DK*Lp4U@^Qi}UF8fbj2uCbIVEmQ@HDr9HaE6sSRArXHP4;F6=2aO(P! z@CiG0nw1f!?s{OYn^QM{tqP}(cL;NHZo*v_YG?&g3iL5Q`an^hrG(=hRi}8J& zHRp(eHMiR^2b6)FJVgeOyO(W@j}(kUuAq<;$ENV4GmpXzj+b4{g{*cddt!vhlpu{t&@ZzCjOM`<07K;w6FhOPk8S zQ26qp=C*{Tsr2u|3Yd4S-N0EQf!0`z7jLxBAYouJKD_t*<4|QpP0}92BNV8iQDxVl z&-W2WSX?h53CwOW{y!;HXpiCR6sSRAXCA{+b$*#pUFrz4_d$Dvqwv)vG{TO8=3vB8 z2%s*?QJ5>$=(FXyKFjn*WTd0O1&!I#Tj6!Aqu{dqE-L^7rsUVQd~pVBi7&0|d>MBU ziPJ0aOW3|@8`iP@%oiJQ#FjUVml4LO&lN(W8IYZ7&q1ROcI6A)`wVA#@GD+ojufw| zoExP|#b$2x+g3XLz^e4B^-xtRD5PExaZ;@Y?U8$#7g`_rtlfHNlX#p$HjQ4@B>Kl< z1tGn95K_^mLn0IIK3#wt1Fe4}pWvnv1KOtMd_cxgBX6NV4Si*5yHz($DB6$Q3}+MudJ>03D6#4tshE4BW$fT2P4+{ zAgIgDS|c*jT66Wqto6b0x+`lfD*$V)w4yB-an|~$&@`V5hpqL`@kK;b#2D;-gu%9fJGI&S+12#pPrRc|X0@fZ&+~V5VxMQLQbDOV_SvR0VxpIXzjG@c zer=n!eHGSe47H}!UxgKf40Y(rGCdCpeb7cO^dbI|vvts%qbVMsKn-PNs@oSk!lJq* ziO){mo=>4dPma!^Kn)7BR=4{@dxW|@I|+@jy44(vsM}{jF}bN*L`JGxE`&_o&Vtun zsashAs9Po5ZtC{+&@`=Xuf;DtP`3xNArDLV<1w0c83I)5Lkg}_CCfro!>lE&t1JC_ zm*^rH&PootU}))Ws#GxCL;Z>*MpWwpgMVDB8gFcc`j-mLGzwJ{=YIez*nJB1AE7x% zh5C0CsG)95h59i^SX8Ja@!2WV?^3AH3iVqQs6k=Y3iX4~9-&bGGYO5bLe(6MDAcz= zU2X~$k&z0O3n5deGw`}Ag(@omg{oxRO`)!=#wpbO@Jr7WY734cc2TGZ*PTLTEp@F> zhkzUD14?UEsh%oS&LpPLbfBKAPyWeNsO*icP)|~5rctPxIR8YfVD~B1ZP1*fLcM?j zHPnr%P|tOQMTJTdpPfQ&QmD`hb%FvlD9l=+o&xO=3UxdQjj%%19E>Q`7eifc3Kfx& z3Y7~XQ>cf->#h{4tN;|Ml5IDIdOtKxd#DfKmmVn8{j2ptDMptrM8yZrDS~vWM%T0Y zr3U43dT}ji3&Ifm@=pld!?mCnH6}Zc34BU)S~{Qg?^US|viF^0_MCSi?K+#k<%*U6 zuyyK}6<%p{s;0*OUsyp%rw%yxLL^Whn*MhX7kCN~a@4v{P@slRGPUlv8LSbaUx4dS zOuz&Y8rQ@?{hz3Y>CpG@c)L!a??vMT>2&B@RI5YZv$xi3;KB$=VLxn1Ie@4N#f zYpbZZz;}e>eR>>6S&rWn`s{~|{!s+_F*nPtXP|BesGLewOS4>5t6A=_+He6;eQKuj zhC5{Xe5o)aEZ+;hNRG3A+YB!gsK+q8l!DTKaIGgr?7* zg%yO{xsXqmQwvqxhl#c1FEpvoLwMr77omxGfLWQ0pA47o}6lSefXG430 zTHTn0Mp&(C4o1}Kk6`4usZ~Tqs#Pw8Os)P{3_bEB0GA9mSple3CEIRlwF(%~YIOp? z^h~W5E2AwZwaNfHRjbD`wZhwyYL(B9*^Rr_{JAmS8lUKH)_fhXEWP4=gDTZi#VZ&Y znmcnXE54bEm%p+V@0|+MH1lXppFe{YgcR>u_l^&nH{VZz8k)=0&G#`_ zb0Up141LXWe_2nY6Q&rCQq9uS=uh%iou<)Wp{k{)7@}G|#c-I@eTS$%^(2EgY%8+3 ziFB0XU4$-vLtq~>t@~vPp3kil!bbC4RI7QO7`j+VRGymSyx9&p-ltV)WF9?-eUsBt z@ypgO>j(m3m_EQj-GUdJscLDai)sy~C(Wg|5S6E9I&U@&)01J*=_3_yc21v9&>l0F z9-}}F)tR-^_cj=mgwt0_LL=<-X%0r5zN?`LH>VGgkxm~sfXwOpGkD$ATv}EDP9Lw< zd0*bUfe*CPcQ1bFnbTKub@~`!r%vAq%<1E8;i3Zi7nY}|OmS8_96knIOYiqRq)PSF z?-RTv{XX%{^!xZLTQh%FVVcJ8)AaeD!3sit-?~(N6LtM$ac)zzJ2l__0J)Xwg-zAB z04Yb;?=cG0&|jwO_caD zkvE%c=WwfEl;diINBockXL~JydyEfq7z1_lL5`=YrTHzY)%A z(5!vH&w)N9e89IRp%L~0H3uU;;9o%#ZayF)BYi+_44Du3AiVC%2b2|n52$RcEf{g@ zDPIOY&_3W-@k9_p(qq`h4UrxLP@Q^z%VjGvtkm%`%8!5#>AkwARH+V* zV?s|Q>D&pHKg1Xma_Qy?%HdDe8315YFffiDr zhKe!Ww0VxOs6#xX!yHA%3Pq`zX;B)~4 z+9v8=z{zpi{cZ}>&~~Oieg%c?^wviF-zaQo#J@y=wnf}lk<_{j+D=j$v$LH)r_iQt=Q9+jL1EUm^Hyk&u$@0n zLL+QDH3uWMvjugz*-k`8+D@(&nC;vNue-9HvI4N3O4-qP8!zFc8U|t0 z;{iI3maag78oJ4}beAz$!_tK&Uq&cg&Vf1V@y85YO*K#3iD?Sd71KrLU=Uo^=*|;g zCAR9f&DJ|93}|F;=d5yyiQh+6OGgPswK__exH$hHQF-bZA#c{!u9Vrun0Q1;y4lI` zc>?>Gxa?d!jD~2_ZbuW0TMbd!m|y z5l{3uXu{1CMP#HW$_+O2L{EU%U3sFi0`NqYA7BecoF_T~e4wK^E&S30PxK&kg`Bn) zXuwXr(gSoA?1)G0n|ADW-UMt(?{;3NO7$w=K~oxu#PpfpM81RZ%Jwkcp)gJ3h-&)$ zw_^n%M|6qphzjpV=dv2^q;`pnm$Z-hSJ0B9kNE%vYG@tP$Go4xX6j>pmTI1M+a9Gr z+hS@Dbjm403qbZzb{63K6xwtw;zkSb)cq&fj%W&ZZ2b5N+jsd-P)Qd zAnOLpIeA8UPnD@?m#MO?^59=MFdZ~#e|ejH5uB}&4A~eJ|A=JZVQMYgi&1D*3r!Xt z>DVA03%!)yAZ=8ob{h}%kucmK3I3V}iNCT9(wPd=GzLl2=bwQUgbdPV+aRUtsf?`G zrGS>B^}3h>H8iGay|yvfOs!XwYM!=UbqdrKQx)@IhZJZ>z<$3fP`_>i}5rKh%{T0ON*jNg2cs66$=mN#o#B+6C6_)S!VF{e>& zM@X@M=syV5V};H)v_P982^%}JT<#{vmLT~F|b=3rbws~ z4~$@+Wvo)auqjzQ>8#-vFi0kH|je5FKQ?4Zl!R41p)!`P1ojOgzxUI2*I@X-zXk z7wq%(bBX(%B}%%SJ3uh*Q>A)3moOO>o=b>t=3Iim zQiF!wboGeBG|gN>)8{{o6@=yz`K0d1Xu|LXXge}gY2Wc*C{SZm%icq8>(dN2(+R_q zRP!{Z-=#oZG3^tJrXcv-7twQu$R|k4GN5g-_ydImjqh(LP*;47W8*ZY#!LFrmz6u6 z`+XS&+7>%ofm0qiIv(T*$j-UF|9kK<7QvOye%@lRUE;~vD4J07nVDt!5vK8C>bZq-?bM91WJ<#?Cs#LGG zd?+?Cv#SmNJoSNFef8Vc?e{1I)0lismj7<7AY}4;v!3-Zv=kWAGk0yRdD>>@Pq zA7ZeX8u%|#&C{5Ejsk6qsXesgOcL2c*_ouDP-xR8=|>c(L1ES=>Cd4(!X$k^35~Ex z(j1JKr0bzBH40Spx z`AaEKL$Sz?K_$Q45f;69lEUm%=buri(5myb6sSRA)~fS7Xpc~x(@AKARj1})M0IY2 zy4+MJA|q8Nmo%n2kA~M>sZLn|s7@uoZmRQdplRA&{wRLwf$Ge)8q=_qFGg!F7Q6U3 zP^SuWfoP-i3(~(Rx+L9=0H0H(f>-xu8=mA#$TOZR-*XKqf7*KJn+m%$%2Ctge*-HB zDaVKKy6TW}OmiRl&!A36<@gf{)KDF!a{Q4aEGkEm!t9je@?8Xlv~pZRff^KMtsK7% zTq2a?!Xz}p%29JLq8vXBO}HsXL`Et{E@@0Tegax=Mzj5N7iMKP1R9bR{3a%BZza+T(@1tTt$^Wy0^lRJQ4;!Upq z@bW$OkQur>Wmg5Af`#Mt(qz?JH`%IGXCkZ#=2V&hFmnU2!hZ;=DX$K;rFP%3?)AfP zN=cW)R0Yt+qjKdq4Ko^Q05;b!} zu%uFhoTXu}R;-r*Y9?4P*&1DU{JLfZ{=M4%IP5hV^~P|yP%Blv2JjhCEw*NYr9AV= zsAUeTWhPh?`Vy+buY-ICOL)B+hdHmf=&W_eL;LoJP~l)1($EA(m4QW7plK#pTrJeb zCJSR8aJ&S+_w;Hr!QKt8SZ|buOWp*?2Q*V*oSs{(*G7R%;7A1yb8C2`m0jR<2TKZ* zt@^Nj73^67p;}y;9G@6gf(MLcFWA9FwL%rUZYg^M+^sdS`}M1drZ-ut5APaBky_^M zny5F$kw>B=lagX3SrrM1fS+EBeQw!Y-; zTn~`V^)tafSOROr62ox4HgLHGT%8Z^*S&bd(ZH4^jnQJL8GNIgSuLAs)$1@yD_(1q zHB)YlSD}dot-=Tlfl{kHbME|L4G7dYoVp~&mO_8HIf31KW3Y;0Q>it*22u_ZhdcuH z9xQAWrqoXuc*~m8wN_!*aJe#8uEPI8OyNF7l<-B(@dAveD<@(2HlgI+>^J{8T&))i zf`e;{6j+so^n=a6_<{SKB!Q3)sPYt_fi1 zffRw-t$KE8yHui|6le(*RBA1Ch*k2CO*KL8$w;p2Pwcp4vDa`4!Sk5{aQ z$EEly%o%{kLVP@dk00RUGi%}TDSUh%A3wpz?d#$3R(!nUD0sXZANL#$ zkH5gj#4+&LiI4oT@Hh+~gU7*RBR;-~kH_)xyG`(T79Srt5gs4L$Ln4Kk7<0|gOB&( zqjd&6uENLZXTxI)K5lv`JZ{5>e?B}W@X@#c9=q^y&CB6&9X@`1Av}JLj~~7g9?#(8 zluO{T2_IMBV-z17Uj>ij@KL)A9!-2Ky&N9<;NvNLJdKa*hv9JxKKv`-;o;+EeB6eQ z2TJgG5FZmBJa*#4AB9I9AJ5?9SNM2n3?3iH$Mt1++=7oC6?lx|Bf!V)_&CUi$D#Om z79X?W_IDrekoWP9c^@x7X4gymcy%7ToY2SHp4fd_K3)O9E?MyLAXs+Pu8*e~vhyN+ zJe!G~e&yrdcy{`Lk9#E9E>s^gtywO+kEu^Axy;8EXO>Lj;{pm>qVh3X$l|d+PGZg&+G&`n1dC0z&ot5EVg482s) z3#=L!$W~dQ6;}P=Wi?vp*6kCnOMKZQ=mW>0Sf2z}6g(t2h^<-$B@Hxy^EmHx@%|1_ zq1Vn_h(@~50DBA?xd}cCZ&3LU=Ij3Q0&pd#n_z?GYwh07;Zq_jCtb{vRt4){3Oc)i z?rWY~iu@sB!ZbE7 z!EAP0d@y^<*0U}^Kax)X{7-@M1S^eB{<*Vo!uA-e(*IBRV^9BRT91vTwFdqeh|*5a zf#lyN(1D1t?E9Sm>jcmU$^I{(geLe4pdY&-_>^RVhe5;&AcR#1%J;s?xwDrgfF@D? zrk8X_d3;yHG^bW-PBumz2ws;!h0)b(6F`#)KJ$|92;LCe)s4}D1F2^x;BSz6MgnLO zsS{w`x{>OmVoBu-$PUyN5=bzpy*vRliP|f{_I5+O(@ bP1K1M^ diff --git a/doc/build/doctrees/pages/acknowledgements.doctree b/doc/build/doctrees/pages/acknowledgements.doctree index 5d2a92246032fba9e59b4256e6c85c9760860c25..f8d716ac9420c11bac4658fbae4584e36b88598c 100644 GIT binary patch delta 67 zcmZ3ayiS>=fpzNQjVwNl>f!pK#i>Qb`o)Qv>8VAfB^jwjY57Gd`bDV)`NjGrsksI5 VAm-+5Mn7)m(vq~zdw8C)002Hd85aNm delta 55 zcmZ3dyhxd)fo1CSjVwNl(%$+R`MIh31x2aJIhlE>rTU3^DY=P7`o(GGo68yfxS2yk L+%})!dBy?&7u*wx diff --git a/doc/build/doctrees/pages/citing.doctree b/doc/build/doctrees/pages/citing.doctree index ba773abfa2d106e8acb14ef272f10eb2d66661e1..e4bbb92c6a092fc22ede5694d38d76c633aabd16 100644 GIT binary patch delta 69 zcmcc6#CWHPk)?rkYT!ne2u5{Z{m|mnqGJ8x#LV>6qSBI#)S|Tfq7?n2)PnqC{gTw& Xf_M;fb2;M^tI4u9?3i+tn#i>Qb`o)Qv>8VAfB^jwjY57Gd`bDV)`NjGrsksI5 VAm-*M#wAS5r6p;b8CjmO003gF86p4x delta 55 zcmew?_*{^sfo1BQjV#uT($4xB`MIh31x2aJIhlE>rTU3^DY=P7`o(GGo6{JVFfoUQ LxNYWTdBy?&HF6V_ diff --git a/doc/build/doctrees/pages/installation.doctree b/doc/build/doctrees/pages/installation.doctree index d0afce1eb50c11a5625ceba89d01dc99988f7068..449bd82acea0268ae69ea3289e9cb4f3196acd6e 100644 GIT binary patch delta 81 zcmexa&|1jSz&f>RBZ~{8da!T7FTAeo<;cezAT@YHmS1 jh`BkLF-DcQv?NW}VoHxpYEe;s(d2$LiOo;crV0W8@S-06 delta 55 zcmZoI{8_-#z%n&&BZ~{8w7Y&rer~FMK~ZXQPG(+eseWQ!N^WA2esNm)=6uE&Rp!tT Lx6S9&whICPIOr5R diff --git a/doc/build/doctrees/pages/modindex.doctree b/doc/build/doctrees/pages/modindex.doctree new file mode 100644 index 0000000000000000000000000000000000000000..0a0969364ef9d62ad6e1665e9e646a6297735466 GIT binary patch literal 308570 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

;$`9Ny_c5g^m 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 literal 0 HcmV?d00001 diff --git a/doc/build/doctrees/pages/pcntoolkit_background.doctree b/doc/build/doctrees/pages/pcntoolkit_background.doctree index 885f22767d232e32ffcadd2e6e8755e5e8d93e4d..6430d29abded6f91b1e5c18caa2d2c14fc8b741c 100644 GIT binary patch delta 2522 zcmZ9Odr(wW9LM+EeVt3|_&^B-L|6&k1r`)iC={o$W^ClS2!cFy;j+AzT^7?uC20_O zY5h>l0-|7a3N72plCdz$F(<|xEX~QZ8OIt;5p!(DvFtq9ySw~zIFHZw`}>}AE_d(1 zRiXQ;plVd_Yd@ySL6I9w*$#`{VRDpIlw0i1ddXrhtF>=3*)6tOhpFCDZ7ZT(R|6WJ zrkQOkDYrOWiPKjrHFJ2sc~;nRxMxffX7ZdFAPrHHeO-|f^C+TnM9?*fdpi2s9kAn1!DEK~Oy6}*y77O?3pD;*C6v33a z1?$ubg|pPU6W*{HU~i^w_FSiAu)Zd+dQ%r6HcN+ekep>j%i(ZV7)pW*W&?VbwBgKg zfmwx?l93*Yle8!n(#`287y8X6l*h<*K4g{;sfVrEdh`nPW=Ei{jBfCu9bWW|d;^LG zRKE;{*6DFS*#kM9J~ysjPe$*^_**1~9v<}4OUD0T0S4DcV~vI-Rl!7okwFN;3Jr(@xp~<*gabh`5I6~V3flAa zIGJP5QB0jrcDtNnLV^Y|$V~)oFk&+&GG!vooA_bFQe4T23YjQ#6EpJ{PoD0#1rfN7 zbGr#=Ld}e6;&o~Zjrcvj`Yv1T@m^g}6o1q$5UTac@PQ>lbaI75R=|(tD#0czP&Lbt zU1k*9Ioj%r?wX2f%Dct)Ioj`w_R6SG?VY1kGK#0U;*>`*BB_M0WCa>#Is7Xk#VZ`W z?2G<16&S$cVxdOt`mlLg%gUNqSAnd8p^KLl&T(c4H z;NUg}Zgqnmv;S%r<37&xz^XbEI>?xAj~VNR!!27H{+fd)8F<1CdYRQl;%l6_%671c zoWd2xUGf;RZaDmDZ^KVG{I3i@a>H&@GY%Emv4%iKV<^N1>C_ftI**M~3ZD|57UgZT zZa4%wci@#=lk#gVbd?mD~qZ9!-f zp=KZy5)4aFJ9DGIbG=Kn)0znFER_Os4Rct&1tr9*D@k;_ z%eMVGLW4w}BiKba731$Z**K^akMn|`iSY5-cy$9s!L~iXb>j7hN^}~Iwa9qLZV90= zXl@-tzp@lt+IEuh>P(G|k=2!YKFrEhhT0+gRquP9@$IyEjpVRzvkb2<+;538gaa zYIxK&PG)cIOGNRoaDPt}|1>bwR2SJw>?KuImZ}(AO?j6zysb+*Y28dr9W?caq)s`E z?@tus{a|FxT-OWTW`uY0nvKwRfGyN*pW_-oa6>C}`jIyz{gQ~+08tVRJi(pg{TYCYVk)Qplc)^jj_C|FZVPR&{c zDML{Sadb9@HviDc2ekQ)4!d8txns&y3lhh`U2lT@ok zsZg|7M_HRdK5Sa7J*=g^sm!y5`K3Cg1=Jx&(n)Djl=U20mkK11I;_*kBW2an<~Q1e b(xwXhkHpPmx$JVTyR=7hksTHuc@g~&+M+Kp delta 2310 zcmY+Fdr(wW9LG6_eVj|6f{uzHux^O#iYv-0h#E7EG-DHpfZ!tXT)C|5@?J62!6GAB zF#Cb{7zK>XCUnNOhbER5V>UKU7?$Rs*i<%CiRd^r+BlsDdw2ck%(>_Lx!> z*9h(#!Kx-z=YdnI92B<3Bv;z4rm7liNkzHCT5l?Hl-i4GOtqV~xL-qOgUIx%qB3i( zX@UF2$*cUy49`=0h%WItdC;XTwQee^uc&jMT>S`3->UrlP4=%F$z4y}j7StgXHP&u5+@}ZpB zdbAY|W=Eq&h1$ZYx(Q0>qzS9EXaQ8Ovyh`|zMme77}e|PIS+H}Adk$*F_JR^_)Gyl z_5pBiJo#P#R}|p#1mF$2kZU3L1$K{P78>*?$Nu0!z3bzMR>OCqhEo<3!f?l8Kul~P z5kd&(Lg<9xHLqTm2$gwr$V!1NrW|6gdqmjHTh+c&_;!A`V#FNhjaatih z@etmukiuy4v*51tJ>}DLUE|zUk#(-nNX7**rVxMm5{{y`2+{IxHPBmTR5K5M7%DSh z>fy~AC=Mg@1UHv+8!4yfT$Gk~m|qe{UJ%3zg;?fGOeq~A>jaUd5E&lAn{~_@O`L+O zqnrs84&)H z%VWq*q2A!?kG|?a`TOL7Q15f~Z;$HnFP1_G?{f-)p=1NBv8!+p0kk|81;B+GJ>1wF zBgG0N#)~}fi|nfCl2Qb+(u*W>1P2hGd-=s4Clv~EBPTV*^kVXaSuV{Bh3+anVlzEl zb(Ba=0^Z?;8-;DIukGW?SgA*lM+8|yZ4U|C{faG{>EZXPbm_c+zwp9mh3)=lY^!6W zQ9<4kWGS`1DQvGRwrqA%@{@RtPrcw1VR_xxGNEp^6eY+Ai50G~Qp+%j7d{il>qG%; zc2r6c;7Tu;%t1U;n&?#LG|+FZ&KU`ftEbTm8_$f?%7eCx^g|Ba&R?Yl;oy{@H*6uQ zb)VaE2;*i6iuxj9g<%nHqeg~U)a?H0)l7dTjCLkK;SLo=tvf~$KF0J2pnnJWuO~un zr+ck+A89({FP#?7pVAFQC8(WTIM5uZ!h_Gg(!iCcE!z+phQ{4L;1QwsgleI_wUI84 zw_XgsN8RWjMQ)W3zKrM_!*I#92M^POFARtJHXWvCqZ=i|j4PI>!;`jHc+h6{AgkKr z@idM^cn}v_(D6pc&W`OJ^JF!9V7B~#nq6#9w34|~A#l%aX;}cjxx@folGz>oc9IIG z2Jq}{9WUW@Zj4f4tYeIhw>y(mX8H=;3tkNA`^uDmE)49O8QegXT6UBVX^70QQz2$- zGrPmP97uXyMC}aVt?i|V+5O%A$6CCHWq697gUs&8-V}e-4TXJc@o|yg4Y&H@=+f;@ zv*siACf)Dcscy@AlK-rS)Ymo1T2oV5Q($$JRF;-I$~s)??5tGx_T%mTnk!VdU54QP za7`7h&}N6l{qt8ZVyh-L{L2;}vEd?n{F)B+F3C}3x7KZ~vexnvEot)FG#hhDllQgQ zrXWAR7TX;6qSBI#)S|Tfq7?n2)PnqC{gTw& Wf_M;fvkBu1E9TOYw9WE1Y-|8c3K(tx delta 57 zcmdnk!nm}Bk)?rUs?RPH;2Q!zkAMo%N?QC5y*Y9 z8mW^RIB_LDWviAQw=+3SwOmJ6ty$Aj;+9%8?YN`ni)m(Q3D)!Us`(hC^Q5awj~YpW z$3{kRYpRFF=C*oa62H92((HT+q#FTYFd_jO{q6!!I{Me%rS06UM222}&*Amd8 ziN#j{KmBPAT0H3T%L$Mqkslux`Dui1F7<x4$(6~Q0lKAXx>Vo3m zW@MN=Wm0DHfk}f2FmRW-Gt8ax#Vt#POkQO`K`kS`JN)X%@6

F}G+3jeC0SHK(S;#Jl+saX6cwaQl1XRAWYSJEh!#;4 zT+}G7cQo44rHg_><4ao#0TG3?Xt8d@ogfGWrMPpc@6AB#=Df!{-#PcrR5rMo4NkZt zJZY~AX$bYj^*OIc3Tz8E;;}B8ppk0FFp9%mB1x`Tm<>8kG|PL>-heu3#)j!Qi89vwX@yP51C(lZj~NR zTEox#!nl@fZt5=p-Jy=1BRk2dXX60BXd;X8DG}=A=+qUEw~5~O$l~V45?PrJg1p-| zb}^%;xqplmNg_sQ(}|g`7Kq~DtN|3B4f7o)h0EJD+b-IJnw2SL4Qt3Pix*O-$={dv zd8wEBW&$6~rIIb4}B_<0@cm@WToMdvGic^*?wV;)53~QtAE%%_=i+iib zqO3>pIutLjcv;2EDUKdtWCCwjFUGuK0&Dk%8JojgcJqeS*^Lb|CBnb{0Pqn%kptX{ zh;v;L4Ax>?L&WYgL0Cq$_6pWL_lNIZnr{`p(Ul+3*JDLt&EC26`AQ+i~; z5|a~FNNnD^B88c43s7~|#>w()(%9C3m@6lDuaRTiGI`Y+M`4hFUsfHE>?qApsGXt# zRQPXAD&xw{DQjOaGFfv>7M!TUES+UBxpci3qvzxU>rFT;I2af*WHPiTJIc#Ume^p( KXg1kvLm&X3mQYpz delta 228 zcmbRGkZJZqCYA=4sS`G`u2QWrs@|Ir6%WO=B1YEC+4N(CKl-zr&bV%| zU=M3zPG)-Elpgk!)U?FXoRZBhOZAzhZnHBm^zfw?73CMjr{*Q+r)1`(Pw8O|4RPDt zyP}PmZ5vQNYx88uHEF8rK+M%3q7Fn<1Buce&b-9j)RM}A)Z!_%Q#7)+O5lEy!=KXWU-T$hd=%k!!mJGvi~8 z=?A13S+*PMFv>AA&YT{g&nU)RT9URsQ=hSonXQDIfg!7K`d1^yAhsM3J9E0PF{2z~ n$@Cm!MpI#skYAP^knAYUP^g`v0aUxim@$wsb32;}V<;m462K^} delta 195 zcmccmiskkz7M2EEs^HNLo6Z2AX6N~hV)5@E>8Mk*c zGVWkxl-cgW%=lPCu!l7s?RE({@#c&h%FDq&YtdW%qYiL mHa*Rl(Nq{DBqfeSEPAWE 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 VAm(Nb#rTU3^DY=P7`o(GGn@t$kGV_Lp ZxanF<>5)k-D#|aKT*xB1nVt0x8vs_77ybYM diff --git a/doc/build/html/.buildinfo b/doc/build/html/.buildinfo index c59d51bf..b2dac09c 100644 --- a/doc/build/html/.buildinfo +++ b/doc/build/html/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 5392fc74b0cbc4bdad768b650de8eaca +config: a089bcd75031977ac6ed557ec8eaf85b tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/doc/build/html/_modules/bayesreg.html b/doc/build/html/_modules/bayesreg.html index 1507a312..4894cac7 100644 --- a/doc/build/html/_modules/bayesreg.html +++ b/doc/build/html/_modules/bayesreg.html @@ -13,6 +13,8 @@ + + @@ -34,10 +36,10 @@ + - @@ -111,6 +113,8 @@

  • Bayesian Linear Regression
  • Hierarchical Bayesian Regression
  • +
  • Estimating lifespan normative models
  • +
  • Using lifespan models to make predictions on new data
  • Other Useful Stuff

    Other Useful Stuff

  • Gaussian Process Regression