docs: def ffconvert(fname, limit_states, ff, min_iml=1E-10):
      """
      Convert a fragility function into a numpy array plus a bunch
      of attributes.
  
      :param fname: path to the fragility model file
      :param limit_states: expected limit states
      :param ff: fragility function node
      :returns: a pair (array, dictionary)
      """
      with context(fname, ff):
          ffs = ff[1:]
          imls = ff.imls
      nodamage = imls.attrib.get('noDamageLimit')
      if nodamage == 0:
          # use a cutoff to avoid log(0) in GMPE.to_distribution_values
          logging.warning('Found a noDamageLimit=0 in %s, line %s, '
                          'using %g instead', fname, ff.lineno, min_iml)
          nodamage = min_iml
      with context(fname, imls):
          attrs = dict(format=ff['format'],
                       imt=imls['imt'],
                       id=ff['id'],
                       nodamage=nodamage)
  
      LS = len(limit_states)
      if LS != len(ffs):
          with context(fname, ff):
              raise InvalidFile('expected %d limit states, found %d' %
                                (LS, len(ffs)))
      if ff['format'] == 'continuous':
          minIML = float(imls['minIML'])
          if minIML == 0:
              # use a cutoff to avoid log(0) in GMPE.to_distribution_values
              logging.warning('Found minIML=0 in %s, line %s, using %g instead',
                              fname, imls.lineno, min_iml)
              minIML = min_iml
          attrs['minIML'] = minIML
          attrs['maxIML'] = float(imls['maxIML'])
          array = numpy.zeros(LS, [('mean', F64), ('stddev', F64)])
          for i, ls, node in zip(range(LS), limit_states, ff[1:]):
              if ls != node['ls']:
                  with context(fname, node):
                      raise InvalidFile('expected %s, found' %
                                        (ls, node['ls']))
              array['mean'][i] = node['mean']
              array['stddev'][i] = node['stddev']
      elif ff['format'] == 'discrete':
          attrs['imls'] = ~imls
          valid.check_levels(attrs['imls'], attrs['imt'], min_iml)
          num_poes = len(attrs['imls'])
          array = numpy.zeros((LS, num_poes))
          for i, ls, node in zip(range(LS), limit_states, ff[1:]):
              with context(fname, node):
                  if ls != node['ls']:
                      raise InvalidFile('expected %s, found' %
                                        (ls, node['ls']))
                  poes = (~node if isinstance(~node, list)
                          else valid.probabilities(~node))
                  if len(poes) != num_poes:
                      raise InvalidFile('expected %s, found' %
                                        (num_poes, len(poes)))
                  array[i, :] = poes
      # NB: the format is constrained in nrml.FragilityNode to be either
      # discrete or continuous, there is no third option
      return array, attrs