#!/usr/bin/env python

# python3 status: started

# system libraries
import sys
import gc, math, copy

# AFNI libraries
from afnipy import option_list as OL
from afnipy import lib_timing as LT
from afnipy import afni_util as UTIL
from afnipy import lib_textdata as TD

# ----------------------------------------------------------------------
# globals

g_help_string = """
=============================================================================
timing_tool.py    - for manipulating and evaluating stimulus timing files
                    (-stim_times format: where each row is a separate run)

   purpose: ~1~

   This program is meant to work with ascii files containing rows of floats
   ('*' characters are ignored).  This is the format used by 3dDeconvolve
   with the -stim_times option.  Some timing files do not need evaluation,
   such as those where the timing is very consistent.  However, it may be
   important to examine those files from a random timing design.

   Recall that an ISI (inter-stimulus interval) is the interval of time
   between the end of one stimulus and start of the next.

   The basic program operations include:

       o reporting ISI statistics, such as min/mean/max values per run
       o reporting overall ISI statistics for a set of timing files
       o converting stim_times format to stim_file format
       o adding a constant offset to time
       o combining multiple timing files into 1 (like '1dcat' + sort)
       o appending additional timing runs (like 'cat')
       o sort times per row (though 3dDeconvolve does not require this)
       o converting between local and global stim times

   A sample stimulus timing file having 3 runs with 4 stimuli per run
   might look something like the following.  Note that the file does not
   imply the durations of the stimuli, except that stimuli are generally
   not allowed to overlap.

      17.3 24.0 66.0 71.6
      11.0 30.6 49.2 68.5
      19.4 28.7 53.8 69.4

   The program works on either a single timing element (which can be modified),
   or a list of them (which cannot be modified).  The only real use of a list
   of timing elements is to show statistics (via -multi_show_isi_stats).

--------------------------------------------------------------------------
examples: ~1~

   Example 0. basic commands ~2~

         timing_tool.py -help
         timing_tool.py -hist
         timing_tool.py -show_valid_opts
         timing_tool.py -ver

   Example 1. combine the timing of 2 (or more) files ~2~

      Extend one timing by another and sort.  Write to a new timing file.

         timing_tool.py -timing stimesB_01_houses.1D         \\
                        -extend stimesB_02_faces.1D          \\
                        -sort                                \\
                        -write_timing stimesB_extended.1D

   Example 2. subtract 12 seconds from each stimulus time ~2~

      For example, subtract 12 seconds to offset TRs dropped prior to
      the magnetization steady state.

         timing_tool.py -timing stimesB_01_houses.1D         \\
                        -add_offset -12.0                    \\
                        -write_timing stimesB1_offset12.1D

   Example 2b. similar to 2, but scale times (multiply) by 0.975 ~2~

      Scale, perhaps to account for a different TR or stimulus duration.

         timing_tool.py -timing stimesB_01_houses.1D         \\
                        -scale_data 0.975                    \\
                        -write_timing stimesB1_scaled.1D

   Example 2c. shift times so first event is at start of run ~2~

      This is like adding a negative offset equal to the first event time
      of each run.

         timing_tool.py -timing stimesB_01_houses.1D         \\
                        -shift_to_run_offset 0               \\
                        -write_timing stimesB1_offset0.1D

   Example 3. show timing statistics for task and rest ~2~

      Show timing statistics for the 3 timing files generated by example 3
      from "make_random_timing -help".  To be accurate, specify the run and
      stimulus durations.

         timing_tool.py -multi_timing stimesC_*.1D           \\
                        -run_len 200 -multi_stim_dur 3.5     \\
                        -multi_show_isi_stats

   Example 4. show timing stats where durations and run lengths vary ~2~

      Show timing statistics for the timing files generated by example
      6 from "make_random_timing -help".  Since both the run and stimulus
      durations vary, 4 run lengths and 3 stimulus durations are given.

         timing_tool.py -multi_timing stimesF_*.1D           \\
                        -run_len 200 190 185 225             \\
                        -multi_stim_dur 3.5 4.5 3            \\
                        -multi_show_isi_stats

   Example 5. partition a timing file based on a partition file ~2~

      Partition the stimulus timing file 'response_times.1D' into
      multiple timing files based on the labels in a partition file,
      partitions.1D.  If partitions.txt contains (0, correct, incorrect),
      there will be 2 output timing files, new_times_correct.1D and
      new_times_incorrect.1D.
      Times where the partition label is '0' will be skipped.

         timing_tool.py -timing response_times.1D       \\
                        -partition partitions.txt new_times

   Example 6a. convert a stim_times timing file to 0/1 stim_file format ~2~

      Suppose the timing is random where each event lasts 2.5 seconds and runs
      are of lengths 360, 360 and 400 seconds.  Convert timing.txt to sfile.1D
      on a TR grid of 0.5 seconds (oversampling), where a TR gets an event if
      at least 30% of the TR is is occupied by stimulus.

          timing_tool.py -timing timing.txt -timing_to_1D sfile.1D      \\
                         -tr 0.5 -stim_dur 2.5 -min_frac 0.3            \\
                         -run_len 360 360 400

      ** consider option -timing_to_1D_warn_ok

    Example 6b. evaluate the results ~2~

       Use waver to convolve sfile.1D with GAM and use 3dDeconvolve to
       convolve the timing file with BLOCK(2.5).  Then plot.

          waver -GAM -TR 0.5 -peak 1 -input sfile.1D > waver.1D

          3dDeconvolve -nodata 2240 0.5 -concat '1D: 0 720 1440'        \\
                       -polort -1 -num_stimts 1                         \\
                       -stim_times 1 timing.txt 'BLOCK(2.5)'            \\
                       -x1D X.xmat.1D -x1D_stop

          1dplot -sepscl sfile.1D waver.1D X.xmat.1D

   Example 6c. like 6a, but per run; leave each run in a separate file ~2~

       Add option -per_run_file.

          timing_tool.py -timing timing.txt -timing_to_1D sfile.1D      \\
                         -tr 0.5 -stim_dur 2.5 -min_frac 0.3            \\
                         -run_len 360 360 400 -per_run_file

   Example 6d. like 6c, but write amplitude modulators ~2~

       Add option -timing_to_1D_mods.

          timing_tool.py -timing timing.txt -timing_to_1D smods.1D      \\
                         -timing_to_1D_mods                             \\
                         -tr 0.5 -stim_dur 2.5 -min_frac 0.3            \\
                         -run_len 360 360 400 -per_run_file

   Example 6e. use multiple timing files, add combine using 'sum' ~2~

       Apply with -multi_timing and add -timing_to_1D_method.
       Assume the stimulus durations are encoded in the timing files.

          timing_tool.py -multi_timing stim*.txt -timing_to_1D sum.1D   \\
                         -timing_to_1D_method sum                       \\
                         -tr 0.5 -min_frac 0.3 -run_len 400

   Example 7a.  truncate stimulus times to the beginning of respective TRs ~2~

      Given a TR of 2.5 seconds and random stimulus times, truncate those times
      to multiples of the TR (2.5).

          timing_tool.py -timing timing.txt -tr 2.5 -truncate_times     \\
                         -write_timing trunc_times.txt

      Here, 11.83 would get truncated down to 10, the largest multiple of 2.5
      less than or equal to the original time.

   Example 7b. round time based on TR fraction, rather than truncating ~2~

      Instead of just truncating the times, round them to the nearest TR,
      based on some TR fraction.  In this example, round up to the next TR
      when a stimulus occurs at least 70% into a TR, otherwise round down to
      the beginning.

          timing_tool.py -timing timing.txt -tr 2.5 -round_times 0.7    \\
                         -write_timing round_times.txt

      With no rounding, a time of 11.83 would be truncated to 10.0.  But 11.83
      is 1.83 seconds into the TR, or is 73.2 percent into the TR.  Since it is
      at least 70% into the TR, it is rounded up to the next one.

      Here, 11.83 would get rounded up to 12.5.

   Example 8a.  create an event list from stimulus timing files ~2~

      The TR is 1.25s, events are ~1 TR long.  Require them to occupy at
      least half of the given TR.  Specify that rows should be per run and
      the run durations are all 370.

          timing_tool.py -multi_timing stimes.*.txt        \\
               -multi_timing_to_events all.events.txt      \\
               -tr 1.25 -multi_stim_dur 1 -min_frac 0.5    \\
               -per_run -run_len 370

   Example 8b.  break the event list into events and ISIs ~2~

      Break the event list into 2, one for a sequence of changing event types,
      one for a sequence of ISIs (TRs from one event to the next, including
      the TR of the event).  So if the event file from #8 shows:
         0 0 3 0 0 0 0 1 0 2 2 0 0 0 ...
      The resulting event/ISI files would read:
        event: 0 3 1 2 2 ...
        ISI:   2 5 2 1 4 ...

          timing_tool.py -multi_timing stimes.*.txt            \\
               -multi_timing_to_event_pair events.txt isi.txt  \\
               -tr 1.25 -multi_stim_dur 1 -min_frac 0.5        \\
               -per_run -run_len 370

   Example 9a.  convert from global stim times to local ~2~

       This requires knowing the run lengths, say 4 runs of 200 seconds here.
       The result will have 4 rows, each starting at time 0.

          timing_tool.py -timing stim.1D                       \\
                -global_to_local local.1D                      \\
                -run_len 200 200 200

       Note that if stim.1D looks like this ( ** but as a single column ** ):

                12.3 115 555 654 777 890

       then local.1D will look like this:

                12.3 115
                *
                155 254 377 490

       It will complain about the 3 times after the last run ends (no run
       should have times above 200 sec).

   Example 9b.  convert from local timing back to global ~2~

          timing_tool.py -timing local.1D                       \\
                -local_to_global global.1D                      \\
                -run_len 200 200 200

   Example 10.  display within-TR statistics ~2~

       Display within-TR statistics of stimulus timing files, to show
       when stimuli occur within TRs.  The -tr option must be specified.

       a. one file: show offset statistics (using -show_tr_stats)

             timing_tool.py -timing stim01_houses.txt -tr 2.0 -show_tr_stats

       b. (one or) many files (use -multi_timing)

             timing_tool.py -multi_timing stim*.txt -tr 2.0 -show_tr_stats

       c. only warn about potential problems (use -warn_tr_stats)

             timing_tool.py -multi_timing stim*.txt -tr 2.0 -warn_tr_stats

       d. create a histogram of stim time offsets within the TR
          (time modulo TR)
          (quietly output offsets, and pipe them through 3dhistog)

             timing_tool.py -timing stim01_houses.txt -verb 0 \\
                            -show_tr_offsets -tr 1.25         \\
                            | 3dhistog -nbin 20 1D:stdin > hist.1D
             1dplot -hist -x hist.1D'[0]' hist.1D'[1]'

          consider also:  3dhistog -noempty 1D:stdin

       e. show per run and global TR-offset statistics

             timing_tool.py -timing stim01_houses.txt         \\
                            -show_tr_offset_stats -tr 1.25

       f. show per run and global amplitude modulation statistics
          (can be run with -multi_timing)

             timing_tool.py -timing stim01_houses.txt -show_modulator_stats

   Example 11.  test a file for local/global timing issues ~2~

       Test a timing file for timing issues, which currently means having
       times that are intended to be local but might be read as global.

          timing_tool.py -multi_timing stim*.txt -test_local_timing

   Examples 12 and 13 : akin to Example 8...  ~2~

   Example 12. create a timing style event list  ~2~

      Create a simple horizontal event list (one row per run), where the event
      class is the (1-based) index of the given input file.  This is very
      similar to the first file output in example 8b, but no TR information is
      required here.  Events are simply ordered.

          timing_tool.py -multi_timing stimes.*.txt            \\
               -multi_timing_to_event_list index elist12.txt

   Example 13a. create a GE (global events) list of ALL fields ~2~

      Create a vertical GE (global events) list, showing ALL fields.

      timing_tool.py -multi_timing stim.* -multi_timing_to_event_list GE:ALL -

      Note: for convenience, one can also use -show_events, as in:

        timing_tool.py -multi_timing stim.* -show_events

      This is much easier to remember, and it is a very common option.

   Example 13b. like 13a, but restrict the output ~2~

      Restrict global events list to:

             event index (i), duration (d), offset from previous (o),
             start time (t), and stim file (f)

       Also, write the output to elist13b.txt, rather than the screen.

          timing_tool.py -multi_timing stimes.*.txt            \\
               -multi_timing_to_event_list GE:idotf elist13b.txt

   Example 14.  partition one stimulus class based on others ~2~

      Class '1' (from the first input) is partitioned based on the class that
      precedes it.  If none precede an early class 1 event, event INIT is used
      as the default (else consider '-part_init 2', for example).

          timing_tool.py -multi_timing stimes.*.txt            \\
               -multi_timing_to_event_list part part1.pred.txt

      The result could be applied to actually partition the first timing file,
      akin to Example 5:

         timing_tool.py -timing stimes.1.txt                   \\
                        -partition part1.pred.txt stimes.1.part

   Example 15.  add a simple linear modulator ~2~

      For modulation across a run, add the event modulator as the event
      time divided by the run length, meaning the fraction the run that
      has passed before the event time.

         timing_tool.py -timing stim_times.txt -run_len 300     \\
                        -marry_AM lin_run_fraq -write_timing stim_mod.txt

   Example 16.  use end times to imply event durations ~2~

      Given timing files A.txt and B.txt, suppose that B always follows A
      and that there is no rest between them.  Then the durations of the A
      events would be defined by the B-A differences.  To apply durations
      to class A events as such, use -apply_end_times_as_durations.

         timing_tool.py -timing A.txt -apply_end_times_as_durations B.txt \\
                        -write_timing A_with_durs.txt

   Example 17. show duration statistics ~2~

      Given a timing file with durations, show the min, mean, max and stdev
      of the list of event durations.

         timing_tool.py -timing stimes.txt -show_duration_stats

   Example 18a. convert FSL formatted timing files to AFNI timing format ~2~

      A set of FSL timing files (for a single class), one file per run,
      can be read using -fsl_timing_files (rather than -timing, say).  At
      that point, it internally becomes like a normal timing element.

      If the files have varying durations, the result will be in AFNI
      duration modulation format.  If the files have amplitudes that are not
      constant 0 or constant 1, the result will have amplitude modulators.

         timing_tool.py -fsl_timing_files fsl_r1.txt fsl_r2.txt fsl_r3.txt \\
                        -write_timing combined.txt

   Example 18b.  force to married format, via -write_as_married ~2~

         timing_tool.py -fsl_timing_files fsl_r1.txt fsl_r2.txt fsl_r3.txt \\
                        -write_timing combined.txt -write_as_married

   Example 18c.  apply one FSL run as run 3 of a 4-run timing file ~2~

         timing_tool.py -fsl_timing_files fsl_r1.txt \\
                        -select_runs 0 0 1 0 -write_timing NEW.txt

   Example 18d.  apply two FSL runs as run 3 and 4 of a 5-run timing file ~2~

      The original runs can be duplicated, put into a new order or omitted.
      Also, truncate the event times to 1 place after the decimal (-nplaces),
      and similarly truncate the married terms (durations and/or amplitudes)
      to 1 place after the decimal (-mplaces).

         timing_tool.py -fsl_timing_files fsl_r1.txt fsl_r2.txt \\
                        -nplaces 1 -mplaces 1 -write_as_married \\
                        -select_runs 0 0 1 2 0 -write_timing NEW.txt

   Example 19a. convert TSV formatted timing files to AFNI timing format ~2~

      A tab separated value file contains events for all classes for a single
      run.  Such files might exist in a BIDS dataset.  Convert a single run
      to multiple AFNI timing files (or convert multiple runs).

         timing_tool.py -multi_timing_ncol_tsv sing_weather.run*.tsv \\
                        -write_multi_timing AFNI_timing.weather

      Consider -write_as_married, if useful.

   Example 19b.  extract ISI/duration/TR stats from TSV files ~2~

         timing_tool.py -multi_timing_ncol_tsv sing_weather.run*.tsv \\
                        -multi_show_isi_stats -multi_show_duration_stats

         timing_tool.py -multi_timing_ncol_tsv sing_weather.run*.tsv \\
                        -tr 2 -show_tr_stats

   Example 19c.  convert non-standard formatted TSV timing files to AFNI ~2~

      The default column labels were assumed in the prior examples:
         onset duration trial_type
      in this example, RT is used for duration, and participant_response is
      used for trial_type.  These TSV files are from the ds001205 dataset from
      openneuro.org.

      Output is just to an event list.

         timing_tool.py -tsv_labels onset RT participant_response           \\
                        -multi_timing_ncol_tsv sub-001_task-MGT_run*.tsv    \\
                        -write_multi_timing timing.sub-001.C.

   Example 19d.  as 19c, but include amplitude modulators ~2~

      Like 19c, but include "gain" and "loss" as amplitude modulators.

         timing_tool.py -tsv_labels onset RT participant_response gain loss \\
                        -multi_timing_ncol_tsv sub-001_task-MGT_run*.tsv    \\
                        -write_multi_timing timing.sub-001.D.

   Example 19e.  as 19d, but specify the same columns with 0-based indices ~2~

         timing_tool.py -tsv_labels 0 4 5 2 3                               \\
                        -multi_timing_ncol_tsv sub-001_task-MGT_run*.tsv    \\
                        -write_multi_timing timing.sub-001.E.

   Example 19f.  if duration is n/a, specify backup column ~2~

      In some cases (e.g. as reaction_time), duration might have a value
      of "n/a".  Specify an alternate column to use for duration when this
      occurs.

         timing_tool.py -tsv_labels onset reaction_time task            \\
                        -tsv_def_dur_label duration                     \\
                        -multi_timing_ncol_tsv s10517-pamenc_events.tsv \\
                        -write_multi_timing timing.sub-001.F.

   Example 19g.  just show the TSV label information ~2~

         timing_tool.py -tsv_labels onset reaction_time task            \\
                        -tsv_def_dur_label duration                     \\
                        -multi_timing_ncol_tsv s10517-pamenc_events.tsv \\
                        -show_tsv_label_details

      Consider "-show_events" to view event list.

   Example 19h.  can force the timing to be simple, AM, DM or AMDM ~2~

      Here "-force_write_type simple", but can use AM or DM or AMDM,
      to force the output to be exactly that modulation type.

         timing_tool.py -multi_timing_ncol_tsv sing_weather.run*.tsv \\
                        -write_multi_timing AFNI_timing.weather      \\
                        -force_write_type simple

   Example 20.  set event durations based on next events ~2~

      Suppose one has timing files for conditions Pre, BPress and Post,
      and one wants to set the duration for each Pre condition based on
      whatever comes next (usually a BPress, but if that does not happen,
      Post is the limit).

      Suppose the inputs are 3 timing files stim.Pre.txt, stim.BPress.txt and
      stim.Post.txt, and we want to create stim.Pre_DM.txt to be the same as
      stim.Pre.txt, but with that variable duration attached.  Then use the
      -multi_durations_from_offsets option as follows, providing the old
      label (file name) and the new file name for the class to change.

         timing_tool.py                                                 \\
            -multi_timing stim.Pre.txt stim.BPress.txt stim.Post.txt    \\
            -multi_durations_from_offsets stim.Pre.txt stim.Pre_DM.txt

--------------------------------------------------------------------------
Notes: ~1~

   1. Action options are performed in the order of the options.
      Note: -chrono has been removed.

   2. One of -timing or -multi_timing or -fsl_timing_files is required
      for processing.

   3. Option -run_len applies to single or multiple stimulus classes.  A single
      parameter would be used for all runs.  Otherwise one duration per run
      should be supplied.

--------------------------------------------------------------------------
basic informational options: ~1~

   -help                        : show this help
   -help_basis                  : describe various basis functions
   -hist                        : show the module history
   -show_valid_opts             : show all valid options
   -ver                         : show the version number

------------------------------------------
options with both single and multi versions (all single first): ~1~

   -timing TIMING_FILE          : specify a stimulus timing file to load ~2~

        e.g. -timing stimesB_01_houses.1D

        Use this option to specify a single stimulus timing file.  The user
        can modify this timing via some of the action options listed below.

   -show_isi_stats              : display timing and ISI statistics ~2~

        With this option, the program will display timing statistics for the
        single (possibly modified) timing element.

        If -tr is included, TR offset statistics are also shown.

   -show_timing_ele             : display info on the main timing element ~2~

        With this option, the program will display information regarding the
        single (possibly modified) timing element.

   -stim_dur DURATION           : specify the stimulus duration, in seconds ~2~

        e.g. -stim_dur 3.5

        This option allows the user to specify the duration of the stimulus,
        as applies to the single timing element.  The only use of this is
        in conjunction with -show_isi_stats.

            Consider '-show_isi_stats' and '-run_len'.

   --------------------

   -fsl_timing_files F1 F2 ...   : read a list of FSL formatted timing files ~2~

        e.g. -fsl_timing_files fsl.s1.run1.txt fsl.s1.run2.txt fsl.s1.run3.txt
        e.g. -fsl_timing_files fsl.stim.class.A.run.*.txt

        This is essentially an alternative to -timing, as the result is a
        single multi-run timing element.

        Each input file should have FSL formatted timing for a single run,
        and all for the same stimulus class.  Each file should contain a list
        of entries like:

            event_time  duration  amplitude

        e.g. with varying durations and amplitudes (fully married)

                0         5         3
                17.4      4.6       2.5
                ...

        e.g. with constant durations and (ignored) amplitudes (so not married)

                0         2         1
                17.4      2         1
                ...

        e.g. empty (no events)

                0         0         0

        If all durations are the same, the result will not have duration
        modulators.

        If all amplitudes are 0 or all are 1, the result will not have
        amplitude modulators.

        An empty file or one with a single line of '0 0 0' is considered to
        have no events (note that 0 0 0 means duration and amplitude of zero).

        Comment lines are okay (starting with #).

            Consider -write_as_married.

   --------------------

   -multi_timing FILE1 FILE2 ... : specify multiple timing files to load ~2~

        e.g. -timing stimesB_*.1D

        Use this option to specify a list of stimulus timing files.  The user
        cannot modify this data, but can display the overall ISI statistics
        from it.

        Options that pertain to this timing list include:

            -multi_show_isi_stats
            -multi_show_timing_ele
            -multi_stim_dur
            -run_len
            -write_all_rest_times

   -multi_timing_ncol_tsv FILE1 FILE2 ... : read TSV files into multi timing ~2~

            ** this option was previously called -multi_timing_3col_tsv
               (both work)

        e.g. -multi_timing_ncol_tsv sing_weather_run*.tsv
        e.g. -multi_timing_ncol_tsv tones.tsv

        Tab separated value (TSV) files, as one might find in OpenFMRI data,
        are formatted with a possible header line and 3 tab-separated columns:

            onset   duration    stim_class
            ...

        Timing for all event classes is contained in a single file, per run.

   -multi_show_duration_stats   : display min/mean/max/stdev of durations ~2~

        Show the minimum, mean, maximum and standard deviation of the list of
        all event durations, for each timing element.

   -multi_show_isi_stats        : display timing and ISI statistics ~2~

        With this option, the program will display timing statistics for the
        multiple timing files.

        If -tr is included, TR offset statistics are also shown.

        If -write_all_rest_times is included, write a file of rest durations.

   -multi_show_timing_ele       : display info on multiple timing elements ~2~

        With this option, the program will display information regarding the
        multiple timing element list.

   -multi_stim_dur DUR1 ...     : specify stimulus duration(s), in seconds ~2~

        e.g. -multi_stim_dur 3.5
        e.g. -multi_stim_dur 3.5 4.5 3

        This option allows the user to specify the durations of the stimulus
        classes, as applies to the multiple timing elements.  The only use of
        this is in conjunction with -multi_show_isi_stats.

        If only one duration is specified, it is applied to all elements.
        Otherwise, there should be as many stimulus durations as files
        specified with -multi_timing.

            Consider '-multi_show_isi_stats' and '-run_len'.

   -write_multi_timing PREFIX   : write timing instances to new files ~2~

        e.g. -write_multi_timing MT.

        After modifying the timing data, the multiple timing instances
        can be written out.

            Consider '-force_write_type', '-write_as_married'.

   -write_simple_tsv PREFIX     : write timing to new TSV files ~2~

        e.g. -write_simple_tsv MT.

        Akin to -write_multi_timing, this writes out what is seen as the stored
        (and pertinent) timing information.  The (tab-delimited) output is of
        the form:

            onset duration class [optional modulators...]

        If there are known modulators, they will be output.
        If some classes have modulators and some do not (or have fewer), the
        output will still be rectangular, with such modulators output as zeros.

            Consider '-write_multi_timing'.

------------------------------------------
action options (apply to multi timing elements, only): ~1~
------------------------------------------
action options (apply to single timing element, only): ~1~

   ** Note that these options are processed in the order they are read.

   -add_offset OFFSET           : add OFFSET to every time in main element ~2~

        e.g. -add_offset -12.0

        Use this option to add a single offset to all of the times in the main
        timing element.  For example, if the user deletes 3 4-second TRs from
        the EPI data, they may wish to subtract 12 seconds from every stimulus
        time, so that the times match the modified EPI data.

            Consider '-write_timing'.

   -apply_end_times_as_durations NEW_FILE : compute durations from offsets ~2~

        e.g. -apply_end_times_as_durations next_events.txt

        Treat each NEW_FILE event time as the ending of the corresponding
        INPUT (via -timing) event time to create a duration list.  So they
        should have the same number of events, and each NEW_FILE time should
        be just after the corresponding INPUT time.

            Consider '-write_timing' and '-show_duration_stats'.
            Consider example 16.

        Update: this method (while still available) can be applied via the
                newer -multi_durations_from_offsets option.

        See also, -multi_durations_from_offsets.

   -add_rows NEW_FILE           : append these timing rows to main element ~2~

        e.g. -add_rows more_times.1D

        Use this option to append rows from NEW_FILE to those of the main
        timing element.  If the user then wrote out the result, it would be
        identical to using cat: "cat times1.txt times2.txt > both_times.txt".

            Consider '-write_timing'.

   -extend NEW_FILE             : extend timing rows with those in NEW_FILE ~2~

        e.g. -extend more_times.1D

        Use this option to extend each row (run) with the times in NEW_FILE.
        This has an effect similar to that of '1dcat'.  Sorting the times is
        optional, done via '-sort'.  Note that 3dDeconvolve does not need the
        times to be sorted, though it is more understandable to the user.

            Consider '-sort' and '-write_timing'.

   -global_to_local LOCAL_NAME.1D  : convert from global timing to local ~2~

        e.g. -global_to_local local_times.1D

        Use this option to convert from global stimulus timing (in a single
        column format) to local stimulus timing.  Run durations must be given
        of course, to determine which run each stimulus occurs in.  Each
        stimulus time will be adjusted to be an offset into the current run,
        e.g. if each run is 120 s, a stimulus at time 143.6 would occur in run
        #2 (1-based) at time 23.6 s.

            Consider example 9a and options '-run_len' and '-local_to_global'.

   -local_to_global GLOBAL_NAME.1D : convert from local timing to global ~2~

        e.g. -local_to_global global_times.1D

        Use this option to convert from local stimulus timing (one row of times
        per run) to global stimulus timing (a single column of times across the
        runs, where time is considered continuous across the runs).

        Run durations must be given of course, to determine which run each
        stimulus occurs in.  Each stimulus time will be adjusted to be an
        offset from the beginning of the first run, as if there were no breaks
        between the runs.
        e.g. if each run is 120 s, a stimulus in run #2 (1-based) at time
        23.6 s would be converted to a stimulus at global time 143.6 s.

            Consider example 9b and options '-run_len' and '-global_to_local'.

   -marry_AM MTYPE      : add event modulators based on MTYPE ~2~

        e.g. -marry_AM lin_run_fraq
        e.g. -marry_AM lin_event_index

        Use this option to add a simple amplitude modulator to events.
        Current modulator types are:

           linear modulators (across events or time):

              lin_event_index   : event index, per run (1, 2, 3, ...)
              lin_run_fraq      : event time, as fractional offset into run
                                  (in [0,1])

        Non-index modulators require use of -run_len.

            Consider example 15.

   -partition PART_FILE PREFIX  : partition the stimulus timing file ~2~

        e.g. -partition partitions.txt new_times

        Use this option to partition the input timing file into multiple
        timing files based on the labels in a partition file, PART_FILE.
        The partition file would have the same number of rows and entries on
        each row as the timing file, but would contain labels to use in
        partitioning the times into multiple output files.

        A label of 0 will cause that timing entry to be dropped.  Otherwise,
        each distinct label will have those times put into its timing file.

        e.g.

                timing file:
                    23.5     46.0     79.3     84.9      116.2
                    11.4     38.2     69.7     93.5      121.8

                partition file:
                    correct  0        0        incorrect incorrect
                    0        correct  0        correct   correct

            ==> results in new_times_good.1D and new_times_bad.1D

                new_times_correct.1D:
                    23.5     0        0        0         0
                    0        38.2     0        93.5      121.8

                new_times_incorrect.1D:
                    0        0        0        84.9      116.2
                    *

   -round_times FRAC            : round times to multiples of the TR ~2~
                                  0.0 <= FRAC <= 1.0

        e.g. -round_times 0.7

        All stimulus times will be rounded to a multiple TR, rounding down if
        the fraction of the TR that has passed is less than FRAC, rounding up
        otherwise.

        Using the example of FRAC=0.7, if the TR is 2.5 seconds, then times are
        rounded down if they occur earlier than 1.75 seconds into the TR.  So
        11.83 would get rounded up to 12.5, while 11.64 would be rounded down
        to 10.

        FRAC = 1.0 is essentially floor() (as in -truncate_times), while
        FRAC = 0.0 is essentially ceil().

        This option requires -tr.

            Consider example 7b.  See also -truncate_times.

   -scale_data SCALAR           : multiply every stim time by SCALAR ~2~

        e.g. -scale_data 0.975

        Use this option to scale (multiply) all times by a single value.
        This might be useful in effectively changing the TR, or changing
        the stimulus frequency, if it is regular.

            Consider '-write_timing'.

   -show_duration_stats         : display min/mean/max/stdev of durations ~2~

        Show the minimum, mean, maximum and standard deviation of the list of
        all event durations.

   -show_modulator_stats        : display stats for amplitude modulators ~2~

        For each file (if -multi_timing) and each modulator, show per-run
        statistics (if more than one run) and global statistics of the
        amplitude modulators.  Statistics include the typical min, mean, max
        and stdev values.

   -show_timing                 : display the current single timing data ~2~

        This prints the current (possibly modified) single timing data to the
        terminal.  If the user is making multiple modifications to the timing
        data, they may wish to display the updated timing after each step.

   -show_tr_offsets             : display within-TR offsets of stim times ~2~

        Displays all stimulus times, modulo the TR.  Some examples:

            stim time       offset (using TR = 2s)
            ---------       ------
               0.7           0.7
               9.7           1.7
              10.3           0.3
              15.8           1.8

        Use -verb 0 to get only the times (in case of scripting).

            See also '-show_tr_stats', '-warn_tr_stats'.

   -show_tr_offset_stats        : display stats of within-TR stim offsets ~2~

        This is a more detailed version of -show_tr_stats, and might replace it.

        This displays the min, mean, max and stdev of stimulus times modulo
        the TR, both in seconds and as fractions of the TR.

        This also outputs similar values for the differences of the sorted list
        of fractional offset times.  And the same numbers are output restricted
        to onsets that are unique.

        Assuming onsets are uniformly distributed across the TR, we would
        expect something like:

            fractional:   min close to 0, max close to 1
            frac diffs:   small values for all, notably max
            frac u diffs: small values for all, notably max

        If more than 1 run, results are shown per run.
        Results are always shown globally.
        Include -verb 0 to avoid per-run stats.

        Some comments may be made for the global results.

            See also '-show_tr_stats', '-warn_tr_stats'.

   -show_tr_stats               : display within-TR statistics of stimuli ~2~

        This displays the mean, max and stdev of stimulus times modulo the TR,
        both in seconds and as fractions of the TR.

            See '-warn_tr_stats' for more details.

   -show_tsv_label_details      : display column label info for TSV files ~2~

        Use this option to display label information for TSV files.  It should
        be used in conjunction with -multi_timing_ncol_tsv and related options.

   -warn_tr_stats               : display within-TR stats only for warnings ~2~

        This is akin to -show_tr_stats, but output is only displayed if there
        might be a warning based on the timing.

        Warnings occur when the minimum fraction is positive and the maximum
        fraction is small (less than -min_frac, 0.3).  If such warnings are
        encountered, particularly in the case of TENT basis functions used in
        the linear regression, they can affect the X-matrix, essentially
        scaling beta #0 by the reciprocal of the fraction (noise dependent).

        In such a case the stimuli are almost TR-locked, and the user might be
        better off making them exactly TR-locked (by creating new timing files
        using "timing_tool.py -round_times").

            See also '-show_tr_stats', '-min_frac' and '-round_times'.

   -sort                        : sort the times, per row (run) ~2~

        This will cause each row (run) of the main timing element to be
        sorted (from smallest to largest).  Such a step may be highly desired
        after using '-extend', or after some external manipulation that causes
        the times to be unsorted.

        Note that 3dDeconvolve does not require sorted timing.

            Consider '-write_timing'.

   -test_local_timing           : test for problems with local timing ~2~

        The main purpose of this is to test for timing files that are intended
        to be interpreted by 3dDeconvolve as being LOCAL TIMES, but might
        actually be interpreted as being GLOBAL TIMES.

        Note that as of 18 Feb, 2014, any '*' in a timing file will cause it
        to be interpreted by 3dDeconvolve as LOCAL TIMES, even if the file is
        only a single column.

   -timing_to_1D output.1D      : convert stim_times format to stim_file ~2~

        e.g. -timing_to_1D stim_file.1D

        This action is used to convert stimulus times to set (i.e. 1) values
        in a 1D stim_file.

        The input timing can come from -timing or will be combined when it
        comes from -multi_timing.

        Besides an input -timing file, -tr is needed to specify the timing grid
        of the output 1D file, -stim_dur is needed to specify the duration of
        each stimulus (which might cross many output TRs), and -run_len is
        needed to specify the duration of each (or all) of the runs.

        The -min_frac option may be applied to give a minimum cutoff for the
        fraction of a TR occupied by a stimulus required to label that TR as a
        1.  If not, the default cutoff is 0.3.

        For example, assume options: '-tr 2', '-stim_dur 4.2', '-min_frac 0.2'.
        A stimulus at time 9.7 would last until 13.9.  TRs 0..4 would certainly
        be 0, TR 5 would also be 0 as the stimulus covers only .15 of the TR
        (.3 seconds out of 2 seconds).  TR 6 would be 1 since it is completely
        covered, and TR 7 would be 1 since .95 (1.9/2) would be covered.

        So the resulting 1D file would start with:

                0
                0
                0
                0
                0
                1
                1

        The main use of this operation is for PPI analysis, to partition the
        time series (maybe on a fine grid) with 1D files that are 1 when the
        given stimulus is on and 0 otherwise.

        Another use is to apply this for all timing files at once, to verify
        if and how they overlap.

            Consider -timing_to_1D_warn_ok.
            Consider -tr, -stim_dur, -min_frac, -run_len, -per_run_file.
            Consider timing_to_1D_method.

            Consider example 6a, 6c or 6e.

   -timing_to_1D_method METHOD  : set how to combine multi-timing to 1D ~2~

        Specify a METHOD for combining 1D files when using -multi_timing.

            bool    : simply flag a TR if any timing covers is
            sum     : sum up the timing that covers each TR
                      (this shows how any stimuli might overlap)

        This only applies to -timing_to_1D with -multi_timing.

   -timing_to_1D_mods           : write amp modulators to 1D, not binary ~2~

        For -timing_to_1D, instead of writing a binary 0/1 file, write the
        (first) amplitude modulators to the 1D file.

        This only applies to -timing_to_1D.

   -timing_to_1D_warn_ok        : make some conversion issues non-fatal ~2~

        Conditions from -timing_to_1D that this makes non-fatal:

           o  stimuli ending after the end of a run
           o  stimuli overlapping

        This only applies to -timing_to_1D.

   -transpose                   : transpose the data (only if rectangular) ~2~

        This works exactly like 1dtranspose, and requires each row to have
        the same number of entries (rectangular data).  The first row would
        be swapped with the first column, etc.

            Consider '-write_timing'.

   -truncate_times              : truncate times to multiples of the TR ~2~

        All stimulus times will be truncated to the largest multiple of the TR
        that is less than or equal to each respective time.  That is to say,
        shift each stimulus time to the beginning of its TR.

        This is particularly important when stimulus times are at a constant
        offset into each TR and at the same time using TENT basis functions
        for regression (in 3dDeconvolve, say).  The shorter the (non-zero)
        offset, the more correlated the first two tent regressors will be,
        possibly leading to unpredictable results.

        This option requires -tr.

            Consider example 7.

   -tsv_def_dur_label LABEL     : specify backup duration for n/a ~2~

        e.g. -tsv_def_dur_label duration

        In some TSV event files, an event duration might have a value of n/a,
        such as when the column is based on reaction time.  In such a case,
        this option can be used to specify an alternate TSV column to use for
        the event duration.

            See also, -tsv_labels.

   -write_as_married            : if possible, output in married format ~2~

        e.g. -write_as_married

        If all durations are equal, the default is to not write with duration
        modulation (as the constant duration would likely be provided as part
        of a basis function).  Use -write_as_married to include any constant
        duration as a modulator.

        If there are no modulators in the input but married output is wanted,
        consider -force_write_type.

            See also '-force_write_type'.

   -write_tsv_cols_of_interest NEW_FILE : write cols of interest ~2~

        e.g. -write_tsv_cols_of_interest cols_of_interest.tsv

        This is an esoteric function that goes with -multi_timing_ncol_tsv.
        Since the input TSV files often have many columns that make viewing
        difficult, this option can be used to extract only the relevant
        columns and write them to a new TSV file.

            Consider '-multi_timing_ncol_tsv'.

   -write_timing NEW_FILE       : write the current timing to a new file ~2~

        e.g. -write_timing new_times.1D

        After modifying the timing data, the user will probably want to write
        out the result.  Alternatively, the user could use -show_timing and
        cut-and-paste to write such a file.

            Consider '-force_write_type', '-write_as_married'.

------------------------------------------
action options (apply to multi timing elements, only): ~1~

   -multi_durations_from_offsets OLD NEW : set durations from next events ~2~

        e.g. -multi_durations_from_offsets stim.Pre.txt stim.Pre_DM.txt

        Given a set of timing files input via -multi_timing, set the durations
        for the events in one file to be based on when the next even happens.
        For example, the 'Pre' condition could be ended at the next button
        press event (or any other event that follows).

        Specify the OLD input to modify and the name of the NEW timing file to
        write.

        NEW will be the same as OLD, except for each event duration.

        This option is similar to -apply_end_times_as_durations, except That
        -apply_end_times_as_durations requires 2 inputs to be exactly matched,
        one event following the other.  The newer -multi_durations_from_offsets
        option allows for any follower event, and makes the older option
        unnecessary.

        If the condition to modify comes as the last event in a run, the
        program will whine and set that duration to 0.

           Consider example 20.

        See also -apply_end_times_as_durations.

   -multi_timing_to_events FILE : create event list from stimulus timing ~2~

        e.g. -multi_timing_to_events all.events.txt

        Decide which TR each stimulus event belongs to and make an event file
        (of TRs) containing a sequence of values between 0 (no event) and N
        (the index of the event class, for the N timing files).

        This option requires -tr, -multi_stim_dur, -min_frac and -run_len.

           Consider example 8.

   -multi_timing_to_event_pair Efile Ifile : break event file into 2 pieces ~2~

        e.g. -multi_timing_to_event_pair events.txt isi.txt

        Similar to -multi_timing_to_events, but break the output event file
        into 2 pieces, an event list and an ISI list.  Each event E followed by
        K zeros in the previous events file would be broken into a single E (in
        the new event file) and K+1 (in the ISI file).  Note that K+1 is
        appropriate from the assumption that events are 0-duration.  The ISI
        entries should sum to the total number of TRs per run.

        Suppose the event file shows 2 TRs of rest, event type 3 followed by 4
        TRs of rest, event type 1 followed by 1 TR of rest, type 2 and no rest,
        type 2 and 3 TRs of rest.  So it would read:

           all events:  0 0 3 0 0 0 0 1 0 2 2 0 0 0 ...

        Then the event_pair files would read:

           events:      0 3 1 2 2 ...
           ISIs:        2 5 2 1 4 ...

        Note that the only 0 events occur at the beginnings of runs.
        Note that the ISI is always at least 1, for the TR of the event.

        This option requires -tr, -multi_stim_dur, -min_frac and -run_len.

           Consider example 8b.

   -multi_timing_to_event_list STYLE FILE : make an event list file ~2~

        e.g. -multi_timing_to_event_list index events.txt
        e.g. -multi_timing_to_event_list GE:itodf event.list.txt

        Similar to -multi_timing_to_events, but make a more simple event list
        that does not require knowing the TR or run lengths.

        The output is written to FILE, where 'stdout' or '-' mean to write to
        the terminal window.

        The information and format is specified by the STYLE field:

           index        : write event index classes, in order, one row per run

           part         : partition the first class of events according to the
                          predecessor classes - the output is a list of class
                          indices for events the precede those of the first
                          class
                          (this STYLE is esoteric, written for W Tseng)

           GE:TYPE      : write a vertical list of events, according to TYPE

              TYPE is a list comprised of the following specifiers, where
              column output is in order specified (e.g. if i comes first, then
              the first column of output will be the class index).

                 i : event class index
                 p : previous event class index
                 t : event onset time
                 d : event duration
                 o : offset from previous event (including previous duration)
                 f : event class file name

      * note: -show_events is short for '-multi_timing_to_event_list GE:ALL -'
        See also -show_events.

------------------------------------------
general options: ~1~

   -chrono                      : process options chronologically ~2~

        This option has been removed.

   -force_write_type            : force output to have given modulators ~2~

        By default, or via other options, writing timing files with have
        amplitude modulators, duration modulators, neither or both.

        Use this option to force what is in the output timing files.

            simple  : no modulators
            AM      : only amplitude modulators (if none, use a=1)
            DM      : only duration modulators (if none, use d=0)
            AMDM    : both AM and DM (if missing a=1, d=0)

   -min_frac FRAC               : specify minimum TR fraction ~2~

        e.g. -min_frac 0.1

        This option applies to either -timing_to_1D action or -warn_tr_stats.

        For -warn_tr_stats (or -show), if the maximum tr fraction is below this
        limit, TRs are considered to be approximately TR-locked.

        For -timing_to_1D, when a random timing stimulus is converted to part
        of a 0/1 1D file, if the stimulus occupies at least FRAC of a TR, then
        that TR gets a 1 (meaning it is "on"), else it gets a 0 ("off").

        FRAC is required to be within [0,1], though clearly 0 is not very
        useful.  Also, 1 is not recommended unless that TR can be stored
        precisely as a floating point number.  For example, 0.1 cannot be
        stored exactly, so 0.999 might be safer to basically mean 1.0.

            Consider -timing_to_1D.

   -part_init NAME             : specify a default partition NAME ~2~

        e.g.     -part_init 2
        e.g.     -part_init frogs
        default: -part_init INIT

        This option applies to '-multi_timing_to_event_list part'.  In the
        case of generating a partition based on the previous events, this
        option allow the user to specify the partition class to be used when
        the class in question comes first (i.e. there is no previous event).

        The default class is the label INIT (the other classes will be
        small integers, from 1 to #inputs).

   -nplaces NPLACES             : specify # decimal places used in printing ~2~

        e.g. -nplaces 1

        This option allows the user to specify the number of places to the
        right of the decimal that are used when printing a stimulus time
        (to the screen via -show_timing or to a file via -write_timing).
        The default is -1, which uses the minimum needed for accuracy.

            Consider '-show_timing' and '-write_timing'.

   -mplaces NPLACES             : specify # places used for married fields ~2~

        e.g. -mplaces 1

        Akin to -nplaces, this option controls the number of places to the
        right of the decimal that are used when printing stimulus event
        modulators (amplitude and duration modulators).
        The default is -1, which uses the minimum needed for accuracy.

            Consider '-nplaces', '-show_timing' and '-write_timing'.

   -select_runs OLD1 OLD2 ... : make new timing from runs of an old one ~2~

        example a: Convert a single run into the second of 4 runs.

           -select_runs 0 1 0 0

        example b: Get the last 2 runs out of a 4-run timing file.

           -select_runs 3 4

        example c: Reverse the order of a 4 run timing file.

           -select_runs 4 3 2 1

        example d: Make a 6 run timing file, where they are all the same
                   as the original run 2, except the new run 4 is empty.

           -select_runs 2 2 2 0 2 2

        example e: Convert 3 runs into positions 4, 5 and 2 of 5 runs.
                   So 1 -> posn 4, 2 -> posn 5, and 3 -> posn 2.
                   The other 2 runs are empty.

           -select_runs 0 3 0 1 2


        Use this option to create a new timing element by selecting runs of an
        old one.  Runs are 1-based (from 1 to #runs), and 0 means to use an
        empty run (no events).  For example, if the original timing element has
        5 runs, then use 1..5 to select them, and 0 to select an empty run.

        Original runs can be any number of times, and in any order.

        The number of runs in the result is equal to the number of runs
        listed as parameters to this option.

            Consider '-nplaces', '-show_timing' and '-write_timing'.

   -per_run                     : perform relevant operations per run ~2~

        e.g. -per_run

        This option applies to -timing_to_1D, so that each 0/1 array is
        one row per run, as opposed to a single column across runs.

   -per_run_file                : per run, but output multiple files ~2~

        e.g. -per_run_file

        This option applies to -timing_to_1D, so that the 0/1 array goes in a
        separate file per run.  With -per_run, each run is just a separate row.

   -run_len RUN_TIME ...        : specify the run duration(s), in seconds ~2~

        e.g. -run_len 300
        e.g. -run_len 300 320 280 300

        This option allows the user to specify the duration of each run.
        If only one duration is provided, it is assumed that all runs are of
        that length of time.  Otherwise, the user must specify the same number
        of runs that are found in the timing files (one run per row).

        This option applies to both -timing and -multi_timing files.

        The run durations only matter for displaying ISI statistics.

            Consider '-show_isi_stats' and '-multi_show_isi_stats'.

   -show_events                 : see -multi_timing_to_event_list GE:ALL - ~2~

        This option, since it is so useful, it shorthand for

            -multi_timing_to_event_list GE:ALL -

        This option works for both -timing and -multi_timing.
        It is terminal.

        See also -multi_timing_to_event_list.

   -tr TR                       : specify the time resolution in 1D output ~2~
                                  (in seconds)
        e.g. -tr 2.0
        e.g. -tr 0.1

        For any action that write out 1D formatted data (currently just the
        -timing_to_1D action), this option is used to set the temporal
        resolution of the data.  For example, given -run_len 200 and -tr 0.5,
        one run would be 400 time points.

            Consider -timing_to_1D and -run_len.

   -tsv_labels L1 L2 ...        : specify column labels for TSV files ~2~

        e.g.     -tsv_labels onset RT response
        e.g.     -tsv_labels onset RT response gain loss
        e.g.     -tsv_labels 0 4 5 2 3
        default: -tsv_labels onset duration trial_type

        Use this option to specify columns to be used for:

           stimulus onset time
           stimulus duration
           stimulus class
           optionally: any amplitude modulators ...

        TSV (tab separated value) event timing files typically have column
        headers, including stimulus timing information such as event onset
        time, duration, stimulus type, response time, etc.  Unless specified,
        the default column headers that are processed are:

            onset duration trial_type

        But in some cases they do not exist, so the user must specify alternate
        headers (or indices).

        Columns can be specified by labels, or 0-based indices.

   -verb LEVEL                  : set the verbosity level ~2~

        e.g. -verb 3

        This option allows the user to specify how verbose the program is.
        The default level is 1, 0 is quiet, and the maximum is (currently) 4.

   -write_all_rest_times        : write all rest durations to 'timing' file ~2~

        e.g. -write_all_rest_times all_rest.txt

        In the case of a show_isi_stats option, the user can opt to save all
        rest (pre-stim, isi, post-stim) durations to a timing-style file.  Each
        row (run) would have one more entry than the number of stimuli (for
        pre- and post- rest).  Note that pre- and post- might be 0.

"""

