diff --git a/src/main/java/gov/usgs/earthquake/nshmp/model/ClusterRuptureSet.java b/src/main/java/gov/usgs/earthquake/nshmp/model/ClusterRuptureSet.java
index b17de10cc833448ad910515919f457ebdb07be02..85c9e9ece965efe154344d0309ec936f65fe802c 100644
--- a/src/main/java/gov/usgs/earthquake/nshmp/model/ClusterRuptureSet.java
+++ b/src/main/java/gov/usgs/earthquake/nshmp/model/ClusterRuptureSet.java
@@ -8,12 +8,16 @@ import static java.util.stream.Collectors.toList;
 
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
 import java.util.function.Predicate;
 
+import gov.usgs.earthquake.nshmp.data.XySequence;
 import gov.usgs.earthquake.nshmp.geo.Location;
 import gov.usgs.earthquake.nshmp.geo.json.Feature;
 import gov.usgs.earthquake.nshmp.mfd.Mfd;
-import gov.usgs.earthquake.nshmp.tree.LogicGroup;
+import gov.usgs.earthquake.nshmp.mfd.Mfd.Type;
+import gov.usgs.earthquake.nshmp.tree.Branch;
 import gov.usgs.earthquake.nshmp.tree.LogicTree;
 
 /**
@@ -36,13 +40,36 @@ import gov.usgs.earthquake.nshmp.tree.LogicTree;
  */
 public class ClusterRuptureSet extends AbstractRuptureSet<ClusterSource> {
 
+  /*
+   * Developer notes:
+   *
+   * A ClusterRuptureSet is composed of two or more FaultRuptureSets (fault
+   * sections) that rupture together according to some rate or logic tree of
+   * rates. Each nested FaultRuptureSet itself contains a logic tree of SINGLE
+   * MFDs, usually labelled 'M1', 'M2', etc. The MFDs for each FaultRuptureSet
+   * are the same size and have identical weights. Technically, the M1 fault
+   * branches across each cluster should be considered together, the M2 fault
+   * branches together, etc. in the cluster hazard calculation. However, the
+   * magnitude branch exceedance curves are currently collapsed prior to doing
+   * the cluster calculation.
+   *
+   * For the purposes of reporting an MFD tree for a ClusterRuptureSet, we
+   * create MFDs for each magnitude branch spanning the individual sections in
+   * the cluster. In some cases there may be identical magnitudes in a cluster
+   * such that the cluster rate reflected in the MFD may be doubled or more.
+   *
+   */
+
+  @Deprecated
   final ModelData data; // holds cluster rate tree
   private final List<ClusterSource> source;
+  private final LogicTree<Mfd> mfdTree;
 
   private ClusterRuptureSet(Builder builder) {
     super(builder);
     this.data = builder.data;
     this.source = List.of(builder.source);
+    this.mfdTree = builder.mfdTree;
   }
 
   @Override
@@ -65,23 +92,7 @@ public class ClusterRuptureSet extends AbstractRuptureSet<ClusterSource> {
 
   @Override
   public LogicTree<Mfd> mfdTree() {
-    /*
-     * A ClusterSource is composed of multiple FaultRuptureSets that rupture
-     * together at some rate. For the purposes of reporting an MFD tree for the
-     * rupture set, we combine the SINGLE MFDs associated with each
-     * FaultRuptureSet and set the rate of each magnitude to its weight. We then
-     * build a logic group where the weight (scale) of each branch is set to the
-     * cluster rate.
-     */
-    ClusterSource clusterSource = source.get(0);
-    double rate = clusterSource.rate();
-
-    LogicGroup.Builder<Mfd> builder = LogicGroup.builder();
-    for (RuptureSet<? extends Source> ruptureSet : clusterSource.ruptureSets()) {
-      Mfd mfd = ModelTrees.reduceMfdTree(ruptureSet.mfdTree());
-      builder.addBranch(ruptureSet.name(), mfd, rate);
-    }
-    return builder.build();
+    return mfdTree;
   }
 
   /*
@@ -168,6 +179,7 @@ public class ClusterRuptureSet extends AbstractRuptureSet<ClusterSource> {
 
     private ModelData data; // holds cluster rate-tree
     private ClusterSource source;
+    private LogicTree<Mfd> mfdTree;
 
     private Builder() {
       super(SourceType.FAULT_CLUSTER);
@@ -205,8 +217,37 @@ public class ClusterRuptureSet extends AbstractRuptureSet<ClusterSource> {
       checkNotNull(source, "%s cluster source", label);
     }
 
+    LogicTree<Mfd> createMfdTree() {
+      LogicTree.Builder<Mfd> builder = LogicTree.builder("cluster-mfd");
+      LogicTree<Mfd> model = source.ruptureSets().get(0).mfdTree();
+      /* loop magnitude branches */
+      for (int i = 0; i < model.size(); i++) {
+        /* collect cluster magnitudes and rate; all M have cluster rate */
+        Map<Double, Double> magMap = new TreeMap<>();
+        String magId = model.get(i).id();
+        double magWeight = model.get(i).weight();
+        for (RuptureSet<? extends Source> ruptureSet : source.ruptureSets()) {
+          Branch<Mfd> mfd = ruptureSet.mfdTree().get(i);
+          /* check magnitude branch IDs and weights match */
+          checkState(mfd.id().equals(magId));
+          checkState(mfd.value().properties().type() == Type.SINGLE);
+          magMap.merge(
+              mfd.value().data().x(0),
+              source.rate(),
+              (v1, v2) -> v1 + v2);
+        }
+        XySequence mfdXy = XySequence.create(
+            magMap.keySet(),
+            magMap.values());
+        Mfd clusterMfd = Mfd.create(mfdXy);
+        builder.addBranch(magId, clusterMfd, magWeight);
+      }
+      return builder.build();
+    }
+
     ClusterRuptureSet build() {
       validateAndInit("ClusterRuptureSet.Builder");
+      mfdTree = createMfdTree();
       return new ClusterRuptureSet(this);
     }
   }