fsl_mrsi 19.5 KB
Newer Older
1
2
#!/usr/bin/env python

Saad Jbabdi's avatar
Saad Jbabdi committed
3
# fsl_mrsi - wrapper script for MRSI fitting
4
5
#
# Author: Saad Jbabdi <saad@fmrib.ox.ac.uk>
Saad Jbabdi's avatar
Saad Jbabdi committed
6
#         William Carke <william.clarke@ndcn.ox.ac.uk>
7
#
William Clarke's avatar
William Clarke committed
8
# Copyright (C) 2019 University of Oxford
9

10
11
import time
import warnings
12

13
from fsl_mrs.auxiliary import configargparse
14
15
from fsl_mrs import __version__
from fsl_mrs.utils.splash import splash
16
17
from fsl_mrs.utils import fitting, misc, mrs_io, quantify

18
# NOTE!!!! THERE ARE MORE IMPORTS IN THE CODE BELOW (AFTER ARGPARSING)
19
20


21
22
def main():
    # Parse command-line arguments
William Clarke's avatar
William Clarke committed
23
24
25
26
    p = configargparse.ArgParser(
        add_config_file_help=False,
        description="FSL Magnetic Resonance Spectroscopy Imaging"
                    " Wrapper Script")
27

William Clarke's avatar
William Clarke committed
28
    p.add_argument('-v', '--version', action='version', version=__version__)
29

William Clarke's avatar
William Clarke committed
30
31
32
    required = p.add_argument_group('required arguments')
    fitting_args = p.add_argument_group('fitting options')
    optional = p.add_argument_group('additional options')
33
34
35

    # REQUIRED ARGUMENTS
    required.add_argument('--data',
William Clarke's avatar
William Clarke committed
36
37
                          required=True, type=str, metavar='<str>.NII',
                          help='input NIFTI file')
38
    required.add_argument('--basis',
William Clarke's avatar
William Clarke committed
39
40
                          required=True, type=str, metavar='<str>',
                          help='Basis file or folder')
41
    required.add_argument('--mask',
William Clarke's avatar
William Clarke committed
42
43
                          required=True, type=str, metavar='<str>',
                          help='mask volume')
44
    required.add_argument('--output',
William Clarke's avatar
William Clarke committed
45
46
                          required=True, type=str, metavar='<str>',
                          help='output folder')
47
48

    # FITTING ARGUMENTS
William Clarke's avatar
William Clarke committed
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
    fitting_args.add_argument('--algo', default='Newton', type=str,
                              help='algorithm [Newton (fast, default)'
                                   ' or MH (slow)]')
    fitting_args.add_argument('--ignore', type=str, nargs='+', metavar='METAB',
                              help='ignore certain metabolites [repeatable]')
    fitting_args.add_argument('--keep', type=str, nargs='+', metavar='METAB',
                              help='only keep these metabolites')
    fitting_args.add_argument('--combine', type=str, nargs='+',
                              action='append', metavar='METAB',
                              help='combine certain metabolites [repeatable]')
    fitting_args.add_argument('--ppmlim', default=(.2, 4.2), type=float,
                              nargs=2, metavar=('LOW', 'HIGH'),
                              help='limit the fit to a freq range'
                                   ' (default=(.2, 4.2))')
    fitting_args.add_argument('--h2o', default=None, type=str, metavar='H2O',
                              help='input .H2O file for quantification')
    fitting_args.add_argument('--baseline_order', default=2, type=int,
                              metavar=('ORDER'),
                              help='order of baseline polynomial'
                                   ' (default=2, -1 disables)')
    fitting_args.add_argument('--metab_groups', default=0, nargs='+',
                              type=str_or_int_arg,
                              help="metabolite groups: list of groups or list"
                                   " of names for indept groups.")
    fitting_args.add_argument('--add_MM', action="store_true",
74
                              help="include default macromolecule peaks")
William Clarke's avatar
William Clarke committed
75
76
77
    fitting_args.add_argument('--lorentzian', action="store_true",
                              help="Enable purely lorentzian broadening"
                                   " (default is Voigt)")
78
79
80
81
82
83
    fitting_args.add_argument('--ind_scale', default=None, type=str,
                              nargs='+',
                              help='List of basis spectra to scale'
                                   ' independently of other basis spectra.')
    fitting_args.add_argument('--disable_MH_priors', action="store_true",
                              help="Disable MH priors.")