g_help_trailer = """
-----------------------------------------------------------------------------
R Reynolds    December 2008
=============================================================================
"""

g_help_basis_string = """
=============================================================================
descriptions of various basis functions, as applied by 3dDeconvolve ~1~

-----------------------------------------------------------------------------
quick ~sorted listing (with grouping): ~2~

    BLOCK(d)                    : d-second convolved BLOCK function (def=BLOCK4)
    BLOCK(d,p)                  : d-second convolved BLOCK function, with peak=p
    dmBLOCK                     : duration modulated BLOCK
    dmUBLOCK                    : duration modulated BLOCK,
                                  with convolved Unit height
    BLOCK4(...)                 : explicitly use BLOCK4 shape (default)
    BLOCK5(...)                 : explicitly use BLOCK5 shape

    CSPLIN(b,c,n)               : n-param cubic spline,
                                  from time b to c sec after event
    CSPLINzero(b,c,n)           : same, but without the first and last params
                                  (i.e., an n-2 param cubic spline)

    EXPR(b,c) exp1 ... expn     : n-parm arbitrary expressions,
                                  from time b to c sec after event

    GAM                         : same as GAM(8.6,0.547)
    GAM(p,q)                    : 1 parameter gamma variate
                                  (t/(p*q))^p * exp(p-t/q)
    GAM(p,q,d)                  : GAM(p,q) with convolution duration d
    GAMpw(K,W)                  : GAM, with shape parameters K and W
    GAMpw(K,W,d)                : GAMpw, including duration d
                                  K = time to peak ; W = FWHM ; d = duration
    TWOGAM(p1,q1,r,p2,q2)       : GAM(p1,q1) - r*GAM(p2,q2)
    TWOGAMpw(K1,W1,r,K2,W2)     : GAMpw(K1,W1) - r*GAMpw(K2,W2)

    MION(d)                     : d-second convolution of h(t) =
                                      16.4486 * ( -0.184/ 1.5 * exp(-t/ 1.5)
                                                  +0.330/ 4.5 * exp(-t/ 4.5)
                                                  +0.670/13.5 * exp(-t/13.5) )
    MIONN(d)                    : negative of MION(d) (to get positive betas)

    POLY(b,c,n)                 : n-parameter Legendre polynomial expansion,
                                  from time b to c after event time

    SIN(b,c,n)                  : n-parameter sine series polynomial expansion,
                                  from time b to c after event time

    SPMG                        : same as SPMG2
    SPMG1                       : 1-parameter SPM gamma variate function
                                     exp(-t)*(A1*t^P1-A2*t^P2) where
                                     A1 = 0.0083333333  P1 = 5  (main lobe)
                                     A2 = 1.274527e-13  P2 = 15 (undershoot)
                                : approximately equal to
                                     TWOGAMpw(5,5.3,0.0975,15,9)
    SPMG2                       : 2-parameter SPM = SPMG1 + derivative
    SPMG3                       : 3-parameter SPM : SPMG2 + dispersion
    SPMG1(d)                    : SPMG1 convolved for duration d
    SPMG2(d)                    : SPMG2 convolved for duration d
    SPMG3(d)                    : SPMG3 convolved for duration d

    TENT(b,c,n)                 : n-parameter tent function,
                                  from time b to c after event time
    TENTzero(b,c,n)             : same, but without the first and last params
                                  (i.e., an n-2 param tent on reduced interval)

    WAV                         : same as WAV(0), the old waver -WAV function
    WAV(d)                      : WAV convolved for duration d
                                  equals WAV(d,2,4,6,0.2,2)
    WAV(d,D,R,F,Uf,Ur)          : fully specified WAV function

-----------------------------------------------------------------------------
more details for select functions: ~2~
-----------------------------------------------------------------------------
                                                                GAM ~3~

  GAM                   : same as GAM(p,q), where p=8.6, q=0.547
               duration : approx. 12 seconds
  GAM(p)                : INVALID
  GAM(p,q)              : (t/(p*q))^p * exp(p-t/q)
  GAM(p,q,d)            : convolve with d-second boxcar
               defaults : p=8.6, q=0.547
               duration : approx. 12+d seconds

                   peak : peak = 1.0, default peak @ t=4.7

  GAMpw(K,W,d)          : alternate parameterization of GAM
                          K = time to peak, W = FWHM, d = duration
               duration : ... will ponder ... (and add convolution dur d)
                   peak : K
                
  ------------------------------------------------------------
                                                                BLOCK ~3~

  BLOCK                 : INVALID on its own
                        : BLOCK is an integrated gamma variate function
                          g(t) = t^q * exp(-t) /(q^q*exp(-q))
                          (where q = 4 or 5, used in BLOCK4() or BLOCK5())

  BLOCK(d)              : stimulus duration d (convolve with d-second boxcar)
                   peak : peak of 1.0 (for d=1) @ t=4.5, max peak of ~5.1
               duration : approx. 15+d seconds
  BLOCK(d,p)            : stimulus duration d, peak p
                   peak : peak = p, @t~=4+d/2
  BLOCK4(...)           : default for BLOCK(...)
                          g(t) = t^4 * exp(-t) /(4^4*exp(-4))
  BLOCK5(...)           : g(t) = t^5 * exp(-t) /(5^5*exp(-5))

  ------------------------------------------------------------
                                       for duration modulation: dmBLOCK ~3~

  duration modulation - individual stimulus durations included in timing file

  dmBLOCK               : akin to BLOCK(d), where d varies per stimulus event
                   peak : peak ~= dur, for dur in [0,1]
                        : max ~= 5.1, as dur approaches 15
               duration : see BLOCK(d), approx 15+d seconds

                        *********************************************
  dmBLOCK(p)            * WARNING: basically do not use parameter p *
                        *********************************************
                  p = 0 : same as dmBLOCK
                  p < 0 : same as p=0, or dmBLOCK
                  p > 0 : all peaks equal to p, regardless of duration
                          (same as dmUBLOCK(p))

  dmUBLOCK              : basically equals dmBLOCK/5.1 (so max peak = 1)
                   peak : d=1:p=1/5.1, to max d=15:p=1 (i.e. BLOCK(d)/5.1)
               duration : see BLOCK(d), approx 15+d seconds

  dmUBLOCK(p)     p = 0 : same as dmUBLOCK, no need to use p=0
                  p < 0 : like p=0, but scale so peak = 1 @ dur=|p|
                          e.g. dmUBLOCK(-5) will have peak = 1.0 for a 5s dur,
                               i.e ~= dmBLOCK/4.0
                        : shorter events still have smaller peaks, longer still
                          have longer (up to the max at ~15 s)

                        **********************************************
                        * WARNING: basically do not use p > 0        *
                        *        - this generally does not match     *
                        *          what we expect of a BOLD response *
                        **********************************************
                  p > 0 : all peaks = p, regardless of duration
                          (same as dmBLOCK(p))

  ------------------------------------------------------------
                                                                TENT ~3~

  TENT(b,c,n)           : n tents/regressors, spanning b..c sec after stimulus
                        : half-tent at time b, half-tent at time c
                        : tents are centered at intervals of length (c-b)/(n-1)
                          --> so there are n-1 intervals for n tents
                   peak : peaks = 1 at interval centers
               duration : c-b seconds

  TENTzero(b,c,n)       : n-2 tents, same as above but ignoring first and last
                          --> akin to assuming first and last betas are 0
                        : same as TENT(b+v,c-v,n-2), where v = (c-b)/(n-1)

  ------------------------------------------------------------
                                                                CSPLIN ~3~

  CSPLIN(b,c,n)         : n-param cubic spline, from time b to c sec after event

  ------------------------------------------------------------
                                                                SPMG ~3~

  SPMG1                 : 1-regressor SPM gamma variate
               duration : positive lobe: 0..12 sec, undershoot: 12..24 sec
                   peak : 0.175 @ t=5.0, -0.0156 @ t=15.7

                        * Note that SPMG1 is pretty close to (a manually toyed
                          with and not mathematically derived (that would be
                          too useful)):

                             TWOGAMpw(5,5.3,0.0975,15,9)

                          However TWOGAMpw() scales to a peak of 1.

  SPMG1(d)              : SPMG1 convolved for a duration of d seconds.
                        * Convolved versions are scaled to a peak of 1.

  SPMG, SPMG2           : 2-regressor SPM gamma variate
                        : with derivative, to account for small temporal shift
  SPMG3                 : 3-regressor SPM gamma variate
                        : with dispersion curve

  ------------------------------------------------------------
                                                                WAV ~3~

  WAV                   : 1-regressor WAV function from waver
  WAV(d)                : convolves with stimulus duration d, in seconds
  WAV(d,D,R,F,Uf,Ur)    : includes D=delay time, R=rise time, F=fall time,
                          Uf=undershoot fraction, Ur=undershoot restore time
                        : defaults WAV(d,2,4,6,0.2,2)
        piecewise sum of:
             0.50212657 * ( tanh(tan(0.5*PI * (1.6*x-0.8))) + 0.99576486 )

               duration : stimulus duration d
                   peak : peak = 1, @t=d+6, or duration+delay+rise
             undershoot : fractional undershoot
               consider : WAV(1,1,3,8,0.2,2)
                          - similar to GAM, with subsequent undershoot

  ---------------------------------------------------------------------------
  example of plotting basis functions: ~3~

     With 200 time points at TR=0.1s, these are 20s curves.  The number of
     time points and TR will depend on what one wishes to plot.

     3dDeconvolve -nodata 200 0.1 -polort -1 -num_stimts 4      \\
        -stim_times 1 '1D:0' GAM                                \\
        -stim_times 2 '1D:0' 'WAV(1,1,3,8,0.2,2)'               \\
        -stim_times 3 '1D:0' 'BLOCK(1)'                         \\
        -stim_times 4 '1D:0' SPMG3                              \\
        -x1D X.xmat.1D -x1D_stop

     1dplot -sepscl X.xmat.1D

        OR, to be more complicated:

     1dplot -ynames GAM 'WAV(spec)' 'BLOCK(1)' SPMG_1 SPMG_2 SPMG_3 \\
        -xlabel 'tenths of a second' -sepscl X.xmat.1D

=============================================================================
"""

