From a4b0764d1e88f7be90b047c3a05a9c24bf51b0f4 Mon Sep 17 00:00:00 2001 From: allucas Date: Tue, 5 Dec 2023 13:48:12 -0500 Subject: [PATCH] Add files via upload --- python/docker/Dockerfile | 185 ++++++++++++++++++++ python/docker/brain_shift.py | 219 ++++++++++++++++++++++++ python/docker/ieeg_recon_main_docker.py | 202 ++++++++++++++++++++++ 3 files changed, 606 insertions(+) create mode 100644 python/docker/Dockerfile create mode 100644 python/docker/brain_shift.py create mode 100644 python/docker/ieeg_recon_main_docker.py diff --git a/python/docker/Dockerfile b/python/docker/Dockerfile new file mode 100644 index 0000000..eaa63f9 --- /dev/null +++ b/python/docker/Dockerfile @@ -0,0 +1,185 @@ +# Generated by Neurodocker and Reproenv. + +FROM pyushkevich/itksnap:v3.8.2 +ARG DEBIAN_FRONTEND=noninteractive +RUN apt-get update -qq \ + && apt-get install -y -q --no-install-recommends \ + emacs-nox \ + g++ \ + gcc \ + graphviz \ + less \ + nano \ + ncdu \ + netbase \ + tig \ + tree \ + vim \ + make \ + cmake \ + && rm -rf /var/lib/apt/lists/* + +# install ANTs + +RUN curl -o installANTs.sh https://raw.githubusercontent.com/cookpa/antsInstallExample/master/installANTs.sh + +RUN bash installANTs.sh + +# RUN export PATH=/app/install/bin:$PATH + +ENV PATH /app/install/bin:$PATH + +# Update other required stuff + +RUN export TMPDIR="$(mktemp -d)" \ + && apt-get update -qq \ + && apt-get install -y -q --no-install-recommends \ + bc \ + ca-certificates \ + curl \ + libncurses5 \ + libxext6 \ + libxmu6 \ + libxpm-dev \ + libxt6 \ + unzip \ + && rm -rf /var/lib/apt/lists/* + + + +RUN apt-get update -qq \ + && apt-get install -y -q --no-install-recommends \ + bc \ + ca-certificates \ + curl \ + dc \ + file \ + libfontconfig1 \ + libfreetype6 \ + libgl1-mesa-dev \ + libgl1-mesa-dri \ + libglu1-mesa-dev \ + libgomp1 \ + libice6 \ + libopenblas-base \ + libxcursor1 \ + libxft2 \ + libxinerama1 \ + libxrandr2 \ + libxrender1 \ + libxt6 \ + nano \ + sudo \ + wget \ + && rm -rf /var/lib/apt/lists/* + + +# Set up anaconda +ENV CONDA_DIR="/opt/miniconda-4.7.12" \ + PATH="/opt/miniconda-4.7.12/bin:$PATH" +RUN apt-get update -qq \ + && apt-get install -y -q --no-install-recommends \ + bzip2 \ + ca-certificates \ + curl \ + && rm -rf /var/lib/apt/lists/* \ + # Install dependencies. + && export PATH="/opt/miniconda-4.7.12/bin:$PATH" \ + && echo "Downloading Miniconda installer ..." \ + && conda_installer="/tmp/miniconda.sh" \ + && curl -fsSL -o "$conda_installer" https://repo.continuum.io/miniconda/Miniconda3-4.7.12-Linux-x86_64.sh \ + && bash "$conda_installer" -b -p /opt/miniconda-4.7.12 \ + && rm -f "$conda_installer" \ + # Prefer packages in conda-forge + && conda config --system --prepend channels conda-forge \ + # Packages in lower-priority channels not considered if a package with the same + # name exists in a higher priority channel. Can dramatically speed up installations. + # Conda recommends this as a default + # https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-channels.html + && conda config --set channel_priority strict \ + && conda config --system --set auto_update_conda false \ + && conda config --system --set show_channel_urls true \ + # Enable `conda activate` + && conda init bash + + + +# install FSL + + +# store the FSL public conda channel +ENV FSL_CONDA_CHANNEL="https://fsl.fmrib.ox.ac.uk/fsldownloads/fslconda/public" + +# install tini into base conda environment +RUN /opt/miniconda-4.7.12/bin/conda install -n base -c conda-forge tini +# as a demonstration, install ONLY FSL's bet (brain extraction) tool. This is an example of a minimal, yet usable container without the rest of FSL being installed +# to see all packages available use a browser to navigate to: https://fsl.fmrib.ox.ac.uk/fsldownloads/fslconda/public/ +# note the channel priority. The FSL conda channel must be first, then condaforge +RUN /opt/miniconda-4.7.12/bin/conda install -n base -c $FSL_CONDA_CHANNEL fsl-flirt fsl-bet2 -c conda-forge +# set FSLDIR so FSL tools can use it, in this minimal case, the FSLDIR will be the root conda directory +ENV FSLDIR="/opt/miniconda-4.7.12" +ENV FSLOUTPUTTYPE="NIFTI_GZ" + +# install the python requirements for iEEG-recon +RUN pip install --upgrade pip + +# Install any dependencies specified in requirements.txt +RUN pip install --no-cache-dir ipython\ + nibabel\ + numpy\ + pandas\ + plotly\ + nipype\ + niworkflows\ + antspyx\ + antspynet\ + pyqt5\ + pydeface + +# RUN git clone https://github.com/ANTsX/ANTsPy && cd ANTsPy && python3 setup.py install + + +RUN mkdir /code && chmod 777 /code && chmod a+rwx /code + +RUN mkdir /code/voxtool && chmod 777 /code/voxtool && chmod a+rwx /code/voxtool + +# Create placeholder files +RUN mkdir /atlas_files && chmod 777 /atlas_files && chmod a+rwx /atlas_files +RUN touch /atlas_files/labels.txt +RUN touch /atlas_files/indices.txt +RUN touch /atlas_files/atlas.nii.gz +RUN touch /atlas_files/lut.txt + + +RUN mkdir /freesurfer && chmod 777 /freesurfer && chmod a+rwx /freesurfer + +RUN mkdir /source_data && chmod 777 /source_data && chmod a+rwx /source_data +RUN mkdir /outputs && chmod 777 /outputs && chmod a+rwx /outputs + + +ENV PATH="${PATH}:/code" +#ENV PATH="/code:$PATH" + +RUN test "$(getent passwd neuro)" \ + || useradd --no-user-group --create-home --shell /bin/bash neuro + + +# Download the ieeg-recon repository and then copy the appropriate scripts/files to the appropriate folders +RUN mkdir /opt/ieeg_recon \ + && cd /opt/ieeg_recon \ + && git clone https://github.com/penn-cnt/ieeg-recon.git + +# Copy the main scripts from the repository folder to the code folder + +RUN cp /opt/ieeg_recon/ieeg_recon/python/docker/ieeg_recon_main_docker.py /code/ieeg_recon_v2.py \ + && cp /opt/ieeg_recon/ieeg_recon/python/pipeline/* /code \ + && cp /opt/ieeg_recon/ieeg_recon/python/reports/* /code \ + && cp /opt/ieeg_recon/ieeg_recon/python/source_data/* /code \ + && cp /opt/ieeg_recon/ieeg_recon/python/docker/brain_shift.py /code + # the brain shift code is different + +RUN chmod +x /code/ieeg_recon_v2.py + +ENTRYPOINT ["python","/code/ieeg_recon_v2.py"] + + diff --git a/python/docker/brain_shift.py b/python/docker/brain_shift.py new file mode 100644 index 0000000..da9f04a --- /dev/null +++ b/python/docker/brain_shift.py @@ -0,0 +1,219 @@ +import os + + +import numpy as np +import nibabel as nib +import ants + + +## Argument Parser +import argparse +parser = argparse.ArgumentParser() + +# import module 3 atlas finder + +#-db DATABSE -u USERNAME -p PASSWORD -size 20 +parser.add_argument("-s", "--subject", help="Subject ID") +parser.add_argument("-d", "--source_directory", help="Source Directory") +parser.add_argument("-rs","--reference_session") +parser.add_argument("-cs","--clinical_session") +parser.add_argument("-fs","--freesurfer_dir") +args = parser.parse_args() + + +print(args.subject) + +subject = args.subject + +source_dir = args.source_directory +reference_session = args.reference_session +mod2_folder = os.path.join(source_dir,subject,'derivatives','ieeg_recon', 'module2') +brainshift_folder = os.path.join(mod2_folder,'brainshift') +os.makedirs(brainshift_folder) + +clinical_module_dir = mod2_folder +mod3_folder = os.path.join(source_dir,subject,'derivatives','ieeg_recon', 'module3','brain_shift') +freesurfer_dir = args.freesurfer_dir + +# Load the MRI +if os.path.exists(os.path.join(clinical_module_dir,'MRI_RAS', subject+'_'+reference_session+'_acq-3D_space-T00mri_T1w.nii.gz')): + img_path = os.path.join(clinical_module_dir,'MRI_RAS', subject+'_'+reference_session+'_acq-3D_space-T00mri_T1w.nii.gz') +else: + img_path = os.path.join(clinical_module_dir, subject+'_'+reference_session+'_acq-3D_space-T00mri_T1w_ras.nii.gz') + +# Load the freesurfer data +lh_pial = nib.freesurfer.read_geometry(os.path.join(freesurfer_dir,'surf/lh.pial')) +rh_pial = nib.freesurfer.read_geometry(os.path.join(freesurfer_dir,'surf/rh.pial')) + +# Load the freesurfer mesh +vertices_lh, triangles_lh = lh_pial +vertices_rh, triangles_rh = rh_pial + +vertices = np.vstack([vertices_lh, vertices_rh]) +triangles = np.vstack([triangles_lh, triangles_rh+len(vertices_lh)]) + +# Load the freesurfer and iEEG-recon MRIs, we need them for their affines +volume_fs = nib.load(os.path.join(freesurfer_dir,'mri/T1.mgz')) +volume_recon = nib.load(img_path) + +# get transform from FreeSurfer MRI Surface RAS to iEEG-recon MRI Voxel Space +Torig = volume_fs.header.get_vox2ras_tkr() +affine_fs = volume_fs.affine +affine_target = volume_recon.affine +T = np.dot(np.linalg.inv(affine_target),np.dot(affine_fs,np.linalg.inv(Torig))) + +# transform the vertices to the iEEG-recon MRI voxel space +def apply_affine(affine, coords): + """Apply an affine transformation to 3D coordinates.""" + homogeneous_coords = np.hstack((coords, np.ones((coords.shape[0], 1)))) + transformed_coords = np.dot(homogeneous_coords, affine.T) + return transformed_coords[:, :3] + +transformed_vertices = apply_affine(T, vertices) + +# load the electrode coordinates in MRI space +electrode_coordinates = np.loadtxt(os.path.join(mod2_folder, subject+'_'+reference_session+'_space-T00mri_desc-vox_electrodes.txt')) + +# load the voxtool output to identify the electrode type +voxtool_out = np.loadtxt(os.path.join(source_dir,subject,args.clinical_session,'ieeg', args.subject+'_'+args.clinical_session+'_space-T01ct_desc-vox_electrodes.txt'), dtype=object) +electrode_type = voxtool_out[:,4] + +grid_idx = np.where(electrode_type!='D')[0] +depth_idx = np.where(electrode_type=='D')[0] + +##### Apply electrode snapping to the pial surface using constraints and optimization ###### + +from scipy.optimize import minimize + +def compute_alpha(e0): + N = len(e0) + alpha = np.zeros((N, N)) + distances = np.array([[np.linalg.norm(e0[i] - e0[j]) for j in range(N)] for i in range(N)]) + + # Find the 5 nearest neighbors for each electrode + nearest_neighbors = np.argsort(distances, axis=1)[:, 1:6] # Exclude the diagonal (distance to self) + + # Quantize the distances and determine the bin with the largest count + quantized_bins = (distances // 0.2).astype(int) + bins_counts = np.bincount(quantized_bins.flatten()) + fundamental_distance_bin = np.argmax(bins_counts) + fundamental_distance = fundamental_distance_bin * 0.2 + + threshold = 1.25 * fundamental_distance + + for i in range(N): + for j in nearest_neighbors[i]: + if distances[i][j] < threshold: + alpha[i][j] = 1 + + # Ensure each electrode has at least one connection + for i in range(N): + if np.sum(alpha[i]) == 0: + nearest_electrode = np.argmin(distances[i]) + alpha[i][nearest_electrode] = 1 + + for j in range(N): + if distances[i][j] < 1.25 * distances[i][nearest_electrode]: + alpha[i][j] = 1 + + return alpha + +e0 = electrode_coordinates[grid_idx] +N = len(e0) +Nv = len(transformed_vertices) +e_s_distance_array = np.array([[np.linalg.norm(e0[i] - transformed_vertices[j]) for j in range(Nv)] for i in range(N)]) + +# Original distances between electrodes in a symmetric matrix form +d0 = np.array([[np.linalg.norm(e0[i] - e0[j]) for j in range(N)] for i in range(N)]) + +alpha = compute_alpha(e0) # Random 0s and 1s for the alpha matrix + +s = transformed_vertices[np.argmin(e_s_distance_array,axis=1)] + +# define the objective function and run optimization + +def objective(e_flat): + e = e_flat.reshape(N, 3) + + # Computing the distances between the current electrode positions + d = np.array([[np.linalg.norm(e[i] - e[j]) for j in range(N)] for i in range(N)]) + + term1 = np.sum(np.square(e - e0)) + term2 = np.sum(alpha * ((d - d0)**2)) + + return term1 + term2 + +def constraint(e_flat): + e = e_flat.reshape(N, 3) + return np.sum(np.square(e - s)) + +# Constraints in the form required by `minimize` +cons = {'type':'eq', 'fun': constraint} + +# Initial guesses for e values +x0 = e0.flatten() + +# Solve the optimization problem +result = minimize(objective, x0, constraints=cons) + +optimized_e = result.x.reshape(N, 3) + + + +#### Rename the module 2 outputs to before brainshift + +os.rename(os.path.join(mod2_folder, subject+'_'+reference_session+'_space-T00mri_desc-vox_electrodes.txt'), + os.path.join(mod2_folder, subject+'_'+reference_session+'_space-T00mri_desc-vox_electrodes_before_brainshift.txt')) + +try: + os.rename(os.path.join(mod2_folder, subject+'_'+reference_session+'_acq-3D_space-T00mri_T1w_electrode_spheres.nii.gz'), + os.path.join(mod2_folder, subject+'_'+reference_session+'_acq-3D_space-T00mri_T1w_electrode_spheres_before_brainshift.nii.gz')) +except: + os.rename(os.path.join(mod2_folder, subject+'_'+reference_session+'_acq-3D_space-T00mri_T1w_ras_electrode_spheres.nii.gz'), + os.path.join(mod2_folder, subject+'_'+reference_session+'_acq-3D_space-T00mri_T1w_electrode_spheres_before_brainshift.nii.gz')) + +#### Save the new module 2 outputs after brainshift + +# save the new voxel coordinates + +new_coords = np.zeros((len(grid_idx)+len(depth_idx),3)) + +# reassign the depths +new_coords[depth_idx] = electrode_coordinates[depth_idx] +new_coords[grid_idx] = optimized_e + +np.savetxt(os.path.join(mod2_folder, subject+'_'+reference_session+'_space-T00mri_desc-vox_electrodes.txt'), new_coords) + +# save the new electrode spheres +v = volume_recon.get_fdata() +new_spheres = np.zeros(v.shape, dtype=np.float64) + +def generate_sphere(A, x0,y0,z0, radius, value): + ''' + A: array where the sphere will be drawn + radius : radius of circle inside A which will be filled with ones. + x0,y0,z0: coordinates for the center of the sphere within A + value: value to fill the sphere with + ''' + + ''' AA : copy of A (you don't want the original copy of A to be overwritten.) ''' + AA = A + + + + for x in range(x0-radius, x0+radius+1): + for y in range(y0-radius, y0+radius+1): + for z in range(z0-radius, z0+radius+1): + ''' deb: measures how far a coordinate in A is far from the center. + deb>=0: inside the sphere. + deb<0: outside the sphere.''' + deb = radius - ((x0-x)**2 + (y0-y)**2 + (z0-z)**2)**0.5 + if (deb)>=0: AA[x,y,z] = value + return AA + +val = 0 +for coord in new_coords: + val += 1 + new_spheres = generate_sphere(new_spheres, int(coord[0]), int(coord[1]), int(coord[2]), 2, val) + +nib.save(nib.Nifti1Image(new_spheres, volume_recon.affine),os.path.join(mod2_folder, subject+'_'+reference_session+'_acq-3D_space-T00mri_T1w_electrode_spheres.nii.gz')) \ No newline at end of file diff --git a/python/docker/ieeg_recon_main_docker.py b/python/docker/ieeg_recon_main_docker.py new file mode 100644 index 0000000..d8ba2ec --- /dev/null +++ b/python/docker/ieeg_recon_main_docker.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python + +import argparse +import os +import subprocess +import sys + +def parse_args(): + parser = argparse.ArgumentParser() + + # Global arguments + parser.add_argument("-s", "--subject", help="Subject ID") + parser.add_argument("-rs", "--reference_session") + parser.add_argument("-m", "--module", help="Module to Run, -1 to run modules 2 and 3") + + # Module 2 arguments + parser.add_argument("-d", "--source_directory", help="Source Directory") + parser.add_argument("-cs", "--clinical_session") + parser.add_argument("-g", "--greedy", action="store_true") + parser.add_argument("-gc", "--greedy_centering", action="store_true") + parser.add_argument("-bs", "--brain_shift", action="store_true") + parser.add_argument("-fs", "--freesurfer_dir") + parser.add_argument("-dfo", "--deface_outputs", action="store_true") + + # Module 3 arguments + parser.add_argument("-a", "--atlas_path") + parser.add_argument("-an", "--atlas_name") + parser.add_argument("-ri", "--roi_indices", help="ROI Indices") + parser.add_argument("-rl", "--roi_labels", help="ROI Labels") + parser.add_argument("-r", "--radius", help="Radius for Electrode atlas Assignment") + parser.add_argument("-ird", "--ieeg_recon_dir", help="Source iEEG Recon Directory") + parser.add_argument("-lut", "--atlas_lookup_table", help="Atlas Lookup Table") + parser.add_argument("-apn", "--ants_pynet", help="Run AntsPyNet DKT Segmentation", action="store_true") + parser.add_argument("-ca", "--convert_atlas", help="Converts provided Atlas from MNI to Subject Space", action="store_true") + + # Module 3 MNI + parser.add_argument("-mni", "--run_mni", help="Run MNI registration", action="store_true") + + return parser.parse_args() + + +def file_check(args): + + run_pipeline = True + + # check for the minimum required files to run iEEG-recon + subject_dir = os.path.join(args.source_directory, args.subject) + + ref_dir = os.path.join(subject_dir, args.reference_session,'anat') + + # check for reference anat directory + if os.path.exists(ref_dir)==False: + print('ERROR: Reference directory: ', ref_dir, ' does not exist!') + run_pipeline = False + + # check for reference T1w image + t1w_ref = args.subject+'_'+args.reference_session+'_acq-3D_space-T00mri_T1w.nii.gz' + + if os.path.exists(os.path.join(ref_dir,t1w_ref))==False: + print('ERROR: Reference T1w image ', t1w_ref, ' does not exist in ', ref_dir,'. Check filename...') + run_pipeline = False + + + # check for clinical anat, ct and ieeg directories + clin_dir_t1w = os.path.join(subject_dir, args.clinical_session, 'anat') + clin_t1w = args.subject+'_'+args.clinical_session+'_acq-3D_space-T00mri_T1w.nii.gz' + + clin_dir_ct = os.path.join(subject_dir, args.clinical_session, 'ct') + clin_ct = args.subject+'_'+args.clinical_session+'_acq-3D_space-T01ct_ct.nii.gz' + + clin_dir_ieeg = os.path.join(subject_dir, args.clinical_session, 'ieeg') + clin_ieeg = args.subject+'_'+args.clinical_session+'_space-T01ct_desc-vox_electrodes.txt' + + + if os.path.exists(os.path.join(clin_dir_ct, clin_ct))==False: + print('ERROR: Clinical CT Scan image ', clin_ct, ' does not exist in ', clin_dir_ct,'. Check filenames...') + run_pipeline = False + + if os.path.exists(os.path.join(clin_dir_ieeg, clin_ieeg))==False: + print('ERROR: VoxTool coordinates ', clin_ieeg, ' does not exist in ', clin_dir_ieeg,'. Check filenames...') + run_pipeline = False + + return run_pipeline + + +def get_atlas_lookup_params(args): + if args.ants_pynet: + args.atlas_name = 'DKTantspynet' + args.atlas_path = 'notneeded' + return " -apn" + + if args.atlas_lookup_table: + return " -lut " + args.atlas_lookup_table + + elif (args.roi_indices != None) and (args.roi_labels != None): + return " -ri " + args.roi_indices + " -rl " + args.roi_labels + else: + return None + +def run_module2(args): + cmd = ["python", "/code/module2.py", "-s", args.subject, "-rs", args.reference_session, "-d", args.source_directory, "-cs", args.clinical_session] + + if args.greedy: + print('Module 2 is using Greedy correction ...') + cmd.append("-g") + elif args.greedy_centering: + print('Module 2 is using Greedy centering alone ...') + cmd.append("-gc") + + subprocess.call(cmd) + + if args.deface_outputs: + print("Defacing outputs...") + subprocess.call(["python","/code/module2_deface_outputs.py","-s", args.subject, "-d", args.source_directory]) + + + if args.brain_shift: + print("Applying brain shift correction to module 2 outputs...") + subprocess.call(["python", "/code/brain_shift.py", "-s", args.subject, "-rs", args.reference_session, "-d", args.source_directory, "-fs", args.freesurfer_dir, "-cs", args.clinical_session]) + +def run_core_module3(args, atlas_lookup_params): + if atlas_lookup_params!=None: + if not args.ieeg_recon_dir: + clinical_module_dir = os.path.join(args.source_directory, args.subject, 'derivatives', 'ieeg_recon') + else: + clinical_module_dir = args.ieeg_recon_dir + + cmd = [ + "python", "/code/module3.py", + "-s", args.subject, + "-rs", args.reference_session, + "-ird", clinical_module_dir, + "-a", args.atlas_path, + "-an", args.atlas_name, + "-r", args.radius + ] + atlas_lookup_params.split() + + subprocess.call(cmd) + else: + print('Incorrect Module 3 parameters... \nModule 3 will not run') + +def run_reports(args): + subprocess.call(["python", "/code/create_workspace.py", "-s", args.subject, "-rs", args.reference_session, "-d", args.source_directory, "-cs", args.clinical_session]) + subprocess.call(["python", "/code/create_html.py", "-s", args.subject, "-rs", args.reference_session, "-d", args.source_directory, "-cs", args.clinical_session]) + +def run_mni(args): + print('Running MNI registration, make sure Module 2 has ran already (i.e. run with -m 2 or -m -1 flags if not)') + subprocess.call(["python", "/code/module3_mni_v2.py", "-s", args.subject, "-rs", args.reference_session, "-cs", args.clinical_session,"-d", args.source_directory]) + + +def run_module3(args): + # this function runs module 3 with additional submodules + + + # run MNI transform + mni_xfm_file = os.path.join(args.source_directory,args.subject,'derivatives','ieeg_recon','module3','MNI',args.subject+'_'+args.reference_session+'_MNI152NLin2009cAsym_to_T00mri.h5') + if (args.run_mni) or (args.convert_atlas & (os.path.exists(mni_xfm_file)==False)): + run_mni(args) + + if args.convert_atlas: + subprocess.call(["python", "/code/module3_atlas_from_mni.py", "-s", args.subject, "-rs", args.reference_session, "-a", args.atlas_path,"-an",args.atlas_name,"-d", args.source_directory]) + args.atlas_path = os.path.join(args.source_directory,args.subject,'derivatives','ieeg_recon','module3',args.subject+'_'+args.reference_session+'_space-T00mri_atlas-'+args.atlas_name+'.nii.gz') + + # Determine atlas lookup parameters + atlas_lookup_params = get_atlas_lookup_params(args) + run_core_module3(args, atlas_lookup_params) + + + + +def main(): + + args = parse_args() + + # Printing initial arguments + print("Command:", " ".join(sys.argv)) + print('Subject: ', args.subject) + print('Clinical Session: ', args.clinical_session) + print('Reference Session: ', args.reference_session) + + # Check that the minimum required files are present + run_pipeline= file_check(args) + + if run_pipeline: + if args.module == '-1': + print('Running Modules 2 and 3 ... \n \n \n \n ') + run_module2(args) + run_reports(args) + run_module3(args) + + elif args.module == '2': + print('Running Module 2 ...') + run_module2(args) + run_reports(args) + + elif args.module == '3': + print('Running Module 3 ...') + run_module3(args) + + +if __name__ == "__main__": + main() \ No newline at end of file