84

85
    # ADDITONAL OPTIONAL ARGUMENTS
William Clarke's avatar
William Clarke committed
86
    optional.add_argument('--TE', type=float, default=None, metavar='TE',
87
                          help='Echo time for relaxation correction (ms)')
88
89
    optional.add_argument('--TR', type=float, default=None, metavar='TR',
                          help='Repetition time for relaxation correction (s)')
William Clarke's avatar
William Clarke committed
90
91
92
93
94
95
96
    optional.add_argument('--tissue_frac', type=str, nargs=3, default=None,
                          help='Tissue fraction nifti files registered to'
                               ' MRSI volume. Supplied in order: WM, GM, CSF.')
    optional.add_argument('--internal_ref', type=str, default=['Cr', 'PCr'],
                          nargs='+',
                          help='Metabolite(s) used as an internal reference.'
                               ' Defaults to tCr (Cr+PCr).')
97
98
99
100
101
102
    optional.add_argument('--wref_metabolite', type=str, default=None,
                          help='Metabolite(s) used as an the reference for water scaling.'
                               ' Uses internal defaults otherwise.')
    optional.add_argument('--ref_protons', type=int, default=None,
                          help='Number of protons that reference metabolite is equivalent to.'
                               ' No effect without setting --wref_metabolite.')
103
    optional.add_argument('--ref_int_limits', type=float, default=None, nargs=2,
104
105
                          help='Reference spectrum integration limits (low, high).'
                               ' No effect without setting --wref_metabolite.')
William Clarke's avatar
William Clarke committed
106
    optional.add_argument('--report', action="store_true",
107
                          help='output html report')
William Clarke's avatar
William Clarke committed
108
    optional.add_argument('--verbose', action="store_true",
109
                          help='spit out verbose info')
William Clarke's avatar
William Clarke committed
110
    optional.add_argument('--overwrite', action="store_true",
111
                          help='overwrite existing output folder')
William Clarke's avatar
William Clarke committed
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
    optional.add_argument('--single_proc', action="store_true",
                          help='Disable parallel processing of voxels.')
    optional.add_argument('--conj_fid', action="store_true",
                          help='Force conjugation of FID')
    optional.add_argument('--no_conj_fid', action="store_true",
                          help='Forbid automatic conjugation of FID')
    optional.add_argument('--conj_basis', action="store_true",
                          help='Force conjugation of basis')
    optional.add_argument('--no_conj_basis', action="store_true",
                          help='Forbid automatic conjugation of basis')
    optional.add_argument('--no_rescale', action="store_true",
                          help='Forbid rescaling of FID/basis/H2O.')
    optional.add('--config', required=False, is_config_file=True,
                 help='configuration file')

127
128
    # Parse command-line arguments
    args = p.parse_args()
129

130
131
132
133
134
135
    # Output kickass splash screen
    if args.verbose:
        splash(logo='mrsi')

    # ######################################################
    # DO THE IMPORTS AFTER PARSING TO SPEED UP HELP DISPLAY
William Clarke's avatar
William Clarke committed
136
137
    import os
    import shutil
138
139
    import numpy as np
    from fsl_mrs.utils import report
William Clarke's avatar
William Clarke committed
140
    from fsl_mrs.core import NIFTI_MRS
141
142
143
    import datetime
    import nibabel as nib
    from functools import partial
William Clarke's avatar
William Clarke committed
144
    import multiprocessing as mp
145
    # ######################################################
146

147
148
149
150
    # Check if output folder exists
    overwrite = args.overwrite
    if os.path.exists(args.output):
        if not overwrite:
William Clarke's avatar
William Clarke committed
151
152
            print(f"Folder '{args.output}' exists."
                  " Are you sure you want to delete it? [Y,N]")
153
154
155
156
157
158
159
160
            response = input()
            overwrite = response.upper() == "Y"
        if not overwrite:
            print('Early stopping...')
            exit()
        else:
            shutil.rmtree(args.output)
            os.mkdir(args.output)
161
162
163
    else:
        os.mkdir(args.output)

164
    # Save chosen arguments
William Clarke's avatar
William Clarke committed
165
    with open(os.path.join(args.output, "options.txt"), "w") as f:
166
167
168
        f.write(str(args))
        f.write("\n--------\n")
        f.write(p.format_values())
William Clarke's avatar
William Clarke committed
169
170

    # ######  Do the work #######
171
172

    # Read files
William Clarke's avatar
William Clarke committed
173
174
175
176
177
178
179
180
    mrsi_data = mrs_io.read_FID(args.data)
    if args.h2o is not None:
        H2O = mrs_io.read_FID(args.h2o)
    else:
        H2O = None

    mrsi = mrsi_data.mrs(basis_file=args.basis,
                         ref_data=H2O)
181
182
183
184

    def loadNii(f):
        nii = np.asanyarray(nib.load(f).dataobj)
        if nii.ndim == 2:
William Clarke's avatar
William Clarke committed
185
            nii = np.expand_dims(nii, 2)
186
187
        return nii

William Clarke's avatar
William Clarke committed
188
    if args.mask is not None:
189
190
191
192
        mask = loadNii(args.mask)
        mrsi.set_mask(mask)

    if args.tissue_frac is not None:
William Clarke's avatar
William Clarke committed
193
        # WM, GM, CSF
194
195
        wm = loadNii(args.tissue_frac[0])
        gm = loadNii(args.tissue_frac[1])
William Clarke's avatar
William Clarke committed
196
197
        csf = loadNii(args.tissue_frac[2])
        mrsi.set_tissue_seg(csf, wm, gm)
198
199

    # Set mrs output options from MRSI class object
William Clarke's avatar
William Clarke committed
200
201
202
203
204
205
206
    mrsi.conj_basis = args.conj_basis
    mrsi.no_conj_basis = args.no_conj_basis
    mrsi.conj_FID = args.conj_fid
    mrsi.no_conj_FID = args.no_conj_fid
    mrsi.rescale = not args.no_rescale
    mrsi.keep = args.keep
    mrsi.ignore = args.ignore
207
208

    # ppmlim for fitting
William Clarke's avatar
William Clarke committed
209
    ppmlim = args.ppmlim
210
    if ppmlim is not None:
William Clarke's avatar
William Clarke committed
211
        ppmlim = (ppmlim[0], ppmlim[1])
212
213

    # Store info in disctionaries to be passed to MRS and fitting
William Clarke's avatar
William Clarke committed
214
215
    Fitargs = {'ppmlim': ppmlim, 'method': args.algo,
               'baseline_order': args.baseline_order}
216
    if args.lorentzian:
Saad Jbabdi's avatar
Saad Jbabdi committed
217
        Fitargs['model'] = 'lorentzian'
218
    else:
Saad Jbabdi's avatar
Saad Jbabdi committed
219
220
        Fitargs['model'] = 'voigt'

221
222
223
    if args.disable_MH_priors:
        Fitargs['disable_mh_priors'] = True

Saad Jbabdi's avatar
Saad Jbabdi committed
224
225
    # Echo time
    if args.TE is not None:
226
        echotime = args.TE * 1E-3
Saad Jbabdi's avatar
Saad Jbabdi committed
227
228
229
230
    elif 'TE' in mrsi.header:
        echotime = mrsi.header['TE']
    else:
        echotime = None
231
232
233
234
235
236
237
    # Repetition time
    if args.TR is not None:
        repetition_time = args.TR
    elif 'RepetitionTime' in mrsi_data.hdr_ext:
        repetition_time = mrsi_data.hdr_ext['RepetitionTime']
    else:
        repetition_time = None
238
239

    # Fitting
240
    if args.verbose:
241
242
        print('\n--->> Start fitting\n\n')
        print(f'    Algorithm = [{args.algo}]\n')
243

Saad Jbabdi's avatar
Saad Jbabdi committed
244
245
    # Initialise by fitting the average FID across all voxels
    if args.verbose:
William Clarke's avatar
William Clarke committed
246
247
        print("    Initialise with average fit")
    mrs = mrsi.mrs_from_average()
Saad Jbabdi's avatar
Saad Jbabdi committed
248
    Fitargs_init = Fitargs.copy()
Saad Jbabdi's avatar
Saad Jbabdi committed
249
    Fitargs_init['method'] = 'Newton'
250
    res_init, _ = runvoxel([mrs, 0, None], args, Fitargs_init, echotime, repetition_time)