g_history = """
   timing_tool.py history:

   1.0  Dec 01, 2008 - initial/release version
   1.1  Jul 23, 2009 - added -partition option
   1.2  Sep 16, 2009 - added -scale_data
   1.3  Feb 20, 2010 - added -timing_to_1D, -tr and -min_frac
   1.4  Mar 17, 2010 - fixed timing_to_1D when some runs are empty
   1.5  Jun 09, 2010 - fixed partitioning without zeros
   1.6  Jul 11, 2010 - show TR offset stats if -tr and -show_isi_stats
   1.7  Jul 12, 2010 - added -truncate_times and -round_times
                       (added for S Durgerian)
   1.8  Aug 16, 2010 - use lib_textdata for I/O
   1.9  Oct 15, 2010
        - added -multi_timing_to_events, -multi_timing_to_event_pair, -per_run
          (added for N Adleman)
   1.10 Oct 16, 2010 - fixed timing_to_1D fractions
   1.11 Oct 21, 2010 - added -shift_to_run_offset
   1.12 Nov 19, 2010
        - moved write_to_timing_file to afni_util.py
        - added -write_all_rest_times (for J Poore)
   1.13 Dec 15, 2010
        - use lib_textdata.py for reading timing files
        - allow empty files as valid (for C Deveney)
   1.14 May 25, 2011 - added -global_to_local and -local_to_global (for G Chen)
   2.00 Oct 25, 2011 - process married files with current operations
        1. AfniMarriedTiming inherits from AfniData (instead of local copies)
        2. add all AfniTiming methods to AfniMarriedTiming (as married timing)
        3. rename AfniMarriedTiming back to AfniTiming (but now married)
   2.01 Oct 28, 2011 - allow use of -show_isi_stats w/o stim duration
   2.02 Oct 31, 2011 - added -show_tr_stats and -warn_tr_stats options
   2.03 Oct 03, 2012 - some options do not allow dashed parameters
   2.04 Feb 18, 2014 - added -test_local_timing, to check for local vs. global
        - in some cases, promote married types to combine/compare them
        - keep track of '*' entries from timing files
   2.05 Apr 24, 2014 - added -multi_timing_to_event_list
        - generates simple event lists, partitions, and detailed event lists
   2.06 Apr 29, 2014 - micro change to text output
   2.07 May  9, 2014
        - added -part_init option for WL Tseng
        - removed -chrono option
          (action items are still processed chronologically)
   2.08 May 12, 2014 - default -part_init to INIT (0 not valid for -partition)
   2.09 Sep 18, 2014 - added -help_basis to describe basis functions
                       (mostly to clarify dmBLOCK/dmUBLOCK)
   2.10 Oct 28, 2014 - expanded -help_basis (WAV, 3dDeconvolve, 1dplot)
   2.11 Jan 20, 2015 - allow ',' as married timing separator (with '*')
   2.12 Jun 05, 2015 - added -per_run_file
   2.13 Aug 21, 2015 - start-of-run fix to -multi_timing_to_event_list offsets
   2.14 Feb 24, 2016 - fix crash in -warn_tr_stats if no timing events
   2.15 Mar 15, 2016 - help_basis update: max of BLOCK() is ~5.1 (not 5.4)
   2.16 Aug  5, 2016 - added -marry_AM for J Wiggins
   2.17 Jan  9, 2017 - timediff for event list should use prev duration
   2.18 Aug 22, 2017 - -apply_end_times_as_durations and -show_duration_stats
   2.19 Aug 30, 2017 - added -fsl_timing_files and -write_as_married
   2.20 Sep 12, 2017
        - added -multi_timing_ncol_tsv, -write_multi_timing and
          -multi_show_duration_stats for TSV files
   3.00 Nov  9, 2017 - python3 compatible
   3.01 Dec 22, 2017 - added -select_runs and -mplaces
   3.02 Jan 31, 2018 - in MT2_event_list, if 'part' and no events in run, '* *'
   3.03 Sep 25, 2018 - fixed first timediff in -multi_timing_to_event_list
   3.04 Sep 27, 2018 - handle weakly formatted FSL timing files (fewer columns)
   3.05 Oct  5, 2018 - directly go after expected column headers in TSV files
   3.06 Feb 25, 2019 - added modulators to -multi_timing_to_event_list output
   3.07 Apr 22, 2019 - added -tsv_labels
   3.08 May 07, 2019 - added -timing_to_1D_warn_ok
   3.09 Jul 23, 2019 - added help and examples -tsv_labels
   3.10 Jul 24, 2019 - added -show_tsv_label_details
   3.11 Jul 29, 2019 - formatted -help for sphinx conversion
   3.12 Aug  8, 2019
        - ISI stats: allow and adjust for stim overlap
        - dur stats: show file/condition with stats
        - match output between python2 and python3
   3.13 Dec 26, 2019 - added -timing_to_1D_mods and -show_events
   3.14 Jul 22, 2021 - added -multi_durations_from_offsets
   3.15 Aug 20, 2021 - added -write_tsv_cols_of_interest
   3.16 Dec 29, 2021 - added -write_simple_tsv (and process mod_* columns)
   3.17 Jul 19, 2022 - added details to -help_basis
   3.18 Sep 20, 2022 - make -timing_to_1D overlap error more clear
   3.19 Jan  3, 2023 - fix -write_tsv_cols_of_interest with -tsv_labels
   3.20 Jan  4, 2023 - include -help_basis output in main -help
   3.21 Dec  4, 2023 - allow n/a in more tsv fields
   3.22 Nov 10, 2024 - add -show_tr_offset_stats, and consider -verb 0
   3.23 Dec 10, 2024 - add -show_modulator_stats
   3.24 Feb  6, 2025 - allow -multi_timing with -timing_to_1D
                     - add -timing_to_1D_method
   3.25 Jul 29, 2025 - add -force_write_type
"""

