docs: def calc_hazard_curves(
          groups, ss_filter, imtls, gsim_by_trt, truncation_level=None,
          apply=sequential_apply, filter_distance='rjb', reqv=None):
      """
      Compute hazard curves on a list of sites, given a set of seismic source
      groups and a dictionary of ground shaking intensity models (one per
      tectonic region type).
  
      Probability of ground motion exceedance is computed in different ways
      depending if the sources are independent or mutually exclusive.
  
      :param groups:
          A sequence of groups of seismic sources objects (instances of
          of :class:`~openquake.hazardlib.source.base.BaseSeismicSource`).
      :param ss_filter:
          A source filter over the site collection or the site collection itself
      :param imtls:
          Dictionary mapping intensity measure type strings
          to lists of intensity measure levels.
      :param gsim_by_trt:
          Dictionary mapping tectonic region types (members
          of :class:`openquake.hazardlib.const.TRT`) to
          :class:`~openquake.hazardlib.gsim.base.GMPE` or
          :class:`~openquake.hazardlib.gsim.base.IPE` objects.
      :param truncation_level:
          Float, number of standard deviations for truncation of the intensity
          distribution.
      :param apply:
          apply function to use (default sequential_apply)
      :param filter_distance:
          The distance used to filter the ruptures (default rjb)
      :param reqv:
          If not None, an instance of RjbEquivalent
      :returns:
          An array of size N, where N is the number of sites, which elements
          are records with fields given by the intensity measure types; the
          size of each field is given by the number of levels in ``imtls``.
      """
      # This is ensuring backward compatibility i.e. processing a list of
      # sources
      if not isinstance(groups[0], SourceGroup):  # sent a list of sources
          odic = groupby(groups, operator.attrgetter('tectonic_region_type'))
          groups = [SourceGroup(trt, odic[trt], 'src_group', 'indep', 'indep')
                    for trt in odic]
      # ensure the sources have the right src_group_id
      for i, grp in enumerate(groups):
          for src in grp:
              if src.src_group_id is None:
                  src.src_group_id = i
      imtls = DictArray(imtls)
      param = dict(imtls=imtls, truncation_level=truncation_level,
                   filter_distance=filter_distance, reqv=reqv,
                   cluster=grp.cluster)
      pmap = ProbabilityMap(len(imtls.array), 1)
      # Processing groups with homogeneous tectonic region
      gsim = gsim_by_trt[groups[0][0].tectonic_region_type]
      mon = Monitor()
      for group in groups:
          if group.atomic:  # do not split
              it = [classical(group, ss_filter, [gsim], param, mon)]
          else:  # split the group and apply `classical` in parallel
              it = apply(
                  classical, (group.sources, ss_filter, [gsim], param, mon),
                  weight=operator.attrgetter('weight'))
          for dic in it:
              for grp_id, pval in dic['pmap'].items():
                  pmap |= pval
      sitecol = getattr(ss_filter, 'sitecol', ss_filter)
      return pmap.convert(imtls, len(sitecol.complete))