Saad Jbabdi's avatar
Saad Jbabdi committed
251
252
    Fitargs['x0'] = res_init.params

Saad Jbabdi's avatar
Saad Jbabdi committed
253
    # quick summary figure
William Clarke's avatar
William Clarke committed
254
255
256
257
258
    report.fitting_summary_fig(
        mrs,
        res_init,
        filename=os.path.join(args.output, 'fit_avg.png'))

Saad Jbabdi's avatar
Saad Jbabdi committed
259
    # Create interactive HTML report
Saad Jbabdi's avatar
Saad Jbabdi committed
260
    if args.report:
William Clarke's avatar
William Clarke committed
261
262
263
264
265
266
267
268
269
        report.create_report(
            mrs,
            res_init,
            filename=os.path.join(args.output, 'report.html'),
            fidfile=args.data,
            basisfile=args.basis,
            h2ofile=args.h2o,
            date=datetime.datetime.now().strftime("%Y-%m-%d %H:%M"))

270
271
    warnings.filterwarnings("ignore")
    if args.single_proc:
272
        if args.verbose:
273
274
            print('    Running sequentially (are you sure about that?) ')
        results = []
William Clarke's avatar
William Clarke committed
275
        for idx, mrs in enumerate(mrsi):
276
            res = runvoxel(mrs, args, Fitargs, echotime, repetition_time)
277
278
279
280
281
282
            results.append(res)
            if args.verbose:
                print(f'{idx+1}/{mrsi.num_masked_voxels} voxels completed')
    else:
        if args.verbose:
            print(f'    Parallelising over {mp.cpu_count()} workers ')
William Clarke's avatar
William Clarke committed
283

284
        func = partial(runvoxel, args=args, Fitargs=Fitargs, echotime=echotime, repetition_time=repetition_time)
285
        with mp.Pool(processes=mp.cpu_count()) as p:
William Clarke's avatar
William Clarke committed
286
            results = p.map_async(func, mrsi)
Saad Jbabdi's avatar
Saad Jbabdi committed
287
            if args.verbose:
Saad Jbabdi's avatar
Saad Jbabdi committed
288
                track_job(results)
William Clarke's avatar
William Clarke committed
289

William Clarke's avatar
William Clarke committed
290
            results = results.get()
William Clarke's avatar
William Clarke committed
291

292
    # Save output files
293
    if args.verbose:
294
        print(f'--->> Saving output files to {args.output}\n')
William Clarke's avatar
William Clarke committed
295

296
    # Results --> Images
William Clarke's avatar
William Clarke committed
297
    # Store concentrations, uncertainties, residuals, predictions
298
    # Output:
William Clarke's avatar
William Clarke committed
299
300
301
    # 1) concs - folders with N_metabs x 3D nifti for each scaling
    #    (raw,internal,molarity,molality)
    # 2) uncertainties - N_metabs x 3D nifti as percentage
302
303
304
305
    # 3) qc - 2 x N_metabs x 3D nifti for SNR and fwhm
    # 4) fit - predicted, residuals and baseline?

    # Generate the folders
William Clarke's avatar
William Clarke committed
306
307
308
309
    concs_folder = os.path.join(args.output, 'concs')
    uncer_folder = os.path.join(args.output, 'uncertainties')
    qc_folder = os.path.join(args.output, 'qc')
    fit_folder = os.path.join(args.output, 'fit')
310
311
312
313
314
315

    os.mkdir(concs_folder)
    os.mkdir(uncer_folder)
    os.mkdir(qc_folder)
    os.mkdir(fit_folder)

William Clarke's avatar
William Clarke committed
316
317
318
    # Extract concentrations
    indicies = [res[1] for res in results]
    scalings = ['raw']
319
320
321
322
323
324
325
    if results[0][0].concScalings['internal'] is not None:
        scalings.append('internal')
    if results[0][0].concScalings['molarity'] is not None:
        scalings.append('molarity')
    if results[0][0].concScalings['molality'] is not None:
        scalings.append('molality')

William Clarke's avatar
William Clarke committed
326
327
328
329
330
331
332
    def save_img_output(fname, data):
        if data.ndim > 3 and data.shape[3] == mrsi.FID_points:
            NIFTI_MRS(data, header=mrsi_data.header).save(fname)
        else:
            img = nib.Nifti1Image(data, mrsi_data.voxToWorldMat)
            nib.save(img, fname)