g_version = "timing_tool.py version 3.25, July 29, 2025"



class ATInterface:
   """interface class for AfniTiming"""
   def __init__(self, verb=1):
      # main variables
      self.status          = 0                       # exit value
      self.valid_opts      = None
      self.user_opts       = None
      self.all_edtypes     = ['i', 'p', 't', 'o', 'd', 'm', 'f']

      # user options
      self.force_w_type    = ''         # can force write: simple, AM, DM, AMDM
      self.nplaces         = -1         # num decimal places for writing
      self.mplaces         = -1         # decimal places for married info
      self.run_len         = [0]        # time per run (for single/multi)
      self.verb            = verb

      self.min_frac        = 0.3        # applies to timing_to_1D
      self.tr              = 0          # applies to some output
      self.per_run         = 0          # conversions done per run
      self.part_init       = 'INIT'     # default for -part_init
      self.t21D_warn_ok    = 0          # some timing_to_1D issues are non-fatal
      self.t21D_mods       = 0          # write modulators, rather than binary
      self.t21D_method     = 'bool'     # bool or sum when combining methods
      self.write_married   = 0          # for -write_as_married

      # user options - single var
      self.timing          = None       # main timing element
                                        #    AfniTiming:AfniData instance
      self.fname           = 'no file selected'
      self.all_rest_file   = ''         # for -write_all_rest_times
      self.stim_dur        = -1         # apply on read
      self.tsv_labels      = None       # labels to convert TSV to timing with
      self.tsv_def_dur_lab = None       # label for dur col if n/a
      self.tsv_show_details= 0          # show the TSV label info
      self.tsv_int         = None       # file to write TSV cols of int to

      # user options - multi var
      self.m_timing        = []         # AfniTiming:AfniData instance list
      self.m_fnames        = []
      self.m_stim_dur      = []

      # initialize valid_opts
      self.init_options()

   def set_timing(self, fname):
      """load a timing file, and init the main class elements"""

      self.status = 1 # init to failure
      timing = LT.AfniTiming(fname, dur=self.stim_dur, verb=self.verb)

      if not timing.ready:
         print("** failed to read timing from '%s'" % fname)
         return 1

      # success, so nuke and replace the old stuff

      if self.timing:
         if self.verb > 0:
            print("-- replacing old timing with that from '%s'" % fname)
         del(self.timing)
         del(self.fname)
         self.timing = None
         self.fname = None

      elif self.verb > 1: print("++ read timing from file '%s'" % fname)

      self.timing = timing
      self.fname = fname
      self.status = 0

      return 0

   def set_fsl_timing(self, flist):
      """load a list of FSL timing files (as a list of runs)"""

      self.status = 1 # init to failure
      timing = LT.AfniTiming(fsl_flist=flist, verb=self.verb)

      if not timing.ready:
         print("** failed to read FSL timing files")
         return 1

      # success, so nuke and replace the old stuff

      if self.timing:
         if self.verb > 0:
            print("-- replacing old timing with that from '%s'" % fname)
         del(self.timing)
         del(self.fname)
         self.timing = None
         self.fname = None

      self.timing = timing
      self.fname = flist[0]
      self.status = 0

      return 0

   def multi_set_timing(self, flist):
      """load multiple timing files"""

      if type(flist) != type([]):
         print('** multi_set_timing: list of files required')
         return 1

      if len(flist) < 1: return 0

      sdl = len(self.m_stim_dur)
      if sdl == 0:
         sdurs = [-1 for ind in range(len(flist))]
      elif sdl > 1 and sdl != len(flist):
         print('** length of stim times does not match # files (%d, %d)' % \
               (sdl, len(flist)))
         # set all durations to 0
         sdurs = [-1 for ind in range(len(flist))]
      elif sdl == 1:
         # duplicate duration for all stimuli
         sdurs = [self.m_stim_dur[0] for ind in range(len(flist))]
      else:
         # sdl > 1 and lengths are equal: so use what we have
         sdurs = self.m_stim_dur

      rdlist = []
      errs = 0
      for ind in range(len(flist)):
         name = flist[ind]
         timing = LT.AfniTiming(name, dur=sdurs[ind], verb=self.verb)
         if not timing.ready:
            print("** (multi) failed to read timing from '%s'\n" % name)
            errs += 1
            continue
         rdlist.append(timing)

      if errs: return 1

      # success, so nuke and replace the old stuff

      if self.m_timing:
         if self.verb > 0:
            print("-- replacing multi timing from %d files" % len(flist))
         del(self.m_timing)
         del(self.m_fnames)
         self.m_timing = []
         self.m_fnames = []

      elif self.verb > 1: print("++ read timing from %d files" % len(flist))

      self.m_timing = rdlist
      self.m_fnames = flist

      return 0

   def multi_timing_from_ncol_tsv(self, flist):
      """like multi_set_timing, fill self.mtiming and m_fnames
         do so from a list of 3 column tsv files

         init fnames from fprefix and name
      """

      if type(flist) != type([]):
         print('** multi_timing_from_ncol_tsv: list of files required')
         return 1

      if len(flist) < 1: return 0

      rv, timing_list = LT.read_multi_ncol_tsv(flist, hlabels=self.tsv_labels,
                             def_dur_lab=self.tsv_def_dur_lab,
                             show_only=self.tsv_show_details,
                             tsv_int=self.tsv_int, verb=self.verb)
      if rv: return 1

      self.m_timing = timing_list

      return 0

   def set_stim_dur(self, dur):
      """apply the stim duration to the timing element"""

      if type(dur) != float:
         print("** set_stim_dur: float required, have '%s'" % type(dur))
         return 1

      if dur < 0: return 0

      self.stim_dur = dur
      if self.timing: self.timing.init_durations(dur)

      if self.verb > 2: print('++ applying stim dur: %f' % dur)

   def multi_set_stim_durs(self, durs):
      """apply the stim durations to any multi-timing list"""

      if type(durs) != type([]):
         print('** multi_set_stim_durs: list of durations required')
         return 1

      sdl = len(durs)
      stl = len(self.m_timing)

      # if we have no timing elements, just save the list
      if stl < 1:
         self.m_stim_dur = durs
         return 0

      # now be sure durs matches the list length
      if sdl < 1: return 0
      elif sdl == 1:
         # duplicate duration for all stimuli
         sdurs = [durs[0] for ind in range(stl)]
      elif sdl != stl:
         print('** length of durs list does not match # elements (%d, %d)' % \
               (sdl, stl))
         return 1
      else:
         sdurs = durs

      # now store the list
      self.m_stim_dur = sdurs

      if self.verb > 2: print('++ applying multi stim durs: %s' % sdurs)

      for ind in range(stl):
         self.m_timing[ind].init_durations(sdurs[ind])

   def show_multi(self):
      print('==================== multi-timing list ====================\n')
      for rd in self.m_timing: rd.show()

   def write_timing(self, fname):
      """write the current timing out, with nplaces right of the decimal"""
      if not self.timing:
         print('** no timing to write')
         return 1
      # pass along any sub-class vars
      if self.force_w_type:
         self.timing.force_w_type = self.force_w_type
      return self.timing.write_times(fname, nplaces=self.nplaces,
                mplaces=self.mplaces, force_married=self.write_married)

   def write_multi_timing(self, prefix=''):
      """write the multi timing files out using the given prefix,
         with nplaces right of the decimal
      """
      if len(self.m_timing) < 1:
         print('** no multi_timing to write')
         return 1

      if prefix: pp = prefix
      else:      pp = 'mtiming.'
      for tind, timing in enumerate(self.m_timing):
         if   timing.fname: fname = prefix+timing.fname
         elif timing.name:  fname = prefix+timing.name+'.txt'
         else:              fname = '%sclass_%02d' % (pp, tind)

         # pass along any sub-class vars
         if self.force_w_type:
            timing.force_w_type = self.force_w_type

         timing.write_times(fname, nplaces=self.nplaces,
                    mplaces=self.mplaces, force_married=self.write_married)

   def write_simple_tsv(self, prefix='', suffix='', sep=''):
      """write the multi timing files out using the given prefix, but in a
         simple TSV format:
            onset duration [modulators] trial_type
         if sep == ''      : separate with a tab
         if sep == 'space' : align with spaces      ** TODO **
         else              : use sep
      """
      if len(self.m_timing) < 1:
         print('** no multi_timing to write')
         return 1

      if prefix == '' and suffix == '':
         prefix = 'events_'
      # make sure any prefix has a separator
      # (allow for -/stdout/stderr)
      if prefix != '' and prefix[-1] not in ['_','.'] \
                      and prefix     not in ['-', 'stdout', 'stderr']:
         prefix = prefix + '_'
      # and that any suffix has one
      if suffix != '' and suffix[0] not in ['_','.']:
         suffix = '_' + suffix

      # get complete list of events (get 0-based class_index values)
      # format: [ [ [time, [amp mods], dur, class_index] ..events.. ] ..runs.. ]
      allevents = self.complete_event_list(self.m_timing, nbased=0)
      lablist = self.get_multi_label_list()

      # get max number of modulators (we will print, so speed is not crucial)
      # nmods must be consistent per class, of course
      nmods = 0
      for rind, runevents in enumerate(allevents):
         maxmods = max(len(e[1]) for e in runevents)
         if maxmods > nmods: nmods = maxmods

      if self.verb > 2:
         print("-- write_simple_tsv: have %d runs of events\n"   \
               "   prefix '%s', suffix '%s', sep '%s', nmods %d" \
               % (len(allevents), prefix, suffix, sep, nmods))

      # new format of strings (each run is a rectancular string list)
      # [ [ [onset, dur, class_label, amp mods, ... ], ... ],  ... ]
      #                                         mods   events  runs
      allevents = self.event_list_to_strings(allevents, lablist, nmods)

      # and make matching header array
      header = ['onset', 'duration', 'trial_type']
      header.extend(['mod_%02d'%(mind+1) for mind in range(nmods)])

      # set default separator and extension
      if sep == '': sep = '\t'
      if sep == '\t': exten = '.tsv'
      else:           exten = '.txt'

      # note the number of digits used for run index
      if len(allevents) > 99: rdigits = 4
      else:                   rdigits = 2

      for rind, runevents in enumerate(allevents):
         fname = '%srun-%0*d%s%s' % (prefix, rdigits, rind+1, suffix, exten)

         if prefix in ['-', 'stdout']:
            oname = 'stdout'
            fp = sys.stdout
         elif prefix == 'stderr':
            oname = 'stderr'
            fp = sys.stderr
         else:
            oname = fname
            try: fp = open(fname, 'w')
            except:
               print('** failed to open %s for writing' % fname)
               return 1
         if self.verb > 1:
            print("++ write_simple_tsv: writing run %d to %s" % (rind+1,oname))

         estr = sep.join(header)
         fp.write(estr+'\n')
         for eind, event in enumerate(runevents):
            # todo: handle string alignment
            estr = sep.join(event)
            fp.write(estr+'\n')

         if fp in [sys.stdout, sys.stderr]:
            # if printing to screen, separate runs
            fp.write('\n')
         else:
            fp.close()

      return 0

   def get_multi_label_list(self, prefix=''):
      """return a list of class labels
            if timing.name is not unique, try timing.fname
            else make up from prefix
      """

      # check for short list
      nlab = len(self.m_timing)
      if nlab == 0:
         return []
      if nlab == 1:
         if prefix != '':
            return prefix
         else:
            return self.m_timing[0].name

      # pp is either empty or a '_' padded suffix
      if prefix.endswith('_') or prefix.endswith('.') :
         pp = '%s_' % prefix[:-1]
      else:
         pp = ''

      # get a list of unique labels
      lablist = [self.fname_prefix(t.name) for t in self.m_timing]
      if not UTIL.vals_are_unique(lablist):
         lablist = [self.fname_prefix(t.fname) for t in self.m_timing]
      if not UTIL.vals_are_unique(lablist):
         # create labels, and make sure prefix is not empty
         # (just use 1-based numbers)
         if pp == '':
            pp = 'class_'
         if nlab > 99: ndigs = 4
         else:         ndigs = 2
         lablist = ['%0*d' % (ndigs,ind+1) for ind in range(nlab)]


      return ['%s%s' % (pp,lab) for lab in lablist]

   def fname_prefix(self, fname):
      """return file name prefix
         if fname ends with known extension, return leading part
         else return fname
      """

      suffix = ''
      for known_suf in UTIL.g_text_file_suffix_list:
         if fname.endswith('.%s' % known_suf):
            suffix = known_suf
            break
      # if we found a suffix, strip it
      if suffix != '':
         return fname[:(len(suffix)+1)]
      return fname

   def init_options(self):
      self.valid_opts = OL.OptionList('valid opts')

      # short, terminal arguments
      self.valid_opts.add_opt('-help', 0, [],
                         helpstr='display program help')
      self.valid_opts.add_opt('-help_basis', 0, [],
                         helpstr='describe various basis functions')
      self.valid_opts.add_opt('-hist', 0, [],
                         helpstr='display the modification history')
      self.valid_opts.add_opt('-show_valid_opts', 0, [],
                         helpstr='display all valid options')
      self.valid_opts.add_opt('-ver', 0, [],
                         helpstr='display the current version number')

      # action options - single data
      self.valid_opts.add_opt('-add_offset', 1, [],
                         helpstr='offset all data by the given value')

      self.valid_opts.add_opt('-add_rows', 1, [],
                         helpstr='append the rows (runs) from the given file')

      self.valid_opts.add_opt('-apply_end_times_as_durations', 1, [],
                         helpstr='use as end times to apply as durations')

      self.valid_opts.add_opt('-extend', 1, [],
                         helpstr='extend the rows lengths from the given file')

      self.valid_opts.add_opt('-global_to_local', 1, [],
                         helpstr='convert global times to local and write')

      self.valid_opts.add_opt('-local_to_global', 1, [],
                         helpstr='convert local times to global and write')

      self.valid_opts.add_opt('-partition', 2, [],
                         helpstr='partition the events into multiple files')

      self.valid_opts.add_opt('-part_init', 1, [],
                         helpstr='initial index for event list part (def=0)')

      self.valid_opts.add_opt('-round_times', 1, [],
                         helpstr='round times up if past FRAC of TR')

      self.valid_opts.add_opt('-scale_data', 1, [],
                         helpstr='multiply all data by the given value')

      self.valid_opts.add_opt('-select_runs', -1, [],
                         helpstr='copy old runs to go into new timing')

      self.valid_opts.add_opt('-shift_to_run_offset', 1, [],
                         helpstr='shift each run to start at time OFFSET')

      self.valid_opts.add_opt('-show_duration_stats', 0, [],
                         helpstr='display min/mean/max/stdev of event durs')

      self.valid_opts.add_opt('-show_events', 0, [],
                         helpstr='display events list')

      self.valid_opts.add_opt('-show_timing', 0, [],
                         helpstr='display timing contents')

      self.valid_opts.add_opt('-sort', 0, [],
                         helpstr='sort the data, per row')

      self.valid_opts.add_opt('-test_local_timing', 0, [],
                         helpstr='check timing files for local timing issues')

      self.valid_opts.add_opt('-timing_to_1D', 1, [],
                         helpstr='convert stim_times to 0/1 stim_file')

      self.valid_opts.add_opt('-timing_to_1D_method', 1, [],
                         acplist=['bool', 'sum'],
                         helpstr='how to combine timing: sum, bool')

      self.valid_opts.add_opt('-timing_to_1D_mods', 0, [],
                         helpstr='write amplitude modulators, not binary')

      self.valid_opts.add_opt('-timing_to_1D_warn_ok', 0, [],
                         helpstr='make some conversion issues non-fatal')

      self.valid_opts.add_opt('-transpose', 0, [],
                         helpstr='transpose timing data (must be rectangular)')

      self.valid_opts.add_opt('-marry_AM', 1, [],
                         acplist=LT.g_marry_AM_methods,
                         helpstr='attach event modulators')

      self.valid_opts.add_opt('-truncate_times', 0, [],
                         helpstr='truncation times to multiple of TR')

      self.valid_opts.add_opt('-write_timing', 1, [],
                         helpstr='write timing contents to the given file')

      # (ending with matches for multi)
      self.valid_opts.add_opt('-timing', 1, [],
                         helpstr='load the given timing file')
      self.valid_opts.add_opt('-show_isi_stats', 0, [],
                         helpstr='show ISI stats for the main timing object')
      self.valid_opts.add_opt('-show_timing_ele', 0, [],
                         helpstr='display info about the main timing element')
      self.valid_opts.add_opt('-stim_dur', 1, [],
                         helpstr='provide a stimulus duration for main timing')

      # halfway case for FSL timing list
      self.valid_opts.add_opt('-fsl_timing_files', -1, [], okdash=0,
                         helpstr='load the given list of FSL timing files')

      # action options - multi
      self.valid_opts.add_opt('-multi_timing', -1, [], okdash=0,
                         helpstr='load the given list of timing files')
      self.valid_opts.add_opt('-multi_timing_3col_tsv', -1, [], okdash=0,
                         helpstr='load the 3 column TSV timing files')
      self.valid_opts.add_opt('-multi_timing_ncol_tsv', -1, [], okdash=0,
                         helpstr='load the N column TSV timing files')
      self.valid_opts.add_opt('-multi_show_duration_stats', 0, [],
                         helpstr='display min/mean/max/stdev of event durs')
      self.valid_opts.add_opt('-multi_show_isi_stats', 0, [],
                         helpstr='show ISI stats for load_multi_timing objs')
      self.valid_opts.add_opt('-multi_show_timing_ele', 0, [],
                         helpstr='display info about the multi timing elements')
      self.valid_opts.add_opt('-multi_stim_dur', -1, [],
                         helpstr='provide stimulus durations for timing list')
      self.valid_opts.add_opt('-multi_timing_to_events', 1, [],
                         helpstr='convert stim_times event file')
      self.valid_opts.add_opt('-multi_timing_to_event_pair', 2, [],
                         helpstr='convert stim_times event/isi files')
      self.valid_opts.add_opt('-multi_durations_from_offsets', 2, [],
                         helpstr='set durations for class from next events')
      self.valid_opts.add_opt('-multi_timing_to_event_list', 2, [],
                         helpstr='convert to event list (style, filename)')
      self.valid_opts.add_opt('-write_multi_timing', 1, [],
                         helpstr='write multi timing using the given prefix')
      self.valid_opts.add_opt('-write_simple_tsv', 1, [],
                         helpstr='write multi timing in simple TSV format')


      # general options (including multi)
      self.valid_opts.add_opt('-force_write_type', 1, [],
                         acplist=['simple', 'AM', 'DM', 'AMDM'],
                         helpstr='force timing file type')
      self.valid_opts.add_opt('-min_frac', 1, [],
                         helpstr='min tr fraction (in [0,1.0])')
      self.valid_opts.add_opt('-nplaces', 1, [],
                         helpstr='set number of decimal places for printing')
      self.valid_opts.add_opt('-mplaces', 1, [],
                         helpstr='number of decimal places for married info')
      self.valid_opts.add_opt('-per_run', 0, [],
                         helpstr='perform operations per run')
      self.valid_opts.add_opt('-per_run_file', 0, [],
                         helpstr='perform operations per run (one file each)')
      self.valid_opts.add_opt('-run_len', -1, [], okdash=0,
                         helpstr='specify the lengths of each run (seconds)')
      self.valid_opts.add_opt('-show_modulator_stats', 0, [],
                         helpstr='show stats of amplitude modulators')
      self.valid_opts.add_opt('-show_tr_offsets', 0, [],
                         helpstr='show stimulus times modulo the TR')
      self.valid_opts.add_opt('-show_tr_offset_stats', 0, [],
                         helpstr='show detailed fractional TR stats')
      self.valid_opts.add_opt('-show_tr_stats', 0, [],
                         helpstr='show fractional TR stats of timing')
      self.valid_opts.add_opt('-show_tsv_label_details', 0, [],
                         helpstr='show column labels from TSV file')
      self.valid_opts.add_opt('-write_tsv_cols_of_interest', 1, [],
                         helpstr='write applied TSV columns to new file')
      self.valid_opts.add_opt('-warn_tr_stats', 0, [],
                         helpstr='warn about bad fractional TR stats')
      self.valid_opts.add_opt('-tr', 1, [],
                         helpstr='specify output timing resolution (seconds)')
      self.valid_opts.add_opt('-tsv_def_dur_label', 1, [],
                         helpstr='specify label for duration column if d=n/a')
      self.valid_opts.add_opt('-tsv_labels', -1, [],
                         helpstr='specify labels for conversion from TSV')
      self.valid_opts.add_opt('-verb', 1, [],
                         helpstr='set the verbose level (default is 1)')
      self.valid_opts.add_opt('-write_all_rest_times', 1, [],
                         helpstr='in isi_stats, save rest durations to file')
      self.valid_opts.add_opt('-write_as_married', 0, [],
                         helpstr='attempt to write timing files as married')

      return 0

   def process_options(self):

      # process any optlist_ options
      self.valid_opts.check_special_opts(sys.argv)

      # process terminal options without the option_list interface

      if len(sys.argv) <= 1 or '-help' in sys.argv:
         print(g_help_string)
         print(g_help_basis_string)
         print(g_help_trailer)
         return 0

      if len(sys.argv) <= 1 or '-help_basis' in sys.argv:
         print(g_help_basis_string)
         return 0

      if '-hist' in sys.argv:
         print(g_history)
         return 0

      if '-show_valid_opts' in sys.argv:
         self.valid_opts.show('', 1)
         return 0

      if '-ver' in sys.argv:
         print(g_version)
         return 0

      # ============================================================
      # read options specified by the user
      self.user_opts = OL.read_options(sys.argv, self.valid_opts)

      # for convenience and popping
      uopts = copy.deepcopy(self.user_opts)

      if not uopts: return 1            # error condition

      # ------------------------------------------------------------
      # check general options, esp. chrono

      # ------------------------------------------------------------
      # process general options first (so -show options are still in order)

      oind = uopts.find_opt_index('-verb')
      if oind >= 0:
         val, err = uopts.get_type_opt(int, '-verb')
         if val != None and not err: self.verb = val
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-force_write_type')
      if oind >= 0:
         val, err = uopts.get_string_opt('-force_write_type')
         if val and not err:
            self.force_w_type = val
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-min_frac')
      if oind >= 0:
         val, err = uopts.get_type_opt(float, '-min_frac')
         if val and not err:
            self.min_frac = val
         if self.min_frac < 0.0 or self.min_frac > 1.0:
            print('** invalid -min_frac = %g' % self.min_frac)
            print('   (should be in [0,1])')
            return 1
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-nplaces')
      if oind >= 0:
         val, err = uopts.get_type_opt(int, '-nplaces')
         if val and not err:
            self.nplaces = val
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-mplaces')
      if oind >= 0:
         val, err = uopts.get_type_opt(int, '-mplaces')
         if val and not err:
            self.mplaces = val
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-part_init')
      if oind >= 0:
         val, err = uopts.get_string_opt('-part_init')
         if val and not err:
            self.part_init = val
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-per_run')
      if oind >= 0:
         if uopts.find_opt('-per_run'): self.per_run = 1
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-per_run_file')
      if oind >= 0:
         if uopts.find_opt('-per_run_file'): self.per_run = 2
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-tr')
      if oind >= 0:
         val, err = uopts.get_type_opt(float, '-tr')
         if val and not err:
            self.tr = val
            if self.tr <= 0.0:
               print('** invalid (non-positive) -tr = %g' % self.tr)
               return 1
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-tsv_def_dur_label')
      if oind >= 0:
         val, err = uopts.get_string_opt('-tsv_def_dur_label')
         if val and not err:
            self.tsv_def_dur_lab = val
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-tsv_labels')
      if oind >= 0:
         val, err = uopts.get_string_list('-tsv_labels')
         if type(val) == type([]) and not err:
            if self.verb > 2: print("-- setting labels as %s" % ', '.join(val))
            self.tsv_labels = val
         else: return 1
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-run_len')
      if oind >= 0:
         val, err = uopts.get_type_list(float, '-run_len')
         if type(val) == type([]) and not err:
            self.run_len = val
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-timing_to_1D_method')
      if oind >= 0:
         val, err = uopts.get_string_opt('-timing_to_1D_method')
         if not err: self.t21D_method = val
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-timing_to_1D_mods')
      if oind >= 0:
         if uopts.find_opt('-timing_to_1D_mods'): self.t21D_mods = 1
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-timing_to_1D_warn_ok')
      if oind >= 0:
         if uopts.find_opt('-timing_to_1D_warn_ok'): self.t21D_warn_ok = 1
         uopts.olist.pop(oind)

      # main timing options

      oind = uopts.find_opt_index('-timing')
      if oind >= 0:
         val, err = uopts.get_string_opt('-timing')
         if val and not err:
            if self.set_timing(val): return 1
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-stim_dur')
      if oind >= 0:
         val, err = uopts.get_type_opt(float, '-stim_dur')
         if val and not err:
            self.set_stim_dur(val)
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-fsl_timing_files')
      if oind >= 0:
         val, err = uopts.get_string_list('-fsl_timing_files')
         if type(val) == type([]) and not err:
            if self.set_fsl_timing(val): return 1
         else: return 1
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-multi_timing')
      if oind >= 0:
         val, err = uopts.get_string_list('-multi_timing')
         if type(val) == type([]) and not err:
            if self.multi_set_timing(val): return 1
         else: return 1
         uopts.olist.pop(oind)

      # --------------------------------------------------
      # like multi_timing, but from N column tsv files
      # (allow both ncol_tsv and 3col_tsv)
      oname = '-multi_timing_ncol_tsv'
      oind = uopts.find_opt_index(oname)
      if oind < 0:
         oname = '-multi_timing_3col_tsv'
         oind = uopts.find_opt_index(oname)

      if oind >= 0:
         # allow for just showing label info (but then no writing)
         if uopts.find_opt('-show_tsv_label_details'):
            self.tsv_show_details = 1
            if uopts.find_opt('-write_multi_timing'):
               print("** -show_tsv_label_details with -write_multi_timing" \
                     " is not valid")
               return 1

         # allow for writing TSV cols of interest
         if uopts.find_opt('-write_tsv_cols_of_interest'):
            val, err = uopts.get_string_opt('-write_tsv_cols_of_interest')
            if val and not err:
               self.tsv_int = val

         val, err = uopts.get_string_list(oname)
         if type(val) == type([]) and not err:
            if self.multi_timing_from_ncol_tsv(val): return 1
         else: return 1
         uopts.olist.pop(oind)
      # end '-multi_timing_ncol_tsv'
      # --------------------------------------------------

      oind = uopts.find_opt_index('-multi_stim_dur')
      if oind >= 0:
         val, err = uopts.get_type_list(float, '-multi_stim_dur')
         if type(val) == type([]) and not err:
            self.multi_set_stim_durs(val)
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-write_all_rest_times')
      if oind >= 0:
         val, err = uopts.get_string_opt('-write_all_rest_times')
         if val and not err:
            self.all_rest_file = val
         uopts.olist.pop(oind)

      oind = uopts.find_opt_index('-write_as_married')
      if oind >= 0:
         if uopts.find_opt('-write_as_married'): self.write_married = 1
         uopts.olist.pop(oind)

      # ------------------------------------------------------------
      # selection and process options:
      #    process sequentially, to make them like a script

      for opt in uopts.olist:

         #---------- action options, always chronological ----------

         # check multi- options first, then require self.timing

         if opt.name == '-multi_show_timing_ele':
            self.show_multi()
            continue

         elif opt.name == '-add_rows':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            val, err = uopts.get_string_opt('', opt=opt)
            if val != None and err: return 1

            newrd = LT.AfniTiming(val,dur=self.stim_dur,verb=self.verb)
            if not newrd.ready: return 1

            if self.timing.add_rows(newrd): return 1

         elif opt.name == '-extend':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            val, err = uopts.get_string_opt('', opt=opt)
            if val != None and err: return 1

            newrd = LT.AfniTiming(val,dur=self.stim_dur,verb=self.verb)
            if not newrd.ready: return 1

            if self.timing.extend_rows(newrd): return 1

         elif opt.name == '-partition':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            val, err = uopts.get_string_list('', opt=opt)
            if val != None and err: return 1

            if self.timing.partition(val[0], val[1]): return 1

         elif opt.name == '-add_offset':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            val, err = uopts.get_type_opt(float, opt=opt)
            if val != None and err: return 1
            if self.timing.add_val(val): return 1

         elif opt.name == '-apply_end_times_as_durations':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            val, err = uopts.get_string_opt('', opt=opt)
            if val != None and err: return 1

            # get end timing
            newrd = LT.AfniTiming(val,dur=self.stim_dur,verb=self.verb)
            if not newrd.ready: return 1

            if self.timing.apply_end_times_as_durs(newrd): return 1

         elif opt.name == '-scale_data':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            val, err = uopts.get_type_opt(float, opt=opt)
            if val != None and err: return 1
            if self.timing.scale_val(val): return 1

         elif opt.name == '-shift_to_run_offset':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            val, err = uopts.get_type_opt(float, opt=opt)
            if val != None and err: return 1
            if self.timing.shift_to_offset(val): return 1

         elif opt.name == '-select_runs':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            val, err = uopts.get_type_list(int, opt=opt)
            if val != None and err: return 1
            if self.timing.select_runs(val): return 1

         elif opt.name == '-round_times':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            if self.tr <= 0.0:
               print("** '%s' requires -tr" % opt.name)
               return 1
            val, err = uopts.get_type_opt(float, opt=opt)
            if val != None and err: return 1
            if self.timing.round_times(self.tr, round_frac=val): return 1

         elif opt.name == '-truncate_times':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            if self.tr <= 0.0:
               print("** '%s' requires -tr" % opt.name)
               return 1
            if self.timing.round_times(self.tr): return 1

         elif opt.name == '-marry_AM':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            val, err = uopts.get_string_opt('', opt=opt)
            if self.timing.marry_AM(val, self.run_len, nplaces=self.nplaces):
               return 1

         elif opt.name == '-sort':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            self.timing.sort()

         elif opt.name == '-show_duration_stats':
            if not self.timing:
               print("** '%s' requires -timing or -multi_timing" % opt.name)
               return 1
            if self.timing.show_duration_stats(): return 1

         elif opt.name == '-multi_durations_from_offsets':
            if len(self.m_timing) <= 0:
               print("** '%s' requires -multi_timing" % opt.name)
               return 1
            val, err = uopts.get_string_list('', opt=opt)
            if val != None and err: return 1

            self.multi_durations_from_offsets(val[0], ofile=val[1])

         elif opt.name == '-multi_show_duration_stats':
            if len(self.m_timing) <= 0:
               print("** '%s' requires -timing or -multi_timing" % opt.name)
               return 1
            for timing in self.m_timing:
               if timing.show_duration_stats(): return 1

         elif opt.name == '-show_timing':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            if self.verb > 0:
               print('# ++ timing (%d runs)\n' % len(self.timing.data))
            print(UTIL.make_timing_data_string(self.timing.data,
                          nplaces=self.nplaces, verb=self.verb))

         elif opt.name == '-show_isi_stats':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            if self.show_isi_stats(): return 1

         elif opt.name == '-multi_show_isi_stats':
            if len(self.m_timing) < 1:
               print("** '%s' requires -multi_timing" % opt.name)
               return 1
            if self.multi_show_isi_stats(): return 1

         elif opt.name == '-show_modulator_stats':
            if not self.timing and len(self.m_timing) == 0:
               print("** '%s' requires -timing or -multi_timing" % opt.name)
               return 1
            if self.show_modulator_stats(): return 1

         elif opt.name == '-show_tr_offsets':
            if self.show_tr_offsets(): return 1

         elif opt.name == '-show_tr_offset_stats':
            if not self.timing and len(self.m_timing) == 0:
               print("** '%s' requires -timing or -multi_timing" % opt.name)
               return 1
            if self.tr <= 0.0:
               print("** '%s' requires -tr" % opt.name)
               return 1
            if self.show_tr_offset_stats(): return 1

         elif opt.name == '-show_tr_stats':
            if not self.timing and len(self.m_timing) == 0:
               print("** '%s' requires -timing or -multi_timing" % opt.name)
               return 1
            if self.tr <= 0.0:
               print("** '%s' requires -tr" % opt.name)
               return 1
            if self.show_tr_stats(): return 1

         elif opt.name == '-warn_tr_stats':
            if not self.timing and len(self.m_timing) == 0:
               print("** '%s' requires -timing or -multi_timing" % opt.name)
               return 1
            if self.tr <= 0.0:
               print("** '%s' requires -tr" % opt.name)
               return 1
            if self.show_tr_stats(warn=1): return 1

         elif opt.name == '-test_local_timing':
            if not self.timing and len(self.m_timing) == 0:
               print("** '%s' requires -timing or -multi_timing" % opt.name)
               return 1
            self.test_local_timing()

         elif opt.name == '-timing_to_1D':
            if not self.timing and len(self.m_timing) == 0:
               print("** '%s' requires -timing or -multi_timing" % opt.name)
               return 1
            val, err = uopts.get_string_opt('', opt=opt)
            if val != None and err: return 1
            self.write_timing_as_1D(val)

         # pass event and ISI filenames
         elif opt.name == '-multi_timing_to_event_pair':
            if not self.m_timing:
               print("** '%s' requires -multi_timing" % opt.name)
               return 1
            val, err = uopts.get_string_list('', opt=opt)
            if val != None and err: return 1
            self.multi_timing_to_events(val[0], val[1])

         # just pass the event filename
         elif opt.name == '-multi_timing_to_events':
            if not self.m_timing:
               print("** '%s' requires -multi_timing" % opt.name)
               return 1
            val, err = uopts.get_string_opt('', opt=opt)
            if val != None and err: return 1
            self.multi_timing_to_events(val)

         # just pass the event filename
         elif opt.name == '-multi_timing_to_event_list':
            if not self.m_timing:
               print("** '%s' requires -multi_timing" % opt.name)
               return 1
            val, err = uopts.get_string_list('', opt=opt)
            if val != None and err: return 1
            self.multi_timing_to_event_list(val[1], style=val[0])

         # ** a convenience option, because the above is soooo useful
         elif opt.name == '-show_events':
            # if -timing, cheat and use multi_timing
            if self.timing and not self.m_timing:
               if self.multi_set_timing([self.fname]): return 1
            if not self.m_timing:
               print("** '%s' requires -multi_timing" % opt.name)
               return 1
            self.multi_timing_to_event_list('-', style='GE:ALL')
            # terminal
            return 0

         elif opt.name == '-transpose':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            if self.timing.transpose(): return 1

         elif opt.name == '-show_timing_ele':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            self.timing.show()

         elif opt.name == '-show_tsv_label_details':
            # already processed
            pass

         elif opt.name == '-write_tsv_cols_of_interest':
            # already processed
            pass

         # this is a write option
         elif opt.name == '-global_to_local':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            val, err = uopts.get_string_opt('', opt=opt)
            if val != None and err: return 1
            if self.global_to_local(): return 1
            self.write_timing(val)

         elif opt.name == '-local_to_global':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            val, err = uopts.get_string_opt('', opt=opt)
            if val != None and err: return 1
            if self.local_to_global(): return 1
            self.write_timing(val)

         elif opt.name == '-write_timing':
            if not self.timing:
               print("** '%s' requires -timing" % opt.name)
               return 1
            val, err = uopts.get_string_opt('', opt=opt)
            if val != None and err: return 1
            self.write_timing(val)

         elif opt.name == '-write_multi_timing':
            if len(self.m_timing) <= 0:
               print("** '%s' requires -multi_timing*" % opt.name)
               return 1
            val, err = uopts.get_string_opt('', opt=opt)
            if val != None and err: return 1
            self.write_multi_timing(val)

         elif opt.name == '-write_simple_tsv':
            # allow for single timing input
            single2multi = 0
            if len(self.m_timing) == 0 and self.timing is not None:
               self.m_timing = [self.timing]
               single2mluti = 1
            if len(self.m_timing) == 0:
               print("** '%s' requires -multi_timing*" % opt.name)
               return 1
            val, err = uopts.get_string_opt('', opt=opt)
            if val != None and err: return 1
            self.write_simple_tsv(val)
            if single2multi: self.m_timing = []

         else:
            print('** unknown option: %s' % opt.name)
            return 1

      return 0

   def multi_durations_from_offsets(self, cname, ofile='-'):
      """akin to -apply_end_times_as_durations, but set duration based on
         any next events

         cname  : the name of the condition to set durations for
                  (currently required to match some timing.fname)
         ofile  : output file to write new timing to

         Build a new mdata for self.m_timing[cindex] and write it out.
         Put the old one back in place.
      """

      # which m_timing class does this apply to?
      cindex = self.tindex_from_fname(cname)
      if cindex < 0:
         print("** multi_durations_from_offsets: cannot find fname %s" % cname)
         return 1
      elif self.verb > 1:
         print("-- MDFO: using index %d for class %s" % (cindex,cname))

      # get complete list of events (get 0-based class_index values)
      # format: [ [ [time, [amp mods], dur, class_index] ..events.. ] ..runs.. ]
      allevents = self.complete_event_list(self.m_timing, nbased=0)

      # build a new mdata
      mdata = []
      for irun, erun in enumerate(allevents):
         mrun = []
         maxind = len(erun)-1
         for ievent, event in enumerate(erun):
            if event[3] == cindex:
               # what to do if last event?
               if ievent < maxind:
                  dur = erun[ievent+1][0] - event[0]
               else:
                  if self.verb > 0:
                     print("** MDFO: event %s is last in run %d"%(cname,irun))
                  dur = 0
               # add event
               mrun.append([event[0], event[1], dur])
               if self.verb > 3:
                  print("== event @ %7.2f, old dur %5.2f, new dur %5.2f" \
                        % (event[0],event[2],dur))
         mdata.append(mrun)

      # make a new timing instance to write from married data, and write
      timing = self.m_timing[cindex].copy()
      timing.init_from_mdata(mdata)

      timing.write_times(ofile, nplaces=self.nplaces,
                 mplaces=self.mplaces, force_married=1)

      # delete created instance
      del(timing)

      return 0

   def tindex_from_fname(self, fname):
      """return index of fname into m_timing
         return -1 on error
      """
      for tind, timing in enumerate(self.m_timing):
         if timing.fname == fname:
            return tind
      if self.verb > 1:
         print("** tindex_from_fname: cannot find fname %s" % fname)
      return -1

   def complete_event_list(self, tlist, nbased=1):
      """return all events across all runs

         tlist:  probably self.m_timing
         nbased: 0 or 1, for 0-based or 1-based

         return [
                  [ [time, [amp mods], dur, class_index] ... ],
                  [ [time, [amp mods], dur, class_index] ... ],
                  ... more runs
                  [ [time, [amp mods], dur, class_index] ... ]
                ]
      """

      # from timing list, set number of conditions and number of runs

      if nbased not in [0,1]:
         print("** complete_event_list: bad nbased %d" % nbased)
         return []

      ncond = len(tlist)
      if ncond < 1:
         if self.verb > 1: print("** complete_event_list, no conditions")
         return []

      nruns = len(tlist[0].mdata)
      if nruns < 1:
         if self.verb > 1: print("** complete_event_list, no runs")
         return []

      # get all events per run

      allruns = []
      for rind in range(nruns):
         runevents = [] # complete event list for current run
         for index, timing in enumerate(tlist):
             md = copy.deepcopy(timing.mdata[rind])
             for event in md:
                 # nbased should be 0 or 1
                 event.append(index+nbased)
             # now events are: [time, [amp mods], dur, class_index]
             runevents.extend(md)

         runevents.sort() # sort by times, which are the first elements
         allruns.append(runevents)

      return allruns


   def event_list_to_strings(self, allevents, labels, nmods):
      """similar to complete_event_list, return all events across all runs

            BUT: Each run's event list is a rectangular array of strings of
                 the form:

                  return [
                           [ [onset, dur, class_label, amp mods...], ... ]
                              ... more runs ...
                           [ [onset, dur, class_label, amp mods...], ... ]
                         ]

            allevents: from complete_event_list(nbased=0)
            labels:    labels to match class_index values
            nmods:     maximum modulators across classes

         The return format matches that of complete_event_list(), except that:

            - all values are strings
            - class index will be replaced with label[index]
            - all events will have the same number of amp_mods
              (using '0' for any missing)
            - all run lists are the same rectanbular size
            - the order matches tsv output order
      """

      all_str_events = []
      for runevents in allevents:
         str_run = []
         for event in runevents:
            # init with onset, dur and label
            onset = '%s' % event[0]
            dur = '%s' % event[2]
            label = labels[event[3]]
            se = [onset, dur, label]

            # and fill with needed mods
            se.extend(['%s'%m for m in event[1]])
            mext = nmods - len(event[1])
            if mext > 0:
               se.extend(['0' for i in range(mext)])

            # append event to str_run
            str_run.append(se)

         # append run to all_str_events
         all_str_events.append(str_run)

      return all_str_events

   def multi_timing_to_event_list(self, fname='stdout', style='index'):
      """convert multi-stim_times to 1..N sorted event list

         style:
            index       write as a per-run sorted event list 1..N
            part        partition first class events according to predecessors
                        (so 2 CLASS_1 elements in a row is an error)
                        if first element, default to follow first other type
            GE:TYPE     write as global events, comments between runs, with:
                        TYPE = i        : event class index
                               p        : previous event class index
                               t        : event time
                               o        : offset from previous
                               d        : stim duration
                               f        : event class file name
                             ALL        : use all valid types
                        e.g. itf        - for index, time and file name
      """

      tlist = self.m_timing     # convenience
      ncond = len(tlist)

      if ncond < 1:
         print('** no multi_timing, cannot convert')
         return 1
      if style not in ['index', 'part'] and style[0:3] != 'GE:':
         print('** invalid style %s' % style)
         return 1

      # check for partition styles
      if style == 'part' and ncond < 2:
         print('** event_list partition requires at least 2 timing inputs')
         return 1

      # check for GE:TYPE style
      s1d_type = ''
      if style[0:3] == 'GE:':
         s1d_type = style[3:]
         if s1d_type != 'ALL':
            for st in s1d_type:
               if st not in self.all_edtypes:
                  print("** invalid GE: event style '%s' in %s" % (st, style))
                  return 1

      if fname in ['-', 'stdout']: fp = sys.stdout
      else:
         try: fp = open(fname, 'w')
         except:
            print('** failed to open %s for writing' % fname)
            return 1

      nruns = len(tlist[0].mdata)

      # test nruns
      for index, timing in enumerate(tlist):
         if len(timing.mdata) != nruns:
            print('** have variable runs between timing files %s and %s' \
                  % (tlist[0].fname, timing.fname))
            return 1

      # get to work
      for rind in range(nruns):
         allevents = [] # complete event list for current run
         etlist = []   # if printing event types, accumulate first
         for index, timing in enumerate(tlist):
             md = copy.deepcopy(timing.mdata[rind])
             for event in md:
                 event.append(index+1)
             # now events are: [time, [amp mods], dur, class_index]
             allevents.extend(md)

         allevents.sort() # sort by times, which are the first elements

         if s1d_type and self.verb > 0:
            fp.write('# have %d events across %d conditions in run %d\n' \
                     % (len(allevents), ncond, rind+1))
            etlist.append(self.make_s1d_ehdr_list(s1d_type))

         # and output
         nrevents = 0   # count events in this run; if 0 output * *
         for eind, event in enumerate(allevents):
            cind = event[3]
            # track previous class and event times
            if eind == 0:
               cprev = self.part_init
               tprev = 0
               dprev = 0
            else:
                cprev = allevents[eind-1][3]
                tprev = allevents[eind-1][0]
                dprev = allevents[eind-1][2]

            if s1d_type != '':
               etlist.append(self.make_s1d_estr_list(s1d_type,cind,cprev=cprev,
                                   etime=event[0], tprev=tprev, dur=event[2],
                                   pdur=dprev, amods=event[1]))
            elif style == 'index': fp.write('%d ' % cind)
            elif style == 'part':
               if cind != 1: continue # only write predecessors of class 1
               if cprev == 1 and self.verb > 0:
                  print('** run %d, event %d: class %d preceded by class %s' \
                        % (rind, eind, cind, cprev))
               fp.write('%s ' % cprev)
               nrevents += 1
            else:
               print('** invalid style %s' % style)
               return 1

         # if no events, write as empty run
         if style == 'part' and nrevents == 0:
            fp.write('* *')

         # done with current run, print global events, if we have them
         # (could join() them, but they vary in width)
         if len(etlist) > 0:
            clens = self.get_et_col_widths(etlist)
            for eline in etlist:
               for eind, estr in enumerate(eline):
                  if eind: fp.write('  ')
                  fp.write('%-*s' % (clens[eind], estr))
               fp.write('\n')

         fp.write('\n')

      if fp != sys.stdout: fp.close()

      return 0

   def get_et_col_widths(self, estr_list):
      """for each column, return the maximum element length"""
      if len(estr_list) < 1: return []
      elen = len(estr_list[0])
      if elen < 1: return []

      wlist = [max([len(elist[ind]) for elist in estr_list]) \
                                    for ind in range(elen)]

      return wlist

   def make_s1d_estr_list(self, stypes, cind=0, cprev=0,
                             etime=0.0, tprev=0.0, dur=0.0, pdur=0.0,
                             amods=[], maxfilelen=10):
      tlist = self.m_timing

      # apply special case of ALL for types
      if stypes == 'ALL': etypes = self.all_edtypes
      else:               etypes = stypes

      elist = []
      for st in etypes:
         if   st == 'i': astr = '%d' % cind
         elif st == 'p': astr = '%s' % cprev
         elif st == 'f': astr = '%-*s' % (maxfilelen,tlist[cind-1].fname)
         elif st == 'd': astr = '%8.3f' % dur
         elif st == 'o':
            offset = etime-tprev-pdur
            # first event is now handled in list construction
            # if tprev == 0.0: offset = etime
            if offset == 0.0:
                         astr = '   0    '
            else:        astr = '%8.3f' % offset
         elif st == 't': astr = '%8.3f' % etime
         elif st == 'm':
            astr = '*'.join(['%s'%mm for mm in amods])
         else:
            print('** invalid GE: style %s' % style)
            return []
         elist.append(astr)

      return elist

   def make_s1d_ehdr_list(self, stypes):

      # apply special case of ALL for types
      if stypes == 'ALL': etypes = self.all_edtypes
      else:               etypes = stypes

      elist = []
      for sind, st in enumerate(etypes):
         if   st == 'i': astr = 'class'
         elif st == 'p': astr = 'prev_class'
         elif st == 'f': astr = 'stim_file'
         elif st == 'd': astr = 'duration'
         elif st == 'o': astr = 'timediff'
         elif st == 't': astr = 'event_time'
         elif st == 'm': astr = 'amp_mods'
         else:
            print('** invalid GE: style %s' % style)
            return []
         if sind == 0: elist.append('# ' + astr)
         else:         elist.append(astr)

      return elist


   def multi_timing_to_events(self, fname, wname=None):
      """convert multi-stim_times to 0..N event list
         If wname is not set, write event list to fname.
         Otherwise, write collapsed event list and ISI list files.
      """

      errs = 0
      if len(self.m_timing) < 1:
         print('** no multi_timing, cannot convert')
         errs += 1

      if not fname:
         print('** multi_timing_to_events: missing filename')
         errs += 1

      if self.tr <= 0.0:
         print('** error: -tr must be positive')
         errs += 1

      if len(self.run_len) < 2 and self.run_len[0] == 0:
         print('** mulit_timing_to_events requires -run_len')
         errs += 1

      if self.min_frac <= 0.0 or self.min_frac > 1.0:
         print('** error: -min_frac must be in (0.0, 1.0]')
         errs += 1

      if errs: return 1

      amtlist = []
      for index, timing in enumerate(self.m_timing):
         errstr, result = timing.timing_to_1D(self.run_len, self.tr,
                                              self.min_frac, self.per_run)
         if errstr:
            print(errstr)
            return 1
         if self.verb > 3:
            print('++ event list %d : %s' % (index+1, result))
         amtlist.append(result)

      err, combo = self.combine_multi_1D_lists(amtlist)
      if err: return 1

      # maybe this is all the users wants
      if not wname:
         TD.write_1D_file(combo, fname, self.per_run)
         return 0

      err, stimlist, waitlist = self.combo_to_event_and_wait(combo)
      if err: return 1

      TD.write_1D_file(stimlist, fname, self.per_run)
      TD.write_1D_file(waitlist, wname, self.per_run)

      return 0

   def combo_to_event_and_wait(self, combo):
      """return a list of events (0 for leading rest) and waiting periods
         (TRs, including events, until next event)"""

      if type(combo[0]) == type([]):    # then process as list of runs
         elist = []
         wlist = []
         for llist in combo:
            rv, ee, ww = self.single_combo_to_EW(llist)
            if rv: return 1, [], []
            elist.append(ee)
            wlist.append(ww)
      else:                             # process as one long list
         rv, elist, wlist = self.single_combo_to_EW(combo)
         if rv: return 1, [], []

      return 0, elist, wlist

   def single_combo_to_EW(self, combo):
      """return a list of events (0 for leading rest) and waiting periods
         (TRs, including events, until next event)"""

      elist = []
      wlist = []
      eposn = 0
      for ind, val in enumerate(combo):
         if val == 0:
            if ind == 0:        # we are at the beginnng of the run
               elist.append(0)
               wlist.append(1)
            else:               # wait longer for next event (common)
               wlist[-1] += 1
         else:                  # a new event
            elist.append(val)
            wlist.append(1)

      return 0, elist, wlist

   def combine_multi_1D_lists(self, amtlist):
      """combine multiple 0/1 lists into event lists
            - if each list is 2 dimensional, work per row
      """

      if self.verb > 1:
         print('++ creating combo from %d event lists' % len(amtlist))

      if type(amtlist[0][0]) == type([]):
         combo = []
         # test lengths
         L0 = amtlist[0]
         for L in amtlist:
            if len(L) != len(L0):
               print('** CM1L: length mismatch: %d vs %d' % (len(L0), len(L)))
               return 1, []
            for rind in range(len(L)):
               if len(L[rind]) != len(L0[rind]):
                  print('** CM1L: row length mismatch at row %d' % rind)
                  return 1, []
         if self.verb > 2:
            print('-- combining event lists per run, %d lists of length %d' \
                  % (len(amtlist), len(L[0])))
         # put each row together
         for rind in range(len(L0)):
            result = self.combine_1D_lists([L[rind] for L in amtlist])
            if result == None: return 1, []
            combo.append(result)
      else:
         if self.verb > 2: print('-- combining events lists as single sequence')
         combo = self.combine_1D_lists(amtlist)

      if self.verb > 2:
         print('++ have combined TR list: %s' % combo)

      return 0, combo

   def combine_1D_lists(self, tlist):
      """combine these lists of events into a composite, ordered by
         their indices

         there should be no overlap
      """

      if len(tlist) < 1:  return []
      if len(tlist) == 1: return tlist[0]

      combo = [0 for val in tlist[0]]
      # and fill with every other
      for lind, llist in enumerate(tlist):
         for vind, val in enumerate(llist):
            if val != 0:
               if combo[vind] != 0:
                  print('** event duplication between lists %d and %d at' \
                        ' index %d' % (combo[vind], lind+1, vind))
               combo[vind] = lind+1     # insert 1-based list index here

      return combo

   def write_timing_as_1D(self, fname):
      """convert stim_times to 0/1 format"""

      if not self.timing and len(self.m_timing) == 0:
         print('** no timing, cannot convert to 1D')
         return 1

      if not fname:
         print('** write_timing_as_1D: missing filename')
         return 1

      if self.tr <= 0.0:
         print('** error: -tr must be positive')
         return 1

      if len(self.run_len) < 2 and self.run_len[0] == 0:
         print('** timing_to_1D requires -run_len')
         return 1

      if self.min_frac <= 0.0 or self.min_frac > 1.0:
         print('** error: -min_frac must be in (0.0, 1.0]')
         return 1

      # process each timing element (whether single or multi)
      # and take the union of results
      if len(self.m_timing) > 0:
         if self.per_run:
            print('** cannot use multi timing_to_1D per run')
            return 1
         tlist = self.m_timing
      else:
         tlist = [self.timing]

      # start with first timing element
      errstr, result = tlist[0].timing_to_1D(self.run_len, self.tr,
                            self.min_frac, self.per_run,
                            allow_warns=self.t21D_warn_ok,
                            write_mods=self.t21D_mods)

      # now process all of the rest
      tind = 1
      while errstr == '' and tind < len(tlist):
          errstr, res = tlist[tind].timing_to_1D(self.run_len, self.tr,
                            self.min_frac, self.per_run,
                            allow_warns=self.t21D_warn_ok,
                            write_mods=self.t21D_mods)
          if errstr: break
          if len(res) != len(result):
             print("** write_timing_as_1D: result length diff")
             print("   len %d=%d, len %d=%d" \
                   % (0, len(result), tind, len(res)))
             return 1

          # merge res into result (keep 'method' out of loop?)
          if self.t21D_method == 'sum':
             for vind, val in enumerate(res):
                if val:
                    result[vind] += val
          else: # default of 'bool'
             for vind, val in enumerate(res):
                if val:
                    result[vind] += val
          del(res)

          tind += 1

      if errstr:
         print(errstr)
         return 1

      # maybe one file per run

      if self.per_run == 2:
         prefix = fname
         if fname.endswith('.1D') and len(fname) > 3:
            prefix = fname[:fname.find('.1D')]
         for run, res in enumerate(result):
            TD.write_1D_file(res, '%s_r%02d.1D'%(prefix,run+1))
         return

      TD.write_1D_file(result, fname, self.per_run)

   def global_to_local(self):
      """convert global times to local, based in run_len array
         return 0 on success, 1 on any error"""

      if not self.timing:
         print('** no timing, cannot convert to local')
         return 1

      if len(self.run_len) < 2 and self.run_len[0] == 0:
         print('** global_to_local requires -run_len')
         return 1

      return self.timing.global_to_local(self.run_len)

   def local_to_global(self):
      """convert local times to global, based in run_len array
         return 0 on success, 1 on any error"""

      if not self.timing:
         print('** no timing, cannot convert to global')
         return 1

      if len(self.run_len) < 2 and self.run_len[0] == 0:
         print('** local_to_global requires -run_len')
         return 1

      return self.timing.local_to_global(self.run_len)

   def show_isi_stats(self):
      if not self.timing:
         print('** no timing, cannot show stats')
         return 1

      rv = self.timing.show_isi_stats(mesg='single element',
              run_len=self.run_len, tr=self.tr, rest_file=self.all_rest_file)
      if rv and self.verb > 2:
         self.timing.make_data_string(nplaces=self.nplaces,
                                      mesg='SHOW ISI FAILURE')

      return 0

   def multi_show_isi_stats(self):
      if len(self.m_timing) < 1:
         print('** no multi-timing, cannot show stats')
         return 1

      amt = self.m_timing[0].copy()

      nele = len(self.m_timing)
      for ind in range(1, nele):
         amt.extend_rows(self.m_timing[ind])

      if self.verb > 2: amt.show('final AMT')

      rv = amt.show_isi_stats(mesg='%d elements'%nele, run_len=self.run_len,
                              tr=self.tr, rest_file=self.all_rest_file)
      if rv and self.verb > 2:
         print(amt.make_data_string(nplaces=self.nplaces,mesg='ISI FAILURE'))

      return 0

   def show_tr_offsets(self):
      """show output from get_TR_offsets()
      """

      if not self.timing:
         print('** no timing, cannot show stats')
         return 1

      if self.tr <= 0.0:
         print("** show_tr_stats requires -tr  (> 0)")
         return 1

      offsets = self.timing.get_TR_offsets(self.tr)
      if len(offsets) <= 0:
         return 1

      # convert to strings
      soff = ['%g'%t for t in offsets]

      # print the soff vals, depending on verbosity
      if self.verb == 0:
         jstr = ' '
         print(jstr.join(soff))
      else:
         jstr = '\n   '
         print("offsets:")
         print(jstr + jstr.join(soff))
         print()

      return 0

   def show_tr_stats(self, warn=0):
      """show results from get_TR_offset_stats()
            if warn, only output warnings (where max is below min_frac)
      """

      if self.min_frac >= 0 and self.min_frac < 1: frac = self.min_frac
      else:                                        frac = 0.3

      if not self.timing and len(self.m_timing) == 0:
         print('** no timing, cannot show stats')
         return 1

      if self.tr <= 0.0:
         print("** show_tr_stats requires -tr")
         return 1

      # either way, process as a list
      tlist = self.m_timing
      if len(tlist) == 0: tlist = [self.timing]

      for timing in tlist:
         rv, rstr = timing.get_TR_offset_stats_str(self.tr, mesg=timing.fname,
                                                   wlimit=frac)
         if rv or not warn: print(rstr)

      return 0

   def show_modulator_stats(self):
      """show statistics for amplitude modulators in timing files
      """

      if not self.timing and len(self.m_timing) == 0:
         print('** no timing, cannot show stats')
         return 1

      # do per run if verbose
      perrun = self.verb > 0

      # either way, process as a list
      tlist = self.m_timing
      if len(tlist) == 0: tlist = [self.timing]

      for timing in tlist:
         rv, rstr = timing.modulator_stats_str(perrun=perrun)
         print(rstr)

      return rv

   def show_tr_offset_stats(self):
      """show results from detailed_TR_offset_stats_str()
      """

      if self.min_frac >= 0 and self.min_frac < 1: frac = self.min_frac
      else:                                        frac = 0.3

      if not self.timing and len(self.m_timing) == 0:
         print('** no timing, cannot show stats')
         return 1

      if self.tr <= 0.0:
         print("** show_tr_stats requires -tr")
         return 1

      # do per run if verbose
      perrun = self.verb > 0

      # either way, process as a list
      tlist = self.m_timing
      if len(tlist) == 0: tlist = [self.timing]

      for timing in tlist:
         rv, rstr = timing.detailed_TR_offset_stats_str(self.tr, perrun=perrun)
         print(rstr)

      return rv

   def test_local_timing(self):
      """test timing files for known issues
            - looks local, but might be treated as global
      """

      # just start with everything that might be a timing element
      if self.timing: tlist = [self.timing]
      else:           tlist = []
      tlist.extend(self.m_timing)

      if len(tlist) == 0:
         print('** no timing/multi_timing, nothing to test')
         return 1

      errs = 0
      for timing in tlist:
         errs += timing.looks_local_but_3dD_global(warn=1)

      return errs

   def test(self, verb=3):
      # init
      print('------------------------ initial reads -----------------------')
      self.verb = verb
      # these should not fail, so quit if they do
      # first try AFNI_data4, then regression data
      if self.set_timing('X.xmat.1D'):
         return None

      # reset
      print('------------------------ reset files -----------------------')

      # failures
      print('------------------------ should fail -----------------------')
      self.set_timing('noxmat')

      # more tests
      return None

def compare_events(e0, e1):
   if   e0[0] < e1[0]: return -1
   elif e0[0] > e1[0]: return 1
   else:               return 0

def main():
   ard = ATInterface()
   if not ard: return 1

   rv = ard.process_options()
   if rv > 0: return 1

   return ard.status

if __name__ == '__main__':
   sys.exit(main())


