///|
/// Each source hypothesis maps to a nonempty disjoint block of the target.
/// Blocks must partition the target. No probabilities are invented within a
/// block: every focal is extended to the union of its corresponding blocks.
pub fn Mass::refine(
  self : Mass,
  target : Frame,
  blocks : Array[Int32],
  budget~ : Limits = default_limits(),
) -> Mass raise BeliefError {
  if blocks.length() != self.domain.size() {
    raise Invalid("refinement requires one block per source hypothesis")
  }
  let mut covered = 0
  for block in blocks {
    target.check_mask(block)
    if block == 0 || (covered & block) != 0 {
      raise Invalid("refinement blocks must be nonempty and disjoint")
    }
    covered = covered | block
  }
  if covered != target.universe() {
    raise Invalid("refinement blocks must partition target frame")
  }
  budget.check_dense(target)
  let bins = Array::make(target.universe() + 1, 0.0)
  for (mask, value) in self.focals {
    let mut mapped = 0
    for i = 0; i < blocks.length(); i = i + 1 {
      if (mask & (1 << i)) != 0 {
        mapped = mapped | blocks[i]
      }
    }
    bins[mapped] = value
  }
  // A partition induces an injective focal mapping, so no merging is needed.
  from_bins(target, bins, budget)
}

///|
/// Derive the inverse category map from a valid refinement partition.
pub fn Frame::refinement_inverse(
  self : Frame,
  target : Frame,
  blocks : Array[Int32],
) -> Array[Int32] raise BeliefError {
  let _ = vacuous(self).refine(target, blocks)
  let result = Array::make(target.size(), 0)
  for coarse = 0; coarse < blocks.length(); coarse = coarse + 1 {
    for fine = 0; fine < target.size(); fine = fine + 1 {
      if (blocks[coarse] & (1 << fine)) != 0 {
        result[fine] = coarse
      }
    }
  }
  result
}