333
334
    metabs = results[0][0].metabs
    for scale in scalings:
William Clarke's avatar
William Clarke committed
335
        cur_fldr = os.path.join(concs_folder, scale)
336
337
        os.mkdir(cur_fldr)
        for metab in metabs:
William Clarke's avatar
William Clarke committed
338
339
            metab_conc_list = [res[0].getConc(scaling=scale, metab=metab)
                               for res in results]
340
            file_nm = os.path.join(cur_fldr, metab + '.nii.gz')
William Clarke's avatar
William Clarke committed
341
342
343
344
345
346
            save_img_output(file_nm,
                            mrsi.list_to_matched_array(
                                metab_conc_list,
                                indicies=indicies,
                                cleanup=True,
                                dtype=float))
William Clarke's avatar
William Clarke committed
347
348

    # Uncertainties
349
    for metab in results[0][0].metabs:
William Clarke's avatar
William Clarke committed
350
351
        metab_sd_list = [res[0].getUncertainties(metab=metab)
                         for res in results]
352
        file_nm = os.path.join(uncer_folder, metab + '_sd.nii.gz')
William Clarke's avatar
William Clarke committed
353
354
355
356
357
358
        save_img_output(file_nm,
                        mrsi.list_to_matched_array(
                            metab_sd_list,
                            indicies=indicies,
                            cleanup=True,
                            dtype=float))
359
360
361

    # qc - SNR & FWHM
    for metab in results[0][0].original_metabs:
William Clarke's avatar
William Clarke committed
362
363
        metab_fwhm_list = [res[0].getQCParams(metab=metab)[1]
                           for res in results]
364
        file_nm = os.path.join(qc_folder, metab + '_fwhm.nii.gz')
William Clarke's avatar
William Clarke committed
365
366
367
368
369
370
        save_img_output(file_nm,
                        mrsi.list_to_matched_array(
                            metab_fwhm_list,
                            indicies=indicies,
                            cleanup=True,
                            dtype=float))
William Clarke's avatar
William Clarke committed
371
372
373

        metab_snr_list = [res[0].getQCParams(metab=metab)[0]
                          for res in results]
374
        file_nm = os.path.join(qc_folder, metab + '_snr.nii.gz')
William Clarke's avatar
William Clarke committed
375
376
377
378
379
380
        save_img_output(file_nm,
                        mrsi.list_to_matched_array(
                            metab_snr_list,
                            indicies=indicies,
                            cleanup=True,
                            dtype=float))
381
382

    # fit
William Clarke's avatar
William Clarke committed
383
    # TODO: check if data has been conjugated, if so conjugate the predictions
384
385
    mrs_scale = mrsi.get_scalings_in_order()
    pred_list = []
William Clarke's avatar
William Clarke committed
386
    for res, scale in zip(results, mrs_scale):
387
        pred_list.append(res[0].pred / scale['FID'])
William Clarke's avatar
William Clarke committed
388
    file_nm = os.path.join(fit_folder, 'fit.nii.gz')
William Clarke's avatar
William Clarke committed
389
390
391
392
393
394
    save_img_output(file_nm,
                    mrsi.list_to_matched_array(
                        pred_list,
                        indicies=indicies,
                        cleanup=False,
                        dtype=np.complex64))
395
396

    res_list = []
William Clarke's avatar
William Clarke committed
397
    for res, scale in zip(results, mrs_scale):
398
        res_list.append(res[0].residuals / scale['FID'])
William Clarke's avatar
William Clarke committed
399
    file_nm = os.path.join(fit_folder, 'residual.nii.gz')
William Clarke's avatar
William Clarke committed
400
401
402
403
404
405
    save_img_output(file_nm,
                    mrsi.list_to_matched_array(
                        res_list,
                        indicies=indicies,
                        cleanup=False,
                        dtype=np.complex64))
406
407

    baseline_list = []
William Clarke's avatar
William Clarke committed
408
    for res, scale in zip(results, mrs_scale):
409
        baseline_list.append(res[0].baseline / scale['FID'])
William Clarke's avatar
William Clarke committed
410
    file_nm = os.path.join(fit_folder, 'baseline.nii.gz')
