docs: def _load_time_variables(layout, dataset=None, columns=None, scan_length=None,
                           drop_na=True, events=True, physio=True, stim=True,
                           regressors=True, skip_empty=True, scope='all',
                           **selectors):
      ''' Loads all variables found in *_events.tsv files and returns them as a
      BIDSVariableCollection.
  
      Args:
          layout (BIDSLayout): A BIDSLayout to scan.
          dataset (NodeIndex): A BIDS NodeIndex container. If None, a new one is
              initialized.
          columns (list): Optional list of names specifying which columns in the
              event files to read. By default, reads all columns found.
          scan_length (float): Optional duration of runs (in seconds). By
              default, this will be extracted from the BOLD image. However, in
              cases where the user doesn't have access to the images (e.g.,
              because only file handles are locally available), a fixed duration
              can be manually specified as a fallback.
          drop_na (bool): If True, removes all events where amplitude is n/a. If
              False, leaves n/a values intact. Note that in the latter case,
              transformations that requires numeric values may fail.
          events (bool): If True, extracts variables from events.tsv
              files.
          physio (bool): If True, extracts variables from _physio files.
          stim (bool): If True, extracts variables from _stim files.
          skip_empty (bool): Whether or not to skip empty Variables (i.e.,
              where there are no rows/records in a file, or all onsets,
              durations, and amplitudes are 0).
          scope (str, list): The scope of the space to search for variables. See
              docstring for BIDSLayout for details and valid predefined values.
          selectors (dict): Optional keyword arguments passed onto the
              BIDSLayout instance's get() method; can be used to constrain
              which data are loaded.
  
      Returns: A NodeIndex instance.
      '''
  
      # Extract any non-keyword arguments
      kwargs = selectors.copy()
  
      if dataset is None:
          dataset = NodeIndex()
  
      selectors['datatype'] = 'func'
      selectors['suffix'] = 'bold'
      images = layout.get(return_type='object', extensions='.nii.gz',
                          scope=scope, **selectors)
  
      if not images:
          raise ValueError("No functional images that match criteria found.")
  
      # Main loop over images
      for img_obj in images:
  
          entities = img_obj.entities
          img_f = img_obj.path
  
          # Run is not mandatory, but we need a default for proper indexing
          if 'run' in entities:
              entities['run'] = int(entities['run'])
  
          tr = layout.get_metadata(img_f, suffix='bold', scope=scope,
                                   full_search=True)['RepetitionTime']
  
          # Get duration of run: first try to get it directly from the image
          # header; if that fails, try to get NumberOfVolumes from the
          # run metadata; if that fails, look for a scan_length argument.
          try:
              import nibabel as nb
              img = nb.load(img_f)
              duration = img.shape[3] * tr
          except Exception as e:
              if scan_length is not None:
                  duration = scan_length
              else:
                  msg = ("Unable to extract scan duration from one or more "
                         "BOLD runs, and no scan_length argument was provided "
                         "as a fallback. Please check that the image files are "
                         "available, or manually specify the scan duration.")
                  raise ValueError(msg)
  
          run = dataset.get_or_create_node('run', entities, image_file=img_f,
                                           duration=duration, repetition_time=tr)
          run_info = run.get_info()
  
          # Process event files
          if events:
              dfs = layout.get_nearest(img_f, extensions='.tsv', suffix='events',
                                        all_=True, full_search=True,
                                        ignore_strict_entities=['suffix'])
              if dfs is not None:
                  for _data in dfs:
                      _data = pd.read_csv(_data, sep='\t')
                      if 'amplitude' in _data.columns:
                          if (_data['amplitude'].astype(int) == 1).all() and \
                                  'trial_type' in _data.columns:
                              msg = ("Column 'amplitude' with constant value 1 "
                                     "is unnecessary in event files; ignoring "
                                     "it.")
                              _data = _data.drop('amplitude', axis=1)
                          else:
                              msg = ("Column name 'amplitude' is reserved; "
                                     "renaming it to 'amplitude_'.")
                              _data = _data.rename(
                                  columns={'amplitude': 'amplitude_'})
                          warnings.warn(msg)
  
                      _data = _data.replace('n/a', np.nan)  # Replace BIDS' n/a
                      _data = _data.apply(pd.to_numeric, errors='ignore')
  
                      _cols = columns or list(set(_data.columns.tolist()) -
                                              {'onset', 'duration'})
  
                      # Construct a DataFrame for each extra column
                      for col in _cols:
                          df = _data[['onset', 'duration']].copy()
                          df['amplitude'] = _data[col].values
  
                          # Add in all of the run's entities as new columns for
                          # index
                          for entity, value in entities.items():
                              if entity in ALL_ENTITIES:
                                  df[entity] = value
  
                          if drop_na:
                              df = df.dropna(subset=['amplitude'])
  
                          if df.empty:
                              continue
  
                          var = SparseRunVariable(name=col, data=df, run_info=run_info,
                                                  source='events')
                          run.add_variable(var)
  
          # Process confound files
          if regressors:
              sub_ents = {k: v for k, v in entities.items()
                          if k in BASE_ENTITIES}
              confound_files = layout.get(suffix='regressors', scope=scope,
                                          **sub_ents)
              for cf in confound_files:
                  _data = pd.read_csv(cf.path, sep='\t', na_values='n/a')
                  if columns is not None:
                      conf_cols = list(set(_data.columns) & set(columns))
                      _data = _data.loc[:, conf_cols]
                  for col in _data.columns:
                      sr = 1. / run.repetition_time
                      var = DenseRunVariable(name=col, values=_data[[col]],
                                             run_info=run_info, source='regressors',
                                             sampling_rate=sr)
                      run.add_variable(var)
  
          # Process recordinging files
          rec_types = []
          if physio:
              rec_types.append('physio')
          if stim:
              rec_types.append('stim')
  
          if rec_types:
              rec_files = layout.get_nearest(img_f, extensions='.tsv.gz',
                                             all_=True, suffix=rec_types,
                                             ignore_strict_entities=['suffix'],
                                             full_search=True)
              for rf in rec_files:
                  metadata = layout.get_metadata(rf)
                  if not metadata:
                      raise ValueError("No .json sidecar found for '%s'." % rf)
                  data = pd.read_csv(rf, sep='\t')
                  freq = metadata['SamplingFrequency']
                  st = metadata['StartTime']
                  rf_cols = metadata['Columns']
                  data.columns = rf_cols
  
                  # Filter columns if user passed names
                  if columns is not None:
                      rf_cols = list(set(rf_cols) & set(columns))
                      data = data.loc[:, rf_cols]
  
                  n_cols = len(rf_cols)
                  if not n_cols:
                      continue
  
                  # Keep only in-scan samples
                  if st < 0:
                      start_ind = np.floor(-st * freq)
                      values = data.values[start_ind:, :]
                  else:
                      values = data.values
  
                  if st > 0:
                      n_pad = freq * st
                      pad = np.zeros((n_pad, n_cols))
                      values = np.r_[pad, values]
  
                  n_rows = int(run.duration * freq)
                  if len(values) > n_rows:
                      values = values[:n_rows, :]
                  elif len(values) < n_rows:
                      pad = np.zeros((n_rows - len(values), n_cols))
                      values = np.r_[values, pad]
  
                  df = pd.DataFrame(values, columns=rf_cols)
                  source = 'physio' if '_physio.tsv' in rf else 'stim'
                  for col in df.columns:
                      var = DenseRunVariable(name=col, values=df[[col]], run_info=run_info,
                                             source=source, sampling_rate=freq)
                      run.add_variable(var)
      return dataset