William Clarke's avatar
William Clarke committed
411
412
413
414
415
416
    save_img_output(file_nm,
                    mrsi.list_to_matched_array(
                        baseline_list,
                        indicies=indicies,
                        cleanup=False,
                        dtype=np.complex64))
417

418
419
420
421
    if args.verbose:
        print('\n\n\nDone.')


422
def runvoxel(mrs_in, args, Fitargs, echotime, repetition_time):
William Clarke's avatar
William Clarke committed
423
    mrs, index, tissue_seg = mrs_in
424

William Clarke's avatar
William Clarke committed
425
426
427
428
429
    # Parse metabolite groups
    metab_groups = misc.parse_metab_groups(mrs, args.metab_groups)

    if args.add_MM:
        n = mrs.add_MM_peaks(gamma=40, sigma=30)
430
        new_metab_groups = [i + max(metab_groups) + 1 for i in range(n)]
William Clarke's avatar
William Clarke committed
431
432
433
434
435
436
        new_metab_groups = metab_groups + new_metab_groups
    else:
        new_metab_groups = metab_groups.copy()
    res = fitting.fit_FSLModel(mrs, **Fitargs, metab_groups=new_metab_groups)

    # Internal and Water quantification if requested
437
438
439
440
441
442
443
444
445
446
    if (mrs.H2O is None) or (echotime is None) or (repetition_time is None):
        if echotime is None:
            warnings.warn('H2O file provided but could not determine TE:'
                          ' no absolute quantification will be performed.',
                          UserWarning)
        if repetition_time is None:
            warnings.warn('H2O file provided but could not determine TR:'
                          ' no absolute quantification will be performed.',
                          UserWarning)
        res.calculateConcScaling(mrs, internal_reference=args.internal_ref, verbose=args.verbose)
William Clarke's avatar
William Clarke committed
447
    else:
448
449
450
451
        # Form quantification information
        q_info = quantify.QuantificationInfo(echotime,
                                             repetition_time,
                                             mrs.names,
452
453
454
455
                                             mrs.centralFrequency / 1E6,
                                             water_ref_metab=args.wref_metabolite,
                                             water_ref_metab_protons=args.ref_protons,
                                             water_ref_metab_limits=args.ref_int_limits)
456
457
458
459
460
461

        if tissue_seg:
            q_info.set_fractions(tissue_seg)
        if args.h2o_scale:
            q_info.add_corr = args.h2o_scale

William Clarke's avatar
William Clarke committed
462
        res.calculateConcScaling(mrs,
463
464
465
                                 quant_info=q_info,
                                 internal_reference=args.internal_ref,
                                 verbose=args.verbose)
William Clarke's avatar
William Clarke committed
466
467
468
469
470
471
472
473
474
    # Combine metabolites.
    if args.combine is not None:
        res.combine(args.combine)

    return res, index


def str_or_int_arg(x):
    try:
475
        return int(x)
William Clarke's avatar
William Clarke committed
476
    except ValueError:
477
478
        return x

William Clarke's avatar
William Clarke committed
479

480
class PoolProgress:
William Clarke's avatar
William Clarke committed
481
482
    def __init__(self, pool, update_interval=3):
        self.pool = pool
Saad Jbabdi's avatar
Saad Jbabdi committed
483
        self.update_interval = update_interval
William Clarke's avatar
William Clarke committed
484

Saad Jbabdi's avatar
Saad Jbabdi committed
485
486
    def track(self, job):
        task = self.pool._cache[job._job]
William Clarke's avatar
William Clarke committed
487
488
        while task._number_left > 0:
            print("Voxels remaining = {0}".
489
                  format(task._number_left * task._chunksize))
Saad Jbabdi's avatar
Saad Jbabdi committed
490
            time.sleep(self.update_interval)
491

Saad Jbabdi's avatar
Saad Jbabdi committed
492
493
494

def track_job(job, update_interval=3):
    while job._number_left > 0:
William Clarke's avatar
William Clarke committed
495
496
        print(f"    {job._number_left * job._chunksize} Voxels remaining    ",
              end='\r', flush=True)
Saad Jbabdi's avatar
Saad Jbabdi committed
497
498
        time.sleep(update_interval)

William Clarke's avatar
William Clarke committed
499

500
501
if __name__ == '__main__':